"""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