rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
930e11e
·
1 Parent(s): 86fcc31

feat: KI-045 — natural-conversation classifier in fact-find

Browse files

Previously every user message during fact-find got parsed as an answer
to the awaiting slot. The bot couldn't gracefully handle off-topic
asides ("by the way, what's a co-payment?"), mid-flow questions, or
explicit intent change ("never mind, just suggest a policy").

New (heuristic, zero LLM call): before the existing normalizer flow
runs, classify the user's message:

• intent_change — matches one of: "never mind", "forget the profile",
"actually let me ask", "wait, can you", "skip this", "let's
switch", "stop asking", "i have a question", "just tell me".
• off_topic_question — ends with "?", ≥4 words, AND none of the
obvious slot-answer keywords (yes, no, self, spouse, kids, parents,
diabetes, bp, thyroid, lakh, lac).
• otherwise — direct_answer, existing flow.

On intent_change OR off_topic_question:
• Set `session.free_form_session = True` and flush.
• Set `treat_as_fact_find = False` so we fall through to the QA /
retrieval / brain path for the rest of handle_turn.
• Keep `awaiting_question_id` pinned so the slot is remembered if the
user wants to resume fact-find later (the should_route_to_fact_find
guard returns False once free_form_session=True, so they don't get
pulled back in automatically — they have to explicitly say "let's
keep going with the profile").

Also updates audit_results/ENTERPRISE_AUDIT.md with the post-fix 93-Q
eval headline (54.8% factual, up from 41.7% pre-fix). 3 questions
killed when the run hung on a NIM rate-limit edge case (rows 94-96
were all regulatory_oos refusals which are already at ~100% earlier).

KI-022 JSON-fallback rescued 7 questions in this run that would have
scored 0 on Groq judge JSON errors — without KI-022 the headline
would've been ~47% instead of ~55%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

audit_results/ENTERPRISE_AUDIT.md CHANGED
@@ -238,7 +238,12 @@ The full eval was sending **every** QA question to `needs_finder`. Sample bot an
238
  - `tests/test_routing_regression.py` — 15 unit tests, all passing. Pins KI-018 / KI-023 / KI-025 invariants.
239
  - 5-Q post-fix smoke (no judge): factual 0% → 60%, nim-chain serving 100% of QA.
240
  - Live HF Space smoke (`https://rohitsar567-insurancebot.hf.space`): PED waiting-period question now answers via `nim-chain::nemotron-3-nano-30b-a3b::v4-flash::qa` with a grounded reply, not the old "Happy to help. First, your age?" misroute.
241
- - Final parallel 96-Q gold eval landing imminently; clean 100-persona audit will run against the final stable image and the numbers will replace the pre-fix baselines above.
 
 
 
 
 
242
 
243
  ## Pending follow-ups (P1)
244
 
 
238
  - `tests/test_routing_regression.py` — 15 unit tests, all passing. Pins KI-018 / KI-023 / KI-025 invariants.
239
  - 5-Q post-fix smoke (no judge): factual 0% → 60%, nim-chain serving 100% of QA.
240
  - Live HF Space smoke (`https://rohitsar567-insurancebot.hf.space`): PED waiting-period question now answers via `nim-chain::nemotron-3-nano-30b-a3b::v4-flash::qa` with a grounded reply, not the old "Happy to help. First, your age?" misroute.
241
+ - **Post-fix parallel 96-Q gold eval (93 of 96 completed; 3 trailing questions killed when the run hung on a NIM rate-limit edge case):**
242
+ - **Factual accuracy: 54.8% (51 / 93)** — up from the pre-fix 41.7% baseline.
243
+ - **KI-022 JSON-fallback** rescued 7 questions that would have scored 0 on Groq judge JSON errors. Without KI-022 the headline would have been ~47%.
244
+ - PED waiting-period type — previously 0% pre-fix; samples now: "Bot correctly states the 24‑month waiting period" / "matched_nums=['24']" via regex fallback / "36‑month period and includes source cit…".
245
+ - Stuck questions: rows 94-96 (all `regulatory_oos` refusals — those routes are already at ~100% earlier in the run; the rate-limit hang affected the brain call, not the refusal logic).
246
+ - Clean 100-persona audit pending — to run once Batch B (bucket reorg) ships and the HF Space is stable.
247
 
248
  ## Pending follow-ups (P1)
249
 
backend/orchestrator.py CHANGED
@@ -300,6 +300,66 @@ async def handle_turn(
300
  free_form_session=session.free_form_session,
301
  )
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  if treat_as_fact_find:
304
  # If we were awaiting an answer, normalize + record it before picking next Q.
305
  # Uses backend/fact_find_normalizer.py to map free-text → schema enums.
 
300
  free_form_session=session.free_form_session,
301
  )
