58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
"""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
|