feat(rag): honest deflection gate with amber UI state and alternative-question chips
This commit is contained in:
@@ -64,6 +64,22 @@ def retrieve(
|
||||
]
|
||||
|
||||
|
||||
def weak_hit_titles(chunks: Sequence[RetrievedChunk]) -> list[str]:
|
||||
"""Distinct parent-document titles of *chunks*, best chunk score first.
|
||||
|
||||
Deflection mode (PLAN §6, A8) is built from these *titles only* — the
|
||||
LOW prompt and the "Maybe try" chips never see document content.
|
||||
"""
|
||||
titles: list[str] = []
|
||||
seen: set[uuid.UUID] = set()
|
||||
for rc in sorted(chunks, key=lambda c: c.score, reverse=True):
|
||||
if rc.document.id in seen:
|
||||
continue
|
||||
seen.add(rc.document.id)
|
||||
titles.append(rc.document.title)
|
||||
return titles
|
||||
|
||||
|
||||
def select_documents(
|
||||
chunks: Sequence[RetrievedChunk],
|
||||
n: int | None = None,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Deflection suggestions — the "Maybe try" chips under a deflected answer.
|
||||
|
||||
v1 behavior (phase 04, honest-deflection story): chips are derived
|
||||
deterministically from the weak-hit document titles, so Brain only ever
|
||||
points the user at topics it actually has indexed — never invented ones.
|
||||
A model-generated list could layer on top later; the title-derived path
|
||||
is the shipped, testable one (PLAN §6: deflection offers 2-3 alternative
|
||||
questions about things the docs DO cover).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
#: The ``done`` event carries at most this many alternative questions.
|
||||
MAX_SUGGESTIONS = 3
|
||||
|
||||
|
||||
def derive_suggestions(
|
||||
titles: Sequence[str],
|
||||
fallback: Sequence[str] = (),
|
||||
max_n: int = MAX_SUGGESTIONS,
|
||||
) -> list[str]:
|
||||
"""Build the alternative-question chips for a deflected turn.
|
||||
|
||||
One chip per weak-hit title (*titles* arrive in best-chunk-score
|
||||
order from :func:`app.rag.retriever.weak_hit_titles`), phrased as a
|
||||
question the knowledge base can ground. If fewer than *max_n* titles
|
||||
are available, *fallback* (the onboarding suggestions) tops the list
|
||||
up so the user still gets 2-3 real options. Whitespace is normalized,
|
||||
duplicates (case-insensitive) are dropped, and the result contains
|
||||
only non-empty strings — at most *max_n* of them.
|
||||
"""
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def push(item: str) -> None:
|
||||
item = " ".join(item.split())
|
||||
if not item:
|
||||
return
|
||||
key = item.lower()
|
||||
if key in seen:
|
||||
return
|
||||
seen.add(key)
|
||||
out.append(item)
|
||||
|
||||
for title in titles:
|
||||
if len(out) >= max_n:
|
||||
break
|
||||
title = " ".join(title.split())
|
||||
if not title:
|
||||
continue
|
||||
push(f"What's in your notes about {title}?")
|
||||
for question in fallback:
|
||||
if len(out) >= max_n:
|
||||
break
|
||||
push(question)
|
||||
return out
|
||||
Reference in New Issue
Block a user