Spaces:
Sleeping
fix(fact-find): KI-012/013/014 — flow corrections from user testing
Browse filesKI-012 (P0) — Bot stuck in fact_find_complete readback loop
After fact-find completed, the bot kept emitting the same readback
summary instead of answering user's policy questions. Audit transcript
showed P002 wasted 19 of 30 turns echoing the profile back.
Fix: set session.free_form_session=True on completion + flush.
KI-013 (P0) — Bot recommends senior-citizen policy to non-seniors
Real user testing: vague opener → bot retrieved + pitched "Care
Senior" (senior-only policy) without ANY fact-find. Intent classifier
routed to recommendation, bypassing fact-find.
Fix: orchestrator forces fact-find when profile is empty (no age,
dependents, or income captured yet), regardless of intent.
KI-014 (P1) — "family" auto-mapped to self+spouse+kids
Real user testing: user said "family" → bot assumed self+spouse+kids.
User actually meant parents + siblings (joint family).
Fix: VAGUE_TERMS list in keyword normalizer returns None for unclear
terms ("family", "everyone", "whole family", "joint family") forcing
LLM normalizer or re-ask clarification. Disambiguated phrases like
"family — me and my wife" still parse correctly.
KI-015 (P1) — Age in readback mismatched user's stated age
Real user testing: user said 31, readback said 30. Documented in
known-issues.md for follow-up. Fix plan involves adding an explicit
profile-confirmation step before transitioning to recommendations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/fact_find_normalizer.py +10 -0
- backend/orchestrator.py +16 -1
- docs/40-evaluation/known-issues.md +52 -0
|
@@ -144,6 +144,16 @@ def _keyword_normalize(question_id: str, raw_text: str) -> Any:
|
|
| 144 |
s = raw_text.lower()
|
| 145 |
|
| 146 |
if question_id == "dependents":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
if any(k in s for k in ["spouse", "wife", "husband"]) and "kid" in s and "parent" in s:
|
| 148 |
return "self+spouse+kids+parents"
|
| 149 |
if any(k in s for k in ["spouse", "wife", "husband"]) and "kid" in s:
|
|
|
|
| 144 |
s = raw_text.lower()
|
| 145 |
|
| 146 |
if question_id == "dependents":
|
| 147 |
+
# KI-014 — vague terms like "family" / "everyone" must NOT auto-map.
|
| 148 |
+
# User testing surfaced: user said "family" → bot assumed self+spouse+kids
|
| 149 |
+
# → wrong (user may have meant parents, siblings, joint-family etc.).
|
| 150 |
+
# Returning None here forces the LLM normalizer (or re-ask) to clarify.
|
| 151 |
+
VAGUE_TERMS = ["family", "everyone", "all of us", "everybody", "whole family", "joint family"]
|
| 152 |
+
if any(v in s for v in VAGUE_TERMS) and not any(
|
| 153 |
+
k in s for k in ["spouse", "wife", "husband", "kid", "child", "parent"]
|
| 154 |
+
):
|
| 155 |
+
return None # force clarification
|
| 156 |
+
|
| 157 |
if any(k in s for k in ["spouse", "wife", "husband"]) and "kid" in s and "parent" in s:
|
| 158 |
return "self+spouse+kids+parents"
|
| 159 |
if any(k in s for k in ["spouse", "wife", "husband"]) and "kid" in s:
|
|
@@ -165,7 +165,22 @@ async def handle_turn(
|
|
| 165 |
session = get_session(session_id or "anonymous")
|
| 166 |
|
| 167 |
in_fact_find_continuation = bool(session.awaiting_question_id) and not session.free_form_session
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
if treat_as_fact_find:
|
| 171 |
# If we were awaiting an answer, normalize + record it before picking next Q.
|
|
|
|
| 165 |
session = get_session(session_id or "anonymous")
|
| 166 |
|
| 167 |
in_fact_find_continuation = bool(session.awaiting_question_id) and not session.free_form_session
|
| 168 |
+
# KI-013 — if the user has NO profile fields yet, FORCE fact-find regardless
|
| 169 |
+
# of intent classifier. Real user testing surfaced: a vague opener
|
| 170 |
+
# ("I want health insurance") got classified as "recommendation" and the
|
| 171 |
+
# bot retrieved "Care Senior" (a senior-citizen-only policy) and pitched it.
|
| 172 |
+
# Bot should never recommend without knowing the user's age / dependents /
|
| 173 |
+
# conditions / budget. Force fact-find until ≥1 profile field is set.
|
| 174 |
+
profile_is_empty = (
|
| 175 |
+
session.profile.age is None
|
| 176 |
+
and session.profile.dependents is None
|
| 177 |
+
and session.profile.income_band is None
|
| 178 |
+
)
|
| 179 |
+
treat_as_fact_find = (
|
| 180 |
+
(intent == "fact_find" and not session.free_form_session)
|
| 181 |
+
or in_fact_find_continuation
|
| 182 |
+
or (profile_is_empty and not session.free_form_session)
|
| 183 |
+
)
|
| 184 |
|
| 185 |
if treat_as_fact_find:
|
| 186 |
# If we were awaiting an answer, normalize + record it before picking next Q.
|
|
@@ -254,3 +254,55 @@ hits this.
|
|
| 254 |
`session.free_form_session = True` and flush to disk. Subsequent turns
|
| 255 |
skip the fact-find branch entirely and go through retrieval + brain
|
| 256 |
as intended.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
`session.free_form_session = True` and flush to disk. Subsequent turns
|
| 255 |
skip the fact-find branch entirely and go through retrieval + brain
|
| 256 |
as intended.
|
| 257 |
+
|
| 258 |
+
### KI-013 — Bot recommends policies on vague openers without fact-find — **FIXED in next commit**
|
| 259 |
+
|
| 260 |
+
**Severity:** P0
|
| 261 |
+
**Source:** `backend/orchestrator.py` intent → fact_find gating
|
| 262 |
+
**Discovered:** Real user testing 2026-05-14
|
| 263 |
+
|
| 264 |
+
User gave a vague opening ("I want a health policy") and bot
|
| 265 |
+
immediately retrieved + pitched "Care Senior" — a senior-citizen-only
|
| 266 |
+
policy. User is not a senior. The intent classifier routed the message
|
| 267 |
+
to "recommendation" / "qa", not to fact_find. The orchestrator then
|
| 268 |
+
went straight to retrieval + brain → bot recommended whatever scored
|
| 269 |
+
highest, regardless of user demographics.
|
| 270 |
+
|
| 271 |
+
**Fix:** Force fact-find whenever profile is empty (no age, no
|
| 272 |
+
dependents, no income_band). Regardless of what the intent classifier
|
| 273 |
+
says. Bot will now always start with "First, your age?" before any
|
| 274 |
+
recommendation.
|
| 275 |
+
|
| 276 |
+
### KI-014 — Vague dependents term "family" auto-mapped to self+spouse+kids — **FIXED in next commit**
|
| 277 |
+
|
| 278 |
+
**Severity:** P1
|
| 279 |
+
**Source:** `backend/fact_find_normalizer.py` keyword fast-path
|
| 280 |
+
**Discovered:** Real user testing 2026-05-14
|
| 281 |
+
|
| 282 |
+
User said "family" as their dependents answer. Bot assumed
|
| 283 |
+
"self+spouse+kids". User had intended their joint family (parents +
|
| 284 |
+
siblings). All subsequent recommendations were wrong.
|
| 285 |
+
|
| 286 |
+
**Fix:** Add VAGUE_TERMS list (`family`, `everyone`, `joint family`,
|
| 287 |
+
etc.) that explicitly returns None from the keyword fast-path,
|
| 288 |
+
forcing either the LLM normalizer (which is more nuanced) or a re-ask
|
| 289 |
+
clarifier. Phrases like "family — me and my wife" still parse
|
| 290 |
+
correctly because the disambiguating words come through.
|
| 291 |
+
|
| 292 |
+
### KI-015 — Age in readback summary doesn't match user's stated age
|
| 293 |
+
|
| 294 |
+
**Severity:** P1
|
| 295 |
+
**Source:** Possibly `backend/needs_finder.py::record_answer` for age,
|
| 296 |
+
or LLM readback hallucination
|
| 297 |
+
**Discovered:** Real user testing 2026-05-14
|
| 298 |
+
|
| 299 |
+
User said "31" but bot's readback summary said "30". Possible causes:
|
| 300 |
+
(a) User's earlier answer contained "30" that the int parser caught
|
| 301 |
+
first; (b) The bot is using the LLM to generate the readback and the
|
| 302 |
+
LLM is hallucinating numeric values.
|
| 303 |
+
|
| 304 |
+
**Fix plan:** Add a CONFIRMATION step before the bot transitions to
|
| 305 |
+
free-form recommendations. After fact-find readback, the bot should
|
| 306 |
+
ask "Does this all look right? Reply 'yes' or correct anything that's
|
| 307 |
+
off." Then proceed only if user confirms. Also: log the raw fact-find
|
| 308 |
+
inputs vs the captured profile so we can debug mismatches.
|