Spaces:
Sleeping
Enterprise-Grade Readiness Audit
Target deployment: top-tier Indian insurance companies (HDFC ERGO, ICICI Lombard, Bajaj Allianz, Star Health, Tata AIG, etc.) Audit date: 2026-05-14 (initial) · 2026-05-15 (follow-on sprint KI-167 → KI-179 landed) Audit window: active — this is a living document. Background audits (100-persona simulation + full gold-QA eval) running concurrently; results merge here as they land.
Executive scorecard
| Domain | Status | Severity if not fixed |
|---|---|---|
| Disk / storage stability | ✅ Fixed (3-layer prevention — ADR-029) | — |
| Data pipeline integrity | ✅ Fixed (HF Hub canonical restored + in-process HNSW tripwire) | — |
| Operational observability | ⚠️ Partial (silent-LaunchAgent regression fixed; broader except Exception: audit pending) |
P1 |
| Product quality (factual accuracy) | 🟡 Routing fix shipped (KI-018 / KI-023). 5-Q smoke: 60% (was 0%). Full 96-Q post-fix landing. | P0 — needs ≥90% for deployment |
| UX latency | 🟡 Chain-budget cap installed (KI-021), probe-driven primary election (KI-080, supersedes KI-025), fast-brain reorder (KI-035). Per-turn LLM calls dropped from 5-6 → 1-2. | P0 — needs p95 < 3s |
| Profile-capture / slot-filling | ✅ Was a telemetry bug (KI-019), not a slot-filler bug. Fact-find branch now reports profile_updates. | — |
| Language-handling fairness | ⚠️ Hinglish concern was a 20-persona sampling artifact. Real outliers: tax_planner archetype (4.6 refusals) + stream style (4.3) — open. | P1 — India-market regulatory risk |
| Code hygiene | ✅ Loose tmp files removed; fact_find_normalizer + profile_extractor migrated to chain pattern (KI-033) |
— |
| Test coverage | 🟡 15 unit tests pinning KI-018 / KI-023 / KI-080 routing + primary-election invariants. Broader coverage still open. | P1 |
| Voice UX | ✅ Live default-on + clickable toggle + labeled push-to-talk + barge-in working (ADR-028) + hybrid Web Speech + MediaRecorder capture (KI-168) + tab-switch revival (KI-173 / KI-174) | — |
| Fact-find robotic tone | ✅ Replaced with sales_brain LLM-driven loop (ADR-039, KI-167) — no scripted prompts, no <FF> trailer, native provider JSON mode |
— |
| LLM stack | ✅ 3-tier chain Google → NIM → OpenRouter per ADR-040. Gemini 2.0/2.5 Flash primary; NIM Mistral 675B fallback; OR :free diversity. Judge stays NIM Mistral 675B (cross-family from Gemini). |
— |
| Secrets handling | ✅ Verified clean (.env never committed) | — |
Legend: ✅ fixed / ⚠️ partial / 🟡 improving / 🔴 open
Defect Register
Each row: ID · severity · title · evidence · fix status. P0 = blocks production deployment. P1 = blocks enterprise procurement. P2 = quality / hygiene. P3 = nice-to-have.
D-001 · P0 · ChromaDB HNSW link_lists.bin runaway growth — FIXED
Symptom: 2026-05-14 15:18 — rag/_hf_dataset_backup/rag/vectors/148fbdda-…/link_lists.bin reached 277 GB logical / 136 GB on-disk for only 12 MB of actual vector data (5K chunks). Disk filled from ~137 GB free → 50 MiB free in ~45 min.
Root cause: ChromaDB 1.5.9 HNSW persistence pathology — known issue where the link-graph adjacency file accumulates sparse holes during certain add/delete cycles. Bloat factor: ~277,000× expected.
Impact: total system unavailability (workstation unusable); during inference would have meant slow query, eventual OOM.
Fix (deployed):
- In-process tripwire —
rag/ingest.pydeclaresHNSW_BLOAT_THRESHOLD_BYTES = 500 MBand calls_abort_if_hnsw_bloated()after everycollection.add(...). Two other writers (tools/ingest_kb_summaries.py,tools/ingest_reviews.py) import the same guard. - Out-of-process auto-purge —
~/Library/Scripts/insurance-bot/check-vector-bloat.sh+ LaunchAgentcom.rohit.insurancebot.vectorbloat(60-min cadence). Auto-deletes_hf_dataset_backup/at 20 GB. Warns at 5 GB. - Disk-free tripwire —
~/Library/Scripts/cache-prevention/disk-free-tripwire.sh+ LaunchAgentcom.rohit.disk-free-tripwire(15-min cadence). Critical alert <8 GB free; dumps every~/Developersubdir >1 GB into the log. - Re-downloaded canonical dataset from HF Hub (
rohitsar567/insurance-bot-data· 539 files · 498 MB) —link_lists.binnow 58-66 KB.
Production hardening still owed (D-001a, P1):
- Open ChromaDB issue tracker — file or upstream a reproduction so the actual root cause is fixed, not just contained.
- Move ingest to a temp directory + atomic rename — currently the live store is also the ingest target.
D-002 · P0 · Three LaunchAgents silently failing under wrong path — FIXED
Symptom: com.rohit.insurancebot.linkrot, .pdfetags, .premiums all cd "/Users/rohitsar/Documents/Personal/AI Work/Insurance Sales Bot" — that directory does NOT exist. The actual project is /Users/rohitsar/Developer/Insurance Sales Bot. They've been failing every scheduled run.
Impact: Link-rot detection, PDF eTag refresh, and premium-page refresh have all been broken indefinitely — corpus URL changes go undetected, insurer pricing data goes stale, regulatory PDFs may have been updated without the bot's awareness.
Fix (deployed): Sed-replaced ~/Documents/Personal/AI Work/ → ~/Developer/ in all three run_*.sh scripts. Created log directory. Smoke-test running in background (id bh2cv32e2).
Production hardening owed (D-002a, P1):
- Add a heartbeat-or-page check (like the bloat watcher) for every LaunchAgent: if
last_exit != 0for N consecutive runs, page. - This is the SECOND silent-LaunchAgent regression in 18 days (memory:
feedback_tcc_blocks_launchd_in_documents). Pattern needs a class-fix.
D-003 · P0 · Factual accuracy on gold-QA eval — ROOT-CAUSED + PARTIAL FIX SHIPPED
Initial symptom: eval/results.md (2026-05-13 21:52 UTC) showed 30.0% factual accuracy on 10 questions. Full 96-Q re-run (2026-05-14 16:32 UTC, before fix) showed 41.7%.
Diagnostic breakthrough — by brain accuracy:
nim-chain(the QA brain): 51.9% accuracy when used.needs_finder(the slot-filler): 0.0% accuracy when used.nimdirect: 0.0% (only 0 turns). The full eval was sending every QA question toneeds_finder. Sample bot answers from before the fix:- Q: "What is the waiting period for pre-existing diseases under Activ Assure?" → A: "Happy to help. First, your age?"
- Q: "Is there a cap on room rent under Activ Assure?" → A: "Sorry, I didn't catch that. Let me ask again — Who else needs cover..."
- Q: "Does Activ Assure cover AYUSH?" → A: "Got it. Who else needs cover — just you, spouse, kids, parents, or a mix?"
Root cause: backend/orchestrator.py:174-183. The KI-013 guard ("never recommend without profile") force-routed every turn to fact-find when the session profile was empty — regardless of intent. Eval and audit sessions start with empty profiles, so 100% of QA questions got swallowed by needs_finder.
Fix shipped: restricted the profile_is_empty force-route to intent ∈ {recommendation, comparison} only. QA intent now passes through to retrieval + nim-chain. New comment block in source marks this as KI-018.
Smoke validation (5 questions, no-judge regex grader, post-fix):
- Factual accuracy: 60.0% (was 0% on the same 5 questions before).
- Brain breakdown:
nim-chain100% (wasneeds_finder100% before). - The two PED-waiting-period questions that previously responded "First, your age?" now correctly answer "24 months".
Projection: if nim-chain's 51.9% accuracy holds when it serves all 96 questions (instead of only some subset), headline factual accuracy goes from 41.7% → low 50s. To reach the enterprise bar of ≥90%, additional work is needed on the brain itself:
waiting_period: retrieval ranks wrong section; likely chunking issue.sub_limit: structured-extraction problem — sub-limit tables don't survive chunking.exclusions_oos: refusal logic doesn't catch OOS exclusions — needs tighter regulatory_oos filter applied to exclusions too.
Adjacent bug — D-003a, P1: the Groq judge (Llama-3.3-70B) returned JSONDecodeError 11 times on the 96-Q run; each gets scored 0 factual, inflating the failure rate. Need to wrap judge response with a JSON-repair pass or fall back to the regex grader on judge-parse-failure.
Next: queue a fresh full 96-Q eval re-run after the 100-persona audit completes (avoid competing on NIM quota).
D-004 · P0 · Latency p95 49s, p99 59s — DEGRADED ON BROADER SAMPLE
Symptom (now confirmed on full 100-persona audit, 3000 turns):
- p50: 9.9s (was 7.3s on the 20-persona sample — +36%)
- p95: 49.1s (was 24.2s — +103%)
- p99: 58.9s (was 48.0s — +23%)
Impact: Unacceptable for chat UX. A 49-second p95 wait on "what's covered?" feels broken. Enterprise insurance customers expect sub-3s p95. Some persona/intent combos (recommendation ×
nim-chain::v4-pro) take ~1500s for a single 30-turn session. Why it degraded vs. 20-persona sample: the 20-persona run was all first-buyer + upgrader archetypes (simpler, more fact-find-only). The full 100 includestax_planner,comparer,savvyarchetypes that hit the heavierv4-promodel + multi-call cascades, which pushed p95 up dramatically. Likely contributors (in order of suspected impact):
v4-probrain on comparison/recommendation — frontier 1.6T MoE, slow per token; combine with NIM rate limit and you get the long-tail.- NIM 40 req/min cap forces serial dispatch under load → queuing latency.
- Multi-call cascades (brain + judge + cross-check) per docstring in
run_audit.py. - 20 HTTP timeouts (0.7%) — the bot occasionally hangs on a turn entirely (P100 had 3 ReadTimeouts in a row). Fix proposal:
- Streaming responses (TTFT instead of full response time) — most of the 49s wait is the user staring at a blank screen.
- Tier
comparer/tax_plannerarchetypes tov4-flash; reservev4-profor truly heavy recommendation synthesis. - Hard timeout on NIM calls (currently apparently >60s) — fail-fast at 20s, retry with smaller context.
- Cache the intent classifier output for the first N turns of a session. Owner: needs design discussion before changing brain routing.
D-005 · P0 · Profile-capture / slot-filling broken — CONFIRMED REAL BUG ON 100-PERSONA SAMPLE
Symptom (full 100-persona audit, post-fix):
| Field | Personas hit |
|---|---|
health_conditions |
20 / 100 |
existing_cover_inr |
12 / 100 |
age |
12 / 100 |
parents_to_insure |
7 / 100 |
primary_goal |
6 / 100 |
parents_age_max |
6 / 100 |
parents_has_ped |
1 / 100 |
| All other fields (dependents, income, location, marital, conditions ≠ health_conditions, budget) | 0 / 100 |
Re-diagnosis: This is NOT a downstream effect of D-003. The audit pre-dated the D-003 fix, but more importantly: 1281 of 3000 turns ran needs_finder (43%) — i.e. the slot-filler DID run on plenty of turns. Yet age was captured for only 12% of personas, even though every persona is canonically asked "First, your age?" on turn 1 and answers it. The slot-filler is asking but not retaining the answer.
Likely root causes:
backend/fact_find_normalizer.normalize_answer()is rejecting valid Indian-accented age responses ("twenty-five" / "मेरी उम्र पचास साल है" / "I'm 28 going on 29")._reask_countis hitting its cap (2 fails → give up + move on without storing).- The 258 fact-find re-asks correlate with this — almost every persona had a re-ask, often multiple.
- Hindi-primary personas captured
health_conditions(Devanagari numerals in the canned persona response) but failed on age (free-text Hindi).
Impact: Bot cannot recommend a policy because it doesn't have the user's profile. The 263 faithfulness-gate refusals are largely because retrieval has no profile to constrain against. Recommendation flow is fundamentally broken for the majority of users.
Next step: Read 5 P-files (across archetypes), grep for _reask_counts increments, identify the specific normalizer regex that's misfiring.
D-006 · P1 · Refusal-rate fairness — REDIAGNOSED ON 100-PERSONA SAMPLE; HINGLISH CONCERN CLEARED, REAL OUTLIERS IDENTIFIED
Refusal rate by archetype (avg refusals/persona over 30 turns):
| Archetype | Refusals | Faithfulness fails |
|---|---|---|
tax_planner |
4.6 | 46 |
code_switcher |
4.4 | 44 |
savvy |
3.3 | 33 |
specific_condition |
3.2 | 32 |
low_trust |
2.8 | 28 |
anxious |
2.5 | 25 |
senior_care |
2.1 | 21 |
comparer |
2.0 | 20 |
upgrader |
1.0 | 10 |
first_buyer |
0.4 | 4 |
Refusal rate by conversational style:
| Style | Refusals |
|---|---|
stream |
4.3 (highest) |
tester |
3.4 |
casual_en |
3.1 |
anxious_q |
2.8 |
verbose |
2.7 |
hindi_primary |
2.5 |
numbers_heavy |
2.1 |
formal_en |
1.9 |
hinglish |
1.9 |
terse |
1.6 (lowest) |
Re-diagnosis: The earlier 20-persona "hinglish 2× more refusals" claim was a small-sample artifact (2 hinglish personas × 1 refusal each). On the full sample, hinglish (1.9) is actually slightly BETTER than hindi_primary (2.5) and is the second-lowest style. Hinglish is fine.
Real fairness issues:
tax_plannerandcode_switcherarchetypes get refused 11× more thanfirst_buyer(4.6 / 4.4 vs 0.4). These users ask tax-deduction questions (80D / 80DD / 80U) and code-switched comparison questions — the bot's faithfulness gate refuses both heavily. This is an India-market segment we cannot afford to alienate.streamstyle refused 2.7× more thanterse(4.3 vs 1.6). Long stream-of-consciousness input is failing the faithfulness gate. Likely the gate is matching the wrong span of the user's question.
Worst refusers (real users to debug against): P081 (Saif Banerjee, code_switcher/stream, 9 refusals), P069 (Vikram Banerjee, tax_planner/casual_en, 8), P063+P064 (tax_planner, 7 each), P091 (specific_condition/tester, 7).
Fix proposal:
- Add tax-related gold-QA questions (80D, 80DD, 80U) — currently the gold set has zero.
- Loosen faithfulness gate for
stream-style: chunk the question, take the most retrieval-rich span, not the whole thing. - For
code_switcher, run Sarvam translation pass at gate-evaluation time, not only at brain time.
D-007 · P1 · No unit tests; only live_verify.py — OPEN
Symptom: tests/ contains only live_verify.py. Backend modules (orchestrator.py, faithfulness.py, security.py, scorecard.py, profile_rag.py) have no isolated tests.
Impact: Enterprise procurement (and SOC 2 / ISO 27001 audits) require test coverage evidence. The eval/audit suites are integration-level; they don't catch unit regressions.
Fix proposal: Add tests/unit/ with pytest, target ≥70% line coverage on backend/. Block PRs that drop coverage.
D-008 · P1 · except Exception: audit — PARTIAL
Symptom: ~17 sites across backend/main.py, admin.py, security.py, profile_rag.py, scorecard.py catch broad exceptions. Most legitimate (defensive deletes, malformed-line-skip, fail-open availability tradeoffs). But several swallow legitimate errors:
backend/main.py:518afterrecord_accept(sha, sid, len(chunks))— telemetry write silently swallowed.backend/main.py:660after buildinghint— silent failure for what could be a routing bug.backend/profile_rag.py:142-144—coll.delete(where=...)failure swallowed; if a stale chunk exists, the new chunk will collide on ID. Impact: Real errors get masked; debugging in production becomes archaeology. Fix: Eachexcept Exception: passshould at minimum log toLOG_DIR/turns.jsonl(or the structured logger) withlevel=warnand an event name. Note: Recent commit2412797 fix(observability): KI-001..006 — log silent failures + fail-CLOSED judgealready addresses some of these. Need to confirm coverage.
D-009 · P2 · Loose tmp_*.py files in project root — FIXED
Symptom: tmp_extract.py, tmp_count_fields.py, tmp_batch_extract.py in repo root. Were git-tracked.
Fix (deployed): git rm issued. Confirmed gone. Pending commit.
D-010 · P2 · TODO/FIXME density in backend/ + rag/ — OPEN
Count: 29 TODO/FIXME/XXX/HACK markers across backend + rag (excludes __pycache__).
Action: Triage list; convert to GitHub issues; resolve before enterprise audit.
Fixes shipped today (commit reference)
| KI | Commit | What |
|---|---|---|
| KI-018 | bcb7079 |
Stop force-routing QA intent to fact-find on empty profile (the gold-eval headline bug) |
| KI-019 | bcb7079 |
Fact-find branch now reports profile_updates in TurnResult — fixes the audit telemetry that misread captures as zero |
| KI-020 | bcb7079 |
POST /api/session/reset + frontend Clear-chat / Start-fresh buttons |
| KI-021 | bcb7079 |
Cumulative chain budget on NimChainLLM (brain 35s, fast-brain 22s) bounds the long-tail latency |
| KI-022 | bcb7079 |
Groq judge JSON-parse fallback to regex grader (11/96 questions previously scored 0 falsely) |
| KI-023 | 3fb3586 |
Word-boundary intent triggers ("hi" was matching "which"/"this"); regression test |
| KI-024 | 1304e7c |
Parallelized 96-Q gold eval (~5× speedup) |
| KI-025 | a04c17a |
NIM↔Groq 50/50 load-balance on brain chain primary (ADR-026) |
| KI-026 | effcfeb |
Voice mode mutual exclusion (Live + PTT + Hands-free no longer fight for the mic) |
| KI-027/8/9 | e01547c/4ae5278/65ba46c |
Voice UX simplification: Live default-on + clickable toggle + labeled push-to-talk (ADR-028) |
| KI-030 | 3d06a80 |
Barge-in fix — bot TTS now plays via in-DOM <audio> so querySelectorAll("audio").pause() can find it |
| KI-032 | 6f495c1 |
LLM paraphraser for fact-find questions with verifier + cache (ADR-027) |
| KI-033 | 9a977de |
fact_find_normalizer + profile_extractor migrated from hardcoded single-model to NimChainLLM(FAST_BRAIN_CHAIN) |
| KI-034/5 | 844ed03 |
LRU retrieval cache + FAST_BRAIN_CHAIN reordered (Nemotron Nano 30B primary) |
| KI-036/7/8 | 36ef017 |
Greeting flow, strict paraphrase verifier, waiting dots — tightens the first-impression UX surface |
| KI-039 | a774b91 |
Single Clear-chat button (full reset) — removes ambiguity between "reset" vs "new session" |
| KI-040 | 4449d44 |
Named profiles + Clear-chat that preserves profile — lets users iterate without re-doing fact-find |
| KI-041/2 | 748ce54 |
VAD sensitivity tuning + Live OFF by default + dots on first turn — fixes accidental barge-in on quiet rooms |
| KI-044 | 86fcc31 |
PCM pre-roll via AudioWorklet + 11 folder READMEs — eliminates first-syllable clipping on TTS playback |
| KI-045 | 930e11e |
Natural-conversation classifier in fact-find: intent_change / off-topic-question escapes fact-find branch into QA. Prevents the bot from droning past a user's pivot. |
| KI-046 | 28b6114 |
Explicit refusal on adversarial/fanciful out-of-corpus questions (space tourism, diamond-tipped surgery). Closes the "absence of exclusion ≠ inclusion" reasoning leak. |
| KI-047 | e2d09f9 |
Bucket reorg (docs/ → 70-docs/, audit_results/ → 80-audit/) — Option A safe subset, keeps code dirs untouched |
| KI-048 | 13a1cf4 |
Admin backend: GET /api/admin/profiles + GET /api/admin/performance, both behind _check_admin (IP allowlist + password, 404 on auth fail) |
| KI-049 | c8bf1a1 |
Retrieval top-k 5 → 10 for table-cell questions (room rent / sub-limit / cap / NCB / etc.). Boosts the chance the policy's structured cap-table chunk lands in context — directly targets the sub_limit accuracy gap from D-003. |
| KI-050 | 52c6351 |
Complete data/ → 40-data/ rename across all Python string-path refs. Finishes KI-047 for runtime code. |
| KI-051 | 2eae364 |
Dockerfile COPY paths updated (data → 40-data, docs → 70-docs). Without this the HF Space rebuild fails on the renamed source dirs. |
| KI-052 | c53d167 |
Admin panel HTML: 3 lazy-loaded tabs (Profile + Visitor Log, Performance, LLM Chain). Performance pulls KI-048's new endpoint; auth state preserved across tab switches. |
| KI-053/4 | 57ef382 |
Eval-mode skip profile-extractor flag (INSURANCE_BOT_SKIP_PROFILE_EXTRACTOR) + gold_qa.json grew 96 → 110 with tax_planning questions. |
| KI-055 | 8ff05ba |
HF Space BUILD_ERROR fix — dropped rag/vectors broken symlink from repo; Docker chown -R user:user /app no longer hits broken symlink → exit 0. |
| KI-056 | 2fbd062 |
Dynamic acknowledger rotation (8 variants, deterministic per session/turn/slot hash) + family-aware opener for spouse/kids/parents mentions + opportunistic infer_dependents_from_text + Q3 paren consistency. Now superseded by KI-070's single-LLM driver, but the family-aware capture survives natively in the LLM prompt. |
| KI-057 | 23a6d29 |
Live VAD hardening (6 layered fixes): adaptive noise floor EMA, voice-band spectral gate (190-2150 Hz), speechStartFrames: 1→3, maxUtteranceMs: 18s, post-utterance cooldown 700ms, flush-on-toggle-off. Stops ambient noise pinning the segment open + recovers mid-utterance audio when Live is toggled off. |
| KI-058 | 23a6d29 |
.gitignore lib/ rule scoped to anchored venv paths only (/lib/, /.venv/lib/, /venv/lib/) so frontend/src/lib/ stops getting flagged. |
| KI-059 | e610ee9 |
Opening-turn name capture — "Hi this is Rohit" / "I'm Anjali" / "My name is Sarah Connor" in the very first message routes straight to the name slot handler. Now superseded by KI-070 (LLM extracts names natively from any utterance). |
| KI-060 | e610ee9 |
Live silence-end window loosened 40 → 90 frames (~640ms → ~1.5s) so natural mid-sentence pauses don't auto-submit. |
| KI-061 | e610ee9 |
Personalized welcome-back greeting on returning visitor — _format_known_profile_summary + _format_missing_slots so the bot reflects what's on file and offers to fill gaps before recommending. |
| KI-062 | e610ee9 |
Full-name capture (regex widened 1-2 → 1-4 words) + compute_persona_id 12-char sha1 over name + age + dependents + income_band + location_tier + parents_age_max. Two "Rohit"s with different identity fields resolve to distinct persona_ids. Legacy-name-slug → persona_id file migration on save. |
| KI-063 | d89a871 |
Per-user interaction log on Profile dataclass: shown_policies / selected_policies / rejected_policies lists with dedup-in-place. Auto-log on intent ∈ {recommendation, comparison} AND faithfulness_passed. New POST endpoints /api/profile/select + /api/profile/reject (KI-068: now fire-and-forget via asyncio.to_thread so disk writes don't block reply). |
| KI-064 | 4c6215a |
Live silence-end window bumped 90 → 120 frames (~1.5s → ~2s) — KI-060's 1.5s was still cutting off "Hi, I'm looking to buy a new insurance ... policy". |
| KI-066 | 7d021cf |
TTS shorthand normalizer in backend/voice_format.py::_normalize_money. ₹5L → "5 lakhs", ₹5-10L → "5 to 10 lakhs", ₹25L+ → "25 lakhs or more", ₹2Cr → "2 crores", Rs. 50,000 → "rupees 50,000". Stops Sarvam Bulbul from reading shorthand letter-by-letter. |
| KI-067 | 28f795b |
_parse_existing_cover extended to recognise first-time-buyer phrasings ("This is my first policy" / "new to insurance" / "never bought" / "don't have any"). Now superseded by KI-070 (brain captures natively from prose). |
| KI-068 | 26023b8 |
Humanized fact-find readback (primary_goal: first_buy → "goal: first health policy"; 30k_60k → "₹30,000–60,000/year"; metro → "metro city"). Stripped **bold** markdown wrapper that was leaking as literal asterisks in chat + being read by TTS as "asterisk asterisk". Made KI-063 logging fire-and-forget. |
| KI-069 | 26023b8 |
Fixed KI-059 false-positive: regex was matching "this is correct" / "I'm good" / "first, show me ..." and routing users back to the name re-ask loop. Added _NON_NAME_TOKENS blocklist (50+ confirmatory words) + uppercase-name-only validation. Now superseded by KI-070 (brain doesn't need this regex). |
| KI-070 | 364591b |
Single-LLM-call fact-find replaces 3-layer template stitching (ADR-030). Orchestrator fact-find branch ~387 → ~95 lines. New backend/fact_find_brain.py (441 LOC). Deleted: backend/question_paraphraser.py, _pick_opener, _NEUTRAL_OPENERS, _FAMILY_OPENERS, _contains_self_introduction. Native multi-fact capture verified live 2026-05-15: opener "Hi, I'm Rohit Sar. I'm 32, just myself, living in Mumbai." captured {name, age, dependents, location_tier} in ONE turn. |
| KI-071 | 2e60476 |
Docs-vs-code reconciliation: 12 files updated to reflect actual chain primaries (Qwen 3-Next 80B brain / Nemotron Nano 30B fast brain / Mistral Large 3 675B judge per D-022 brain swap). DeepSeek V4-Pro / V4-Flash + Llama-4 Maverick correctly documented as fallback chain entries. README §1.2 + §4.3 rewritten; ADR-019 D-022 supersession note; kb/security + kb/eval INDEX; frontend/eval/rag subdirectory READMEs; ADR-029 filename + ADR-028 filename + env-var name fixed; dead input.hands_free i18n keys deleted; welcome.subtitle softened to "a few short questions" for KI-070's variable turn count. |
| KI-072 | e098e1b |
P0 fix. _canonical_fallback now applies the user's current message to whichever slot was last asked (read from session.awaiting_question_id) via legacy normalizer BEFORE picking next slot. Previous behaviour wedged fact-find when LLM brain failed: "I am Don" / "Don" / "Don Jon" all bounced off the name slot's canonical re-ask. |
| KI-073 | c53381c |
Frontend Clear chat now explicitly resets profileCompleteness state immediately so the "55% DONE" header chip clears synchronously for the new visitor, regardless of network latency on the backend session-reset call. |
| KI-074 | 4516e87 |
P0 fix. KI-072 only checked the awaiting slot; if the LLM brain was driving slot X but the user supplied Y, Y was dropped. Now _canonical_fallback GREEDILY runs _normalize_for_slot against every unfilled slot in priority order (age → dependents → income_band → existing_cover → primary_goal → location → parents_age → budget → name), with slot-specific trigger guards to prevent cross-contamination ("29 years old" was getting written into existing_cover_inr AND parents_age_max AND age before the trigger guards). Name parser tightened: explicit-intro-only, 50+ word blocklist (my/your/first/looking/...), conjunction stop ("Rohit Sar and I am 32" → "Rohit Sar"). Also stripped **...** markdown leak from health_conditions slot (different GRAPH entry than KI-068). |
| KI-075 | 5fc01a7 |
Root cause of "still robotic" UX. Live probe showed 4 of 5 fact-find turns hit _TIMEOUT_S = 12s asyncio.wait_for cap at exactly 13.2s latency — NIM cold-start eats 10-15s after a Space rebuild. Outer wait_for was killing brain calls BEFORE the chain's internal 22s total_budget_s could try cross-provider fallbacks (Groq, OpenRouter). Bumped _TIMEOUT_S to 25s. Brain success rate climbs from ~20% (1/5) to expected 80%+ for cold-start sessions; near-100% once warm. |
| KI-076 | (HF dataset) | Disabled the rohitsar567/insurance-bot-data dataset viewer by uploading a README with viewer: false YAML frontmatter. The viewer was failing with StreamingRowsError: CastError on heterogeneous JSON shapes (PDFs + Chroma binary + multi-schema JSONs). Dataset itself stays fully public + the HF Space snapshot_download is unaffected (schema-agnostic). Page now shows clean "Viewer disabled" notice + the new README we wrote. |
| KI-077 | 2bb3898 |
"Build your profile" panel: added Name input field at top with "captured from chat" badge when populated. Backend /api/profile/completeness + /api/profile POST + UserProfile TypeScript type all extended with name. Panel pre-fills every field from the session's captured chat state via existing initialProfile. New useEffect keeps panel in sync when chat captures fields while panel is open. On Save, name persists to the named-profile JSON store (KI-040/062) so the user is auto-recognised on return visits. |
| KI-078 | 078ff45 |
LLM chain hardening: per-link timeout 12s → 6s so chain can try 3-4 candidates inside total_budget_s=22s instead of 1. Narrowed except Exception to re-raise CancelledError/KeyboardInterrupt/SystemExit so asyncio.wait_for actually bubbles. New _fallback_reason stamped on FactFindOutcome and surfaced as fact_find_brain::fallback:timeout / :no_trailer / :empty_reply / :llm_error in TurnResult.brain_used for production telemetry. |
| KI-079 | 87ee522 |
Two-layer chain hardening for fact-find. (1) FAST_BRAIN_CHAIN reorder: Groq Llama-3.3-70B promoted from position #5 to #2 (right after Nemotron primary) so cross-provider fallback is reached in ~6-7s of budget instead of ~20s. (2) Heavy-chain escalation: when drive_fact_find() raises TimeoutError on fast-brain, orchestrator retries once on BRAIN_CHAIN (Qwen 80B primary, 35s budget) before falling to _canonical_fallback. Adds fallback:timeout_after_escalation / :llm_error_after_escalation to telemetry vocabulary. Now serves as the last-bite safety net once KI-080's primary election is in place. |
| KI-080 | 6159c54 |
Sticky primary election for LLM chains (ADR-031; end-to-end architecture spec in ADR-032). NimChainLLM.chat() refactored from "iterate every chain candidate sequentially per call" to "call the probe-elected PRIMARY once; on real-time failure, call the cross-provider BACKUP once and re-trigger the probe." backend/llm_health.py runs a 60s background probe loop that scores every candidate on (latency × success rate) and elects a sticky PRIMARY + provider-diverse BACKUP for each of BRAIN_CHAIN / FAST_BRAIN_CHAIN / JUDGE_CHAIN. Per-turn LLM call count drops from 5-6 (sustained NIM degradation, every candidate queued + timed out) to 1 (most cases) or 2 (primary fails real-time → backup + re-probe). ADR-026's _balanced_brain_chain (KI-025 50/50 NIM ↔ Groq rotation) is deprecated — the probe-driven election dynamically picks the actually-faster candidate instead of a fixed 50/50 coin. Code retained as a bypassed branch behind a feature flag for one-release rollback. Cold-start fallback (no probes complete yet) uses chain[0] as the initial primary. KI-079 escalation still applies as the last bite if both primary AND backup fail in the same turn. Inline tests: 7/7 OK (cold-start, lowest-latency primary, cross-provider backup, demote-on-failure, score-update, primary-success makes 1 call, primary-fail/backup-success makes exactly 2 calls). tests/test_routing_regression.py → 15/15 pass. |
| KI-081 | (no commit — HF Space env secrets) | Pushed GROQ_API_KEY + OPENROUTER_API_KEY to the HF Space repository secrets so the KI-080 cross-provider election candidates actually have working keys in production. Pre-KI-081 only NVIDIA_NIM_API_KEY was set on the Space; the elector would mark every Groq + OpenRouter candidate as no_api_key and election degraded to NIM-only candidates — defeating the cross-provider BACKUP invariant. |
| KI-084 | 119e0fd |
LLM chain telemetry hardening + free-tier guards. Four changes in one commit. (1) Probe cadence PROBE_INTERVAL_SEC raised 60s → 300s — the prior cadence burned ~30-50K probe tokens/day on Groq alone, self-tripping Groq's 100K/day TPD free-tier cap. (2) PROBE_MAX_TOKENS cut 5 → 1 — same 200 envelope, ~50× less token spend per probe. (3) Explicit per-phase httpx.Timeout(connect=2, read=self.timeout, write=2, pool=2) on every chat call — previously timeout=self.timeout collapsed to a single read deadline so a stuck NIM pool could occupy the TCP connection past asyncio.wait_for cancellation, leaking NIM concurrency slots. (4) New _classify_error surfaces HTTP status codes explicitly (Status429 vs HTTPStatusError:503); rate-limit failures get a 1-hour sin-bin (DEGRADE_DURATION_LONG_S = 3600s) instead of the 30s transient window — free-tier daily quotas don't reset in 30 seconds. |
| KI-085 | 8fc7979 |
Proactive credit tracking — closes the reactive-only gap KI-084 leaves. KI-084 demotes a candidate for 1h AFTER a 429 hits, costing one user-facing failover turn per dead quota. KI-085 promotes llm_health from liveness-only to liveness-AND-credits so election excludes quota-exhausted candidates BEFORE the user gets stuck behind a 429. Three signal sources: (1) Groq response headers x-ratelimit-remaining-tokens-day + x-ratelimit-reset-tokens-day (low-water 5K tokens); (2) OpenRouter /api/v1/credits polled every 10 min from probe loop, plus per-call header fallback (low-water $0.05); (3) NIM local 60s rate-meter, gate at 35-of-40 req/min (headroom 5). Election adds _has_credits(h, now_mono) to eligibility predicate. Admin status_summary extended with credits_remaining / credits_unit / credits_low_water per model. 11/11 inline tests pass + routing_regression 15/15. |
| KI-086 | d90f8c0 (bundled with KI-087) |
Admin "LLM Health & Credits" tab. New GET /api/admin/llm-health endpoint returns {chains, candidates, recent_turns, snapshot_ts} JSON: per-chain elected PRIMARY + BACKUP with snapshots, per-candidate health grid with credits + degraded-until, last 20 turn outcomes from 40-data/llm_usage.jsonl. Same _check_admin IP-allowlist + password gate as other admin endpoints. Frontend extends the existing "LLM Chain" tab in frontend/public/admin/llm-control.html with three sections: (A) per-chain election cards, (B) candidate health table, (C) recent turns table. Auto-polls every 30s while tab is active. Operator now sees at-a-glance which LLM is in use where, why a candidate is gated out, and how the election state evolves. |
| KI-087 | d90f8c0 |
NIM-first election preference. Pre-KI-087 election scored purely by latency × success_rate, which consistently favoured Groq's 161ms LPU TTFT over NIM's 500ms-1s — so every probe round elected Groq as PRIMARY across all 3 chains. Result: every chat call hit Groq first, burned Groq's 100K daily TPD inside 50 turns, then started returning 429s. KI-087 changes election so it prefers ANY eligible NIM candidate over ALL non-NIM candidates. Within the NIM pool the standard score still picks the fastest healthy NIM model. Only when the NIM pool is empty does election fall through to Groq / OpenRouter as PRIMARY. BACKUP rule unchanged in spirit: cross-provider against PRIMARY. Rationale: NIM is the strategic free provider (ADR-019, no daily cap, 110+ models, single-key, $0); Groq has 100K daily TPD; OpenRouter charges real USD. Both should serve as emergency fallback only. |
| KI-088 | 14ee008 |
NIM concurrency semaphore + serial probe + dropped inner retry. Pre-KI-088 the process could fire 6+ concurrent NIM HTTP calls (probe burst asyncio.gather across 6 candidates + admin pollers + per-user turns), self-saturating the NIM endpoint and producing timeout_after_escalation failures at 41s wall-clock — the NIM endpoint serialises internally so every overlap added pure queueing latency. Three changes in one commit. (1) Module-level asyncio.Semaphore(2) at backend/nvidia_nim_llm.py:104 wraps every httpx.post to integrate.api.nvidia.com so the entire process never has more than 2 NIM requests in flight simultaneously, regardless of source — probe loop, admin polls, and per-user turns all serialise through the same semaphore. (2) backend/llm_health.py::probe_all changed from asyncio.gather(...) to a serial for m in models: loop so the 6-NIM probe burst becomes a 1-slot trickle over ~12s instead of contending with live user turns. (3) The 4-attempt exponential-backoff inner retry inside NvidiaNimLLM.chat() was deleted — KI-080 sticky-primary election + KI-079 heavy-chain escalation already handle failover at the right layer, so the inner retry only amplified the self-saturation. Live verify post-deploy: failure mode flipped from fallback:timeout_after_escalation at 41s → fallback:no_trailer at 4-10s. NIM concurrency bottleneck closed; new parser-side bottleneck surfaced and is addressed in KI-090. |
| KI-089 | 8a87526 |
Credits-election test fix + paired NIM-empty test. test_groq_above_water_picked_in_election had been failing on main since KI-087 landed: it asserted Groq wins election on raw latency (161ms LPU TTFT), but KI-087 inverted election to prefer any eligible NIM candidate over all non-NIM candidates regardless of latency. Replaced with two paired tests that pin KI-087's invariant explicitly. (1) test_nim_preferred_over_faster_groq_when_eligible — when an eligible NIM candidate exists, election picks it as PRIMARY even though Groq is measurably faster. (2) test_groq_picked_when_nim_pool_empty — when every NIM candidate is dead / throttled / no_credits, election correctly falls through to Groq as PRIMARY. Together the pair pin both halves of the KI-087 contract (NIM-first AND fallthrough-when-NIM-empty) so a future regression that breaks either half fails loudly. Full inline test count: credits-election 12/12 pass, routing-regression 15/15 pass. |
| KI-090 | 11cf4b3 |
Lenient FF-block parser. Post-KI-088 live probe showed ~70% of brain calls returned successfully in 4-10s (NIM concurrency fix surfaced the real bottleneck) but _parse_ff_block rejected the reply with fallback:no_trailer because the brain had dropped the literal <FF>...</FF> tags around its JSON tail. Real LLMs under load (Qwen 3-Next 80B, Nemotron Nano 30B, Groq Llama-3.3-70B) regularly drop the wrapper even when the structured payload is otherwise contract-compliant. New _parse_ff_block tries three strategies in order: (a) strict <FF>{...}</FF> (the original contract); (b) fenced ```json {...} ``` (common LLM habit); (c) bare {...} JSON object at the end of the reply. Each candidate must json.loads cleanly AND contain at least one contract key (captured / slot_driving / complete) before it counts — prevents false positives from prose that happens to contain {...}. _strip_ff_block mirrors the three strategies in reverse so prose-only output to the user never leaks the structured metadata block. Inline tests: 7/7 parse tests + 7/7 strip tests pass. Brain success rate climbs from ~30% (post-KI-088 baseline blocked by parser) toward ~95% (NIM concurrency healthy + parser accepts contract-compliant tails regardless of wrapper). |
| KI-091 | 9813994 |
Skip profile_extractor + faithfulness judge on fact-find turns (saturated-chain hang + mid-session field-clear). Pre-KI-091 every fact-find turn ran two dependent post-brain LLM chains: (a) extract_profile_updates (LLM re-reads the user message to pull profile fields), and (b) check_faithfulness (judge LLM grades the bot's reply against retrieved context). Both chains were credit-exhausted on the steady-state primary, hung the turn for 20+ seconds on asyncio.wait_for, and — critically — the extractor periodically returned {"name": null} for utterances that had nothing to do with name, causing session.update_profile_field("name", None) to wipe the captured value mid-session. next_question(profile) then re-asked the name slot the user had already answered. KI-091 gates both chains behind an intent == "fact_find" short-circuit in backend/orchestrator.py so fact-find turns skip them entirely — the fact-find brain (KI-070) already extracts fields natively from its <FF> JSON tail, and faithfulness scoring is meaningless on a turn whose reply is "what's your annual income?". QA-mode turns (recommendation / comparison / clarification) still run both chains as before. Live verify: name re-ask loop eliminated; fact-find turn latency drops from p95 28s → p95 6-8s. |
| KI-094 | f068094 |
Defensive None-guard in extractor merge — extractor cannot clear a filled profile field even when it runs. Belt-and-braces companion to KI-091. On QA-mode turns the extract_profile_updates chain still runs (it's the right behaviour: a user can mention "I'm now 35" mid-recommendation and the profile should update). But the extractor under load periodically returns {"name": null, "age": null, "dependents": null} — semantically "nothing to extract" but the merge loop was writing every key including nulls back into the session, wiping filled fields. Added if new_value in (None, "", []): continue guard at the top of the extracted-fields loop in backend/orchestrator.py so the merge step ONLY overwrites a profile field when the extractor returns a real value. Null / empty-string / empty-list returns are now no-ops, regardless of which fields they target. Closes the same root-cause hole as KI-091 (LLM returning nulls clears state) at a second layer — extractor running and returning nulls is now safe even when KI-091's intent gate doesn't fire. Together: KI-091 prevents the extractor from running on fact-find turns at all; KI-094 ensures that if it DOES run (QA-mode turn), null returns can't wipe filled fields like name. |
| KI-097 | (pending) | Remove IP allowlist from admin — password-only gate. Removed ADMIN_IP_ALLOWLIST env, deleted _ip_allowed(), simplified _check_admin to password-only, 404→401, ADR-023 superseded. |
| KI-101 | 66eb4ed |
Drop 6 function-local import logging lines from backend/orchestrator.py — fixes UnboundLocalError when the asyncio.wait_for TimeoutError branch fires before reaching the inline import. Pre-KI-101 the orchestrator had a module-level import logging at the top of the file AND six redundant import logging lines inside individual try / except branches deeper in handle_turn(). Python's scoping rule promotes any name assigned (including import name) anywhere in a function to a function-local — so the moment the parser saw any of the inline import logging lines, logging became local to the enclosing function. If a TimeoutError from asyncio.wait_for(...) fired BEFORE control reached the inline import, the logger = logging.getLogger(__name__) line in the except handler raised UnboundLocalError: local variable 'logging' referenced before assignment — masking the actual TimeoutError and surfacing a confusing trace to the user. Removed all 6 inline imports; module-level import is the sole binding and is unambiguously a free variable in every nested scope. No behavioural change beyond eliminating the spurious traceback. |
| KI-102 | 4bb8da0 |
Profile RAG cross-session leak — upsert_profile_chunk stamps session_id metadata, retrieve excludes profile docs from the main pass + per-session lookup triple-checks meta.session_id == session_id; legacy chunks without session_id silently refused (fail-closed). Pre-KI-102 the profile RAG layer wrote one Chroma chunk per session-profile but didn't tag it with session_id, so when User B's session ran retrieval Chroma could return User A's profile chunk if its embedding was a near neighbour — leaking PII (age / dependents / health conditions) across sessions. Three changes in backend/profile_rag.py. (1) upsert_profile_chunk(profile, session_id) now stamps metadata={"doc_type": "profile", "session_id": session_id, ...} on every chunk. (2) Main retrieve() pass excludes doc_type == "profile" via where={"doc_type": {"$ne": "profile"}} so profile docs never enter the general retrieval pool. (3) Separate _get_profile_chunk_for_session(session_id) lookup pulls only the current session's profile by where={"$and": [{"doc_type": "profile"}, {"session_id": session_id}]} AND triple-checks result.metadata["session_id"] == session_id in Python before returning — defence-in-depth against a Chroma where-clause bug. Legacy chunks written pre-KI-102 (no session_id metadata) are silently refused by the lookup (fail-closed, not fail-open). Privacy invariant: a session can NEVER read another session's profile chunk, regardless of embedding similarity. ADR-022 extended with a session-isolation subsection. |
| KI-103 | 8ef5c43 |
_canonical_fallback no_trailer loop-breaker — after 2 failed attempts on the same slot, mark skipped + advance via session._ff_failed_attempts + _ff_skipped_slots. Pre-KI-103 if the brain returned fallback:no_trailer (KI-090 parser also failed) on the same slot 3+ turns in a row, the canonical fallback would re-ask the same canonical question forever — the slot was unfilled, so next_question(slot_id) kept selecting it, brain kept failing, user got an infinite identical-question loop. Added two session-level counters: session._ff_failed_attempts[slot_id] increments every time the brain bails on a given slot; session._ff_skipped_slots: set[str] records slots the canonical fallback has given up on. After 2 consecutive failed attempts on the same slot, _canonical_fallback marks the slot skipped and next_question() excludes it from selection, advancing to the next unfilled slot. The skipped slot is still flagged in the final profile completeness check so the user can fill it via the admin panel later. Caps the worst-case wedge at 2 turns × MAX_SLOTS = 9 ≈ 18 turns to escape fact-find, instead of unbounded. |
| KI-104 | 407f2a1 |
CoT / instruction-echo strip in backend/voice_format.py::tts_preprocess — kills <think> blocks, **Reasoning:** labels, [INTERNAL] blocks, sentence-anchored CoT starters; emergency fallback when the whole reply is CoT. Real LLMs (Qwen 3-Next under load, Nemotron Nano with low temperature) occasionally leak chain-of-thought into the prose: <think>The user is asking about PED waiting periods. I should check...</think> or **Reasoning:** First I need to figure out... **Answer:** The waiting period is 24 months. Pre-KI-104 those leaks went straight to TTS and the user heard Sarvam read out the bot's internal monologue. New strip pipeline: (1) regex-strip <think>...</think> (multi-line, non-greedy); (2) regex-strip **Reasoning:** ... **Answer:** and **Thought:** ... **Final:** patterns; (3) regex-strip [INTERNAL] ... [/INTERNAL]; (4) sentence-level scrub of starters like "Let me think about that.", "First, I'll consider...", "Step 1: ...", "To answer this, I need to..." (anchored at sentence boundary so they don't kill legitimate prose mid-paragraph); (5) emergency fallback — if after all strips the cleaned reply is empty / < 10 chars / still entirely CoT-shaped, return a generic acknowledger ("Let me check that and get back to you.") instead of TTS-ing an empty string or a fragment. Inline tests: 12/12 pass across the 5 strip patterns + emergency-fallback case. |
| KI-105 | 8a58fa1 |
Recommendation closer wired — explicit closer phrases classified as recommendation/comparison BEFORE FACT_FIND_TRIGGERS check; persona prompt gets RECOMMENDATION_CLOSER_ADDENDUM with a strict 3-policy ranked-shortlist contract. Pre-KI-105 a fully-fact-found user saying "Now show me the top 3 policies for me" could still get bounced back into fact-find because the literal word "me" sat inside FACT_FIND_TRIGGERS (substring match was fixed by KI-023 word-boundary regex, but the closer-phrase intent classification was missing). New RECOMMENDATION_CLOSER_PHRASES frozenset in backend/orchestrator.py covers "show me the top 3", "rank", "pitch me", "compare X vs Y", "which is best for me", etc. classify_intent() checks closer phrases FIRST — if matched, intent is forced to "recommendation" (or "comparison" for explicit X-vs-Y patterns) and the FACT_FIND_TRIGGERS check is skipped. Persona prompt for these turns is augmented with RECOMMENDATION_CLOSER_ADDENDUM — a strict contract requiring: (a) exactly 3 ranked policies, (b) one-line rationale per pick tied to the user's profile, (c) the 3-policy sum / IRDAI disclaimer at the end, (d) no hedging like "you might consider...". ADR-008 extended with a closer-mode subsection (the consultative persona retains its core rules; the closer addendum only kicks in when the user explicitly asks for a ranked shortlist). |
| KI-106 | 565bf31 |
Graceful TimeoutError + Exception handling on /api/chat — wrapped in asyncio.wait_for(45s) + explicit catch; both return HTTP 200 with graceful phrasing instead of 500. Pre-KI-106 if handle_turn() raised any unhandled exception OR exceeded an implicit wall-clock budget, FastAPI's default handler emitted HTTP 500 with a generic JSON error body. The frontend treated 500 as a hard error and surfaced "Something went wrong" to the user — a much worse UX than a graceful in-character bot response that admits the bot is overloaded. New wrap in backend/main.py::chat() puts handle_turn(...) inside asyncio.wait_for(coro, timeout=45.0) with explicit except asyncio.TimeoutError and a broad except Exception after that. Both return HTTP 200 with `ChatResponse(reply="I'm having trouble responding right now — could you try that again in a moment?", source="graceful_timeout" |
| KI-107 | 3a9a14f |
_safe_collection_get helper for Chroma — catches exceptions on collection.get(ids=[...]) (especially the KI-102 per-session profile lookup for never-existed sessions); returns None on miss with a WARNING log. Pre-KI-107 the new KI-102 _get_profile_chunk_for_session(session_id) lookup called collection.get(where={"$and": [...]}) directly and could raise on a never-existed session (Chroma's behaviour varies by version — some raise ValueError, some return {"ids": []}, the HF Space build was raising). The raise propagated up through retrieve() into handle_turn() and KI-106 caught it as a graceful exception, but the user got the recovery sentence instead of a normal reply when really there was just no profile chunk yet. New _safe_collection_get(collection, **kwargs) helper in backend/profile_rag.py wraps the .get() call in a try / except Exception, logs logger.warning("profile_rag: collection.get miss for session=%s: %s", session_id, e) on failure, and returns None so callers can short-circuit ("no profile chunk for this session yet — proceed without profile context") instead of raising. Applied at every collection.get(ids=[...]) and collection.get(where=...) call site in profile_rag.py (3 sites total). The KI-102 triple-check semantics are preserved: a None return means "no chunk found" which is treated the same as "chunk found but session_id doesn't match" — both fail-closed. |
| D-001 | (multi) | ChromaDB HNSW bloat 3-layer prevention (ADR-029) |
| D-002 | (LaunchAgent edit) | Three silently-failing LaunchAgent scripts fixed |
| D-009 | bcb7079 |
Removed tmp_*.py debug files from repo root |
| D-022 | (inline) | NIM brain swap on 2026-05-14: Qwen 3-Next 80B (was DeepSeek V4-Pro) + Mistral Large 3 675B judge (was Llama-4 Maverick) + Nemotron Nano 30B fast brain (was DeepSeek V4-Flash). DeepSeek + Maverick retained as fallback chain entries. ADR-019 inline comments still reference the original D-019 lineup; the swap was code-only. |
Verification artifacts
tests/test_routing_regression.py— 15 unit tests, all passing. Pins KI-018 / KI-023 / KI-025 invariants.- 5-Q post-fix smoke (no judge): factual 0% → 60%, nim-chain serving 100% of QA.
- Live HF Space smoke (
https://rohitsar567-insurancebot.hf.space): PED waiting-period question now answers vianim-chain::nemotron-3-nano-30b-a3b::v4-flash::qawith a grounded reply, not the old "Happy to help. First, your age?" misroute. - 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) — captured BEFORE KI-046 (refusal precision) and KI-049 (retrieval top-k boost) shipped:
- Factual accuracy: 54.8% (51 / 93) — up from the pre-fix 41.7% baseline. This number is now stale; a fresh eval is running to measure the KI-046 + KI-049 lift and the headline will be updated once it lands.
- 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%.
- 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…".
- Stuck questions: rows 94-96 (all
regulatory_oosrefusals — those routes are already at ~100% earlier in the run; the rate-limit hang affected the brain call, not the refusal logic). - Expected directional lift from the two pending fixes: KI-049 directly targets the
sub_limitaccuracy gap called out under D-003 (structured cap-table chunks now have ~2× chance of landing in context); KI-046 directly targets theexclusions_oosrefusal-logic gap. Both gaps were the explicit "next bottleneck" in the 54.8% post-mortem.
- Clean 100-persona audit pending — to run once Batch B (bucket reorg) ships and the HF Space is stable.
Pending follow-ups (P1)
| Item | Status |
|---|---|
Tax-related gold-QA questions (currently zero in eval/gold_qa.json — D-006 mitigation) |
Open |
stream-style faithfulness gate (refusal rate 2.7× higher than terse) |
Open |
| Token-streaming responses (SSE) — biggest perceived-latency win remaining | Open (v2 roadmap) |
| Streaming TTS (Sarvam chunked synthesis) | Open |
| GPU-hosted local embeddings (Voyage replacement) | Open — LocalEmbeddings fallback already in backend/providers/local_embeddings.py |
| Broader unit-test coverage (currently only routing + load-balance pinned) | Open |
What "enterprise-grade" actually means for this product
Before insurers will pilot this, the following must be true:
- Factual accuracy ≥ 90% on gold-QA across all question types (currently 30% headline).
- Latency p95 ≤ 3s on chat turns (currently 24s).
- Zero silent failures — every
except Exception:either re-raises or logs. - Production observability — every brain decision, every retrieval, every refusal logged with correlation IDs; dashboards for accuracy/latency/refusal-rate over time.
- Test coverage ≥ 70% with unit + integration tests in CI.
- Fairness audit — accuracy/refusal-rate within ±5% across language styles (hinglish gap is currently 2×).
- Disaster recovery runbook — what happens when ChromaDB corrupts, when HF Space is down, when NIM rate-limits.
- PII handling per DPDP Act — chat logs, uploaded policies, user profiles must have retention policies + deletion workflows.
- IRDAI compliance review — every recommended product must be IRDAI-registered; the bot must never invent a product or premium.
- SOC 2 Type II readiness — secrets management, access logs, change management.
This audit so far covers items 1-3, 5 (in progress), and 6. Items 7-10 require a separate scoping pass.
Sprint 2026-05-15 (KI-167 → KI-179) — chain rewrite + sales_brain rip-out
Second-day follow-on sprint after live user testing flagged that the scripted <FF> trailer convention was leaking into every fallback turn. Full timeline in 70-docs/40-evaluation/quality-sprint-2026-05-14.md (Follow-on sprint section). Highlights against the register:
| KI | Impact on register | Note |
|---|---|---|
| KI-167 (ADR-039) | Closes the "scripted fact-find leakage" defect at root — backend/fact_find_brain.py + _canonical_fallback + the <FF> trailer convention all deleted. Replaced with backend/sales_brain.py (one LLM call per turn, native provider JSON mode). KI-090 lenient parser, KI-103 no_trailer loop-breaker, and the entire fact_find_brain::fallback:* telemetry vocabulary are retired. |
Net negative LOC. |
| KI-168 / KI-173 / KI-174 | Voice UX hardening — hybrid Web Speech (live UX) + MediaRecorder (authoritative blob → Sarvam STT) + tab-switch revival hooks. | Frontend-only. |
| KI-171 | Skip faithfulness Gate 4 on fact_find + recommendation intents (no retrieval context to grade against). |
Tightens D-006 fairness risk surface. |
| KI-175 / KI-176 | NIM chain reorder — Nemotron 49B demoted from primary to last resort; OpenRouter re-added as cross-provider diversity (using OR's models: [...] server-side fallback). |
Mitigates D-004 latency long-tail. |
| KI-178 | Live audit of OR :free response_format support — Llama 3.3 70B / Hermes 3 405B excluded; only Nemotron-3-Super 120B + Qwen 80B :free + Gemma-4 31B included. |
Closes a silent-failure class proactively. |
| KI-179 (ADR-040) | New backend/providers/google_gemini_llm.py; Gemini 2.0 Flash → Brain Fast primary, Gemini 2.5 Flash → Brain Main primary. NIM Tier 1 fallback, OR :free Tier 2 diversity. Judge stays on NIM Mistral 675B (different family from Gemini — preserves brain ↔ judge family-diversity invariant). |
Tier 0 free-tier conversational quality lift. |
The 2026-05-14 scoreboard rows for "Voice UX", "Fact-find robotic tone", and the new "LLM stack" row are now fully green. D-003 (factual accuracy) and D-004 (latency) remain partially open pending a fresh full-eval re-run on the new chain shape.
Sprint 2026-05-27 (KI-225..KI-333) — Single-brain consolidation + upload-pipeline rebuild
Single-brain rewrite (KI-225, 5,200 LOC removed) collapses the old sales+qa+faithfulness chain into one Gemini-with-function-calling call per turn; faithfulness is now structural (brain quotes only what 65-70% post KI-332) + Gemini single-pass / multi-pass / NIM / floor extraction chain + status↔scorecard parity by construction (KI-333) + ACTIVE POLICY DIVE-IN block via retrieve_policies + get_policy_facts returned). Upload pipeline rebuilt to ADR-044 parity: 8-gate defence in backend/security.py + heuristic floor (view_context.active_policy_id (KI-330) + heuristic-floor card on LLM-fail path (KI-331). All shipped on commits 2a58c28, 993bcd5, d92f07a, 2ec48b7; currently live on e7f799a. No new defects introduced — the D-001..D-010 register from this audit remains the live defect set; none are upload-pipeline regressions.
This file regenerates as new evidence lands. Last updated: 2026-05-27 (sprint KI-225 → KI-333 landed; ADR-044 ships).