302
 
303
+ if treat_as_fact_find:
304
+ # KI-045 (2026-05-14) — natural-conversation classifier. Previously
305
+ # EVERY user message during fact-find got parsed as an answer to the
306
+ # awaiting slot. The bot couldn't gracefully handle off-topic asides,
307
+ # mid-flow questions, or volunteered profile information. Now we
308
+ # classify the user's message first; only "direct_answer" continues
309
+ # into the existing normalizer flow.
310
+ #
311
+ # Heuristic-only (no LLM call — keeps latency negligible):
312
+ #
313
+ # • question → message ends with "?" AND has 4+ words AND no
314
+ # obvious slot-answer pattern. User is asking
315
+ # something off-topic; we exit fact-find and let
316
+ # the QA brain answer.
317
+ # • intent_change → explicit phrases like "never mind", "actually
318
+ # let me ask", "wait, can you...", "forget the
319
+ # profile, just tell me...". User is steering
320
+ # away; we exit fact-find.
321
+ # • otherwise → direct_answer (existing normalizer flow).
322
+ #
323
+ # In all non-answer cases we keep the awaiting slot pinned so when
324
+ # the user returns to fact-find next turn, we pick up where we
325
+ # left off — and we flip `session.free_form_session = True` so the
326
+ # rest of handle_turn routes to retrieval + brain, not back here.
327
+ if session.awaiting_question_id and session.awaiting_question_id != "name":
328
+ _t = user_text.lower().strip()
329
+ _intent_change_phrases = (
330
+ "never mind", "forget the profile", "forget profile",
331
+ "actually let me ask", "wait, can you", "wait can you",
332
+ "wait, tell me", "skip this", "skip that question",
333
+ "let's switch", "lets switch", "stop asking",
334
+ "i have a question", "just tell me",
335
+ )
336
+ _looks_intent_change = any(p in _t for p in _intent_change_phrases)
337
+ _ends_q = _t.endswith("?") or _t.rstrip(".!").endswith("?")
338
+ _wordy_q = _ends_q and len(_t.split()) >= 4
339
+ # Don't misclassify enum-answer questions like "spouse?" /
340
+ # "diabetes?" (short, often a clarification request)
341
+ _looks_off_topic_q = _wordy_q and not any(
342
+ kw in _t for kw in (
343
+ "yes", "no", "self", "spouse", "kids", "parents",
344
+ "diabetes", "bp", "thyroid", "lakh", "lac",
345
+ )
346
+ )
347
+ if _looks_intent_change or _looks_off_topic_q:
348
+ # Exit fact-find: let the rest of handle_turn route to the
349
+ # QA / brain path. Keep awaiting_question_id pinned so we
350
+ # remember where we were for next turn. Flip free-form so
351
+ # the should_route_to_fact_find guard returns False on the
352
+ # follow-up turn.
353
+ session.free_form_session = True
354
+ session._flush()
355
+ # Fall out of the fact-find branch entirely — the user's
356
+ # message is treated as a regular QA turn from here.
357
+ treat_as_fact_find = False # noqa: F841 — re-checked below
358
+
359
+ # If we just exited fact-find via the classifier, skip the answer
360
+ # handling block + the question-emission tail; let execution fall
361
+ # through to the QA / retrieval path below.
362
+
363
  if treat_as_fact_find:
364
  # If we were awaiting an answer, normalize + record it before picking next Q.
365
  # Uses backend/fact_find_normalizer.py to map free-text → schema enums.