InsuranceBot / 70-docs /40-evaluation /known-issues.md
rohitsar567's picture
chore(cleanup): purge stale narrative/tombstones/dead code β€” codebase reads as the current standard
23b8fad
|
Raw
History Blame Contribute Delete
22.7 kB
# Known Issues + Quality Sprint Log
> ⚠️ **Historical defect log β€” not the present-state map.** Entries below
> are dated incidents; many reference subsystems (`orchestrator`,
> `faithfulness.py` judge, `fact_find_brain` / `sales_brain`,
> `fact_find_normalizer`, the 3-tier Gemini/NIM/OpenRouter chain,
> sticky-primary election) that were **subsequently removed** by the
> single-LLM-with-tools rewrite (one Gemini 2.5-flash call per turn with
> `save_profile_field` / `retrieve_policies` / `mark_recommendation`,
> structured+vector retrieval, small `nim_fallback`). The log is retained
> as the quality trajectory of record; KI/ADR cross-references stay valid as
> history. Present-state authority: [`README.md`](../../README.md) Β§4.
Living document. Every defect we find β€” whether via code review, eval audit,
or production observation β€” lands here with severity, root cause, and the
plan to fix it. Closed issues stay in the log with a `**FIXED in <sha>**`
annotation so reviewers can audit the project's quality trajectory.
## Severity scale
- **P0 / Critical** β€” User-visible incorrect behavior, BFSI compliance risk,
or data loss. Block any release.
- **P1 / High** β€” Silent degradation; user gets a worse experience but it
doesn't visibly break. Ship a fix in the next sprint.
- **P2 / Medium** β€” Edge case; cosmetic; non-critical path. Backlog.
- **P3 / Low** β€” Code smell or minor inefficiency.
---
## Open issues
### KI-001 β€” Gate 4 (LLM judge) fails OPEN on judge error
**Severity:** P0
**Source:** `backend/faithfulness.py:253-255`
**Discovered:** Code-review sweep 2026-05-14
When the judge LLM call fails (network, rate limit, JSON parse error, NIM
408/503), Gate 4 currently returns `supported=True, ["judge_error_failopen"]`.
The reply ships through without grading.
In BFSI this is the *unsafe* default β€” an unsupported claim that should
have been blocked by Gate 4 leaks through to the user because the judge
hiccupped. The audit log preserves `judge_error_failopen` but the user
never sees the gate failed.
**Fix plan:** Add `FAITHFULNESS_FAIL_CLOSED` env var (default `true` in
production, `false` in dev / smoke tests). When fail-closed, return
`supported=False, ["judge_unavailable_failclosed"]` so the orchestrator's
cross-check retry path or final refusal fires instead.
---
### KI-002 β€” Session-state disk flush silently swallows errors
**Severity:** P1
**Source:** `backend/session_state.py:67-68`
**Discovered:** Code-review sweep 2026-05-14
`SessionState._flush()` writes the profile JSON to disk via a tmp+replace
pattern. On disk-full, EACCES, or JSON encode error, the bare `except
Exception: pass` drops the failure with zero observability. The user's
profile is silently lost on the next Space restart. They redo fact-find.
**Fix plan:** Add `logging.warning("session flush failed for %s: %s",
session_id, e)` β€” keep the no-crash behaviour but surface failure rate to
the HF Space logs so we can detect when it starts happening.
---
### KI-003 β€” Session-state disk load silently returns None on schema drift
**Severity:** P1
**Source:** `backend/session_state.py:114-115`
**Discovered:** Code-review sweep 2026-05-14
When the on-disk session JSON has a schema mismatch (Profile dataclass
field renamed, type changed), `_load_from_disk` catches the exception and
returns `None`. The user gets a fresh blank profile and has to redo
fact-find. No log, no metric.
**Fix plan:** Log the exception with `session_id` so we know schema drift
is happening. Also: tighten the existing valid-field filter (line 105-106)
to additionally type-check values so a stringified int doesn't pass through.
---
### KI-004 β€” Indic translator failure β†’ original Indic text flows into English brain silently
**Severity:** P1
**Source:** `backend/orchestrator.py:148-149`
**Discovered:** Code-review sweep 2026-05-14
When Sarvam-M translator fails on an Indic query, the orchestrator falls
through with the original Indic text, sending it to the English-trained
DeepSeek/NIM brain. The brain handles it imperfectly. The user gets a
degraded reply with no indication that the translator failed.
**Fix plan:** Log the failure (`logging.warning("Indic translator failed
for session %s, lang=%s: %s", session_id, language, e)`). Optionally
return a soft refusal in Indic ("Sorry, I'm having trouble with the
translation right now β€” could you ask in English?") instead of silently
mis-routing.
---
### KI-005 β€” Profile-RAG chunk upsert failure silently swallowed
**Severity:** P1
**Source:** `backend/orchestrator.py:285-287`
**Discovered:** Code-review sweep 2026-05-14
After the conversational profile-update extractor lands a new field, the
orchestrator re-upserts the profile chunk into Chroma so retrieval reflects
the latest state. If that Chroma write fails (lock, disk, schema), the
exception is swallowed. Subsequent turns retrieve the *stale* profile.
The user thinks the bot incorporates their new fact ("I just got
diabetes"); it actually doesn't.
**Fix plan:** Log the failure + record in `TurnResult.profile_updates`
that the upsert hit a problem so the frontend can show a small warning
or retry.
---
### KI-006 β€” Conversational profile extraction failure silently swallowed
**Severity:** P2
**Source:** `backend/orchestrator.py:288-289`
**Discovered:** Code-review sweep 2026-05-14
If `extract_profile_updates()` itself raises (rare; NIM unavailable), the
mid-chat profile-update feature is silently disabled for that turn. User
won't know why their "I just turned 40" didn't take.
**Fix plan:** Log + add to `TurnResult.profile_updates_meta` so the
frontend could surface "we missed an update β€” try mentioning it again".
---
### KI-007 β€” Indic cascade total failure β†’ English reply with zero log
**Severity:** P2
**Source:** `backend/orchestrator.py:425-426`
**Discovered:** Code-review sweep 2026-05-14
When all three Indic drift gates fail (or `translate_to_indic` itself
raises), we fall back to English. The user asked in Hinglish but gets
English. No log of which gate failed.
**Fix plan:** Add structured logging of which gate caused the fall-back
(`anchor` / `llmjudge` / `cosine`) so we can tune thresholds against real
production drift data.
---
### KI-008 β€” TTS preprocess can swallow blocking content
**Severity:** P3
**Source:** `backend/main.py:258-272`
**Discovered:** Code-review sweep 2026-05-14
`tts_preprocess()` is called inside a `try: … except Exception as e: log
+ return text only` block. If the preprocessor strips the acronym expansion
incorrectly, the TTS voice would butcher PED / SI / IRDAI etc. No fall-back
to a hard-coded acronym dict β€” we just log + skip.
**Fix plan:** Add a regression test for `tts_preprocess()` covering the 20
most common BFSI acronyms.
---
### KI-009 β€” Live-mode VAD: no calibration on entry
**Severity:** P2
**Source:** `frontend/src/lib/useLiveConversation.ts` β€” `rmsThreshold: 28`
**Discovered:** Code-review sweep 2026-05-14
The RMS threshold is a constant. Quiet speakers, far-mic users, and noisy
backgrounds all hit one fixed bar. Some users won't trigger VAD; others
will trigger it on background noise.
**Fix plan:** Calibrate the threshold by sampling 1 second of ambient
audio on Live-mode entry. Set threshold to `mean(ambient) + 2 * sigma`.
---
### KI-010 β€” Audit runner: output unbuffered required `PYTHONUNBUFFERED=1`
**Severity:** P3
**Source:** `tools/audit/run_audit.py`
**Discovered:** Self-test 2026-05-14
Initial run had zero progress prints in the captured log because Python's
default stdout buffering held lines until process exit. Fixed in the same
session: all `print()` calls now use `flush=True` and we add per-5-turn
progress prints. Document for future tooling.
**Status:** FIXED in commit during audit framework rollout.
---
## Closed issues (this session)
- **Issue 1: Full-duplex voice barge-in** β€” shipped in `d31e132`.
- **Issue 2 + 4: Garbage profile recording + sidebar sync** β€” shipped in `9a1b321`.
- **Issue 3: Sarvam STT 400 (webm→wav)** — shipped in `a777198`.
- **Bug A: Cold-start "Load failed"** β€” shipped in `f81328f`.
- **Bug B: Citation chip insurer prefix** β€” shipped in `f81328f`.
- **Bug C: "Try again" intent handling** β€” shipped in `f81328f`.
---
## Quality-sprint cadence
Every batch of fixes ships as one commit referencing the KI numbers it
closes. The audit run (`80-audit/<run_id>/report.md`) is the
empirical signal for whether a fix is actually working in production.
The standing ratio target: **for every 1 user-facing bug a reviewer
catches, we should close 5 internal issues from this log before the next
review.**
### KI-011 β€” Fact-find re-ask infinite loop under load β€” **FIXED in `171f2a4`**
**Severity:** P0
**Source:** `backend/orchestrator.py` fact-find branch + `backend/fact_find_normalizer.py` LLM-only path
**Discovered:** First persona of the 100-persona audit (P002, verbose style)
When NIM rate-limited the Llama-3.3-70B normalizer under audit
concurrency, the LLM call raised, the orchestrator marked the answer
ambiguous, kept `awaiting_question_id` set, and re-asked the same
question on the next turn. User moved on with answers to OTHER
questions. Normalizer rejected them. Bot re-asked again. Infinite loop.
**Fix:** Keyword fast-path in `fact_find_normalizer.py` (hand-curated
substring matchers for 9 metros, 15 tier1 cities, dependents
combinations, income/budget bands, primary goals, common health
conditions) bypasses the LLM for ~80% of answers. Re-ask cap in
`orchestrator.py` gives up after 2 failed normalizations on the same
question and marks it asked. Production audit on persona P002
post-fix: 30/30 turns completed in 85s with 0 refusals (vs. infinite
loop before).
### KI-012 β€” Bot stuck in fact_find_complete readback loop β€” **FIXED in next commit**
**Severity:** P0
**Source:** `backend/orchestrator.py` fact-find-complete branch
**Discovered:** Reviewing audit transcript of P002 post-KI-011-fix
After fact-find completes, the orchestrator only calls
`session.set_awaiting(None)` β€” it does NOT flip
`session.free_form_session = True`. On every subsequent turn, the
classifier still routes through the fact-find branch (because
`free_form_session` is false), `next_question()` returns None (all
fields captured), and the code path emits the readback summary AGAIN
instead of going to retrieval + brain.
**Effect:** P002 used 19 of 30 turns repeating the same readback
"Got it β€” here's what I've understood: …" instead of answering the
user's real policy questions. Every persona that completes fact-find
hits this.
**Fix:** When fact-find completes, set
`session.free_form_session = True` and flush to disk. Subsequent turns
skip the fact-find branch entirely and go through retrieval + brain
as intended.
### KI-013 β€” Bot recommends policies on vague openers without fact-find β€” **FIXED in next commit**
**Severity:** P0
**Source:** `backend/orchestrator.py` intent β†’ fact_find gating
**Discovered:** Real user testing 2026-05-14
User gave a vague opening ("I want a health policy") and bot
immediately retrieved + pitched "Care Senior" β€” a senior-citizen-only
policy. User is not a senior. The intent classifier routed the message
to "recommendation" / "qa", not to fact_find. The orchestrator then
went straight to retrieval + brain β†’ bot recommended whatever scored
highest, regardless of user demographics.
**Fix:** Force fact-find whenever profile is empty (no age, no
dependents, no income_band). Regardless of what the intent classifier
says. Bot will now always start with "First, your age?" before any
recommendation.
### KI-014 β€” Vague dependents term "family" auto-mapped to self+spouse+kids β€” **FIXED in next commit**
**Severity:** P1
**Source:** `backend/fact_find_normalizer.py` keyword fast-path
**Discovered:** Real user testing 2026-05-14
User said "family" as their dependents answer. Bot assumed
"self+spouse+kids". User had intended their joint family (parents +
siblings). All subsequent recommendations were wrong.
**Fix:** Add VAGUE_TERMS list (`family`, `everyone`, `joint family`,
etc.) that explicitly returns None from the keyword fast-path,
forcing either the LLM normalizer (which is more nuanced) or a re-ask
clarifier. Phrases like "family β€” me and my wife" still parse
correctly because the disambiguating words come through.
### KI-015 β€” Age in readback summary doesn't match user's stated age
**Severity:** P1
**Source:** Possibly `backend/needs_finder.py::record_answer` for age,
or LLM readback hallucination
**Discovered:** Real user testing 2026-05-14
User said "31" but bot's readback summary said "30". Possible causes:
(a) User's earlier answer contained "30" that the int parser caught
first; (b) The bot is using the LLM to generate the readback and the
LLM is hallucinating numeric values.
**Fix plan:** Add a CONFIRMATION step before the bot transitions to
free-form recommendations. After fact-find readback, the bot should
ask "Does this all look right? Reply 'yes' or correct anything that's
off." Then proceed only if user confirms. Also: log the raw fact-find
inputs vs the captured profile so we can debug mismatches.
### KI-016 β€” NIM has promoted Qwen3-next-80B over DeepSeek-V4-Flash
**Severity:** P2
**Source:** Live audit log: `brain=nim-chain::qwen3-next-80b-a3b-instruct::v4-flash::qa`
**Discovered:** Audit run 2026-05-14
The NIM chain now tries Qwen3-next-80B BEFORE DeepSeek-V4-Flash for
`qa` intents. NIM-side catalog change (not ours). Latency per turn
~10s β€” slower than V4-Flash's ~3s. Worth investigating whether
Qwen3-next is empirically a better fit than V4-Flash for our use
case (Indian health-insurance grounded Q&A), or whether we should
explicitly demote it via the admin panel's chain reorder.
**Fix plan:** Run eval/run.py on the gold set with each model
isolated as primary, compare factual/citation/refusal scores. If
V4-Flash wins, reorder via /api/admin/chain.
### KI-017 β€” Reviews underrepresented in vector store β€” **FIXED in `next commit`**
**Severity:** P2 β†’ user-facing (sparse review retrieval)
**Source:** `tools/ingest_reviews.py` produced 1 chunk per insurer (~500 chars)
**Discovered:** Architecture audit 2026-05-14
Pre-fix state: 10 review chunks in Chroma vs ~116 KB of structured
review data in `data/reviews/*.json` (10 insurers, each with claim
metrics, aggregator ratings, Reddit sentiment, YouTube coverage,
news, aggregate score). A user asking "what do customers say about
Star Health?" retrieved only ONE generic paragraph per insurer,
losing the nuance of metrics-vs-sentiment-vs-news.
**Fix:** Refactored `review_to_paragraph()` β†’ `review_to_chunks()`
that yields 4-6 semantically distinct chunks per insurer:
1. CLAIM METRICS (IRDAI primary-source numbers)
2. AGGREGATOR RATINGS (Policybazaar, InsuranceDekho, MouthShut, Trustpilot)
3. REDDIT/QUORA SENTIMENT (notable themes + sample post URLs)
4. YOUTUBE COVERAGE (creator reviews + sentiment)
5. RECENT NEWS (verified press coverage, one line per item)
6. OVERALL TRUST SCORE (aggregate + letter grade + computation notes)
Each chunk gets a `review_facet` metadata field so retrieval can
filter or boost by facet when intent is clear. Live count:
60 chunks total (was 10), all 10 insurers Γ— 6 facets.
Verified locally; pushed to HF Dataset; live HF Space picks up
on next rebuild.
### KI-018 β€” `rag/policies.duckdb` stub is dead code (132 bytes, never populated)
**Severity:** P3
**Source:** `rag/policies.duckdb` is in the repo but empty (132-byte sqlite header only)
**Discovered:** Data architecture audit 2026-05-14
ADR-004 designed a hybrid structured (DuckDB) + vector (Chroma) split. The
DuckDB half was never populated; the marketplace UI ended up reading
`data/policy_facts/*.json` directly (one file per policy) at request time
via the `/api/policies/all` endpoint. The 132-byte file in the repo is
visually misleading β€” looks like a real store, isn't.
**Fix plan (two options):**
- (A) Populate it from `data/policy_facts/*.json` so the marketplace can
do SQL filters (sum-insured β‰₯ X AND room-rent-cap = no AND restoration
= unlimited). Worth ~30 min.
- (B) Remove the file + the `import duckdb` + the `policies.duckdb` ADR
reference + add a note in ADR-025 explaining why structured filtering
reads JSON directly. Cleaner architecturally.
**Recommendation:** (A) β€” once activated, the marketplace tab gets
proper SQL filtering that's faster than the current N-JSON-load pattern.
### KI-167 β€” Scripted `<FF>` trailer + canonical_fallback removed in favour of LLM-driven `sales_brain` β€” **FIXED** (ADR-039)
**Severity:** P0
**Source:** `backend/fact_find_brain.py` + `_canonical_fallback` + `_pick_opener` family in `backend/orchestrator.py`
**Discovered:** 2026-05-15 β€” user report *"zero natural LLM chat β€” it always defaults to the script."*
Pre-fix, every fact-find turn ran through ADR-030's one-call brain with a `<FF>...</FF>` trailer convention; on parse failure or contract violation the orchestrator fell to `_canonical_fallback`, which emitted the scripted `Question.prompt_en` of the next unfilled slot prefixed with `"Got that β€” {slot}."`. Real LLMs (Qwen, Nemotron, Mistral) under load regularly dropped the trailer tags (KI-090 lenient parser bought partial recovery; KI-155 / KI-156 / KI-158 / KI-161 showed it was structural) and silently violated the contract, so the scripted prefix became the dominant user-facing path.
**Fix:** Ripped out `backend/fact_find_brain.py` (441 LOC) + `_canonical_fallback` + `_pick_opener` + `_NEUTRAL_OPENERS` + `_FAMILY_OPENERS` + `_contains_self_introduction`. Replaced with `backend/sales_brain.py` β€” one LLM call per turn against `FAST_BRAIN_CHAIN` using native provider JSON mode (`response_mime_type=application/json` on Gemini, `response_format={"type":"json_object"}` on NIM). New deterministic post-processor `backend/sales_brain_normalizer.py` validates and normalises the captured fields. KI-091 / KI-094 None-guards retained. See [ADR-039](../60-decisions/ADR-039-llm-driven-sales-brain.md).
---
### KI-168 β€” Hybrid voice capture (Web Speech + MediaRecorder + Sarvam) β€” **FIXED**
**Severity:** P1
**Source:** `frontend/src/lib/useStreamingVoice.ts`
**Discovered:** 2026-05-15
Pre-fix voice UX waited for the full Sarvam STT round-trip before showing the user their transcribed text β€” felt sluggish on the chat surface. New hybrid: Web Speech API streams interim transcripts into the chat input live, while `MediaRecorder` captures the authoritative audio blob in parallel; on browser silence-detect the blob is POSTed to `/api/transcribe` (Sarvam Saarika STT) for the authoritative transcript. Auto-submit falls back to the Web Speech transcript only if Sarvam errors. KI-173 heartbeat + KI-174 `visibilitychange` / `focus` revival hooks keep the mic alive across tab and app switches.
---
### KI-171 β€” Skip faithfulness judge on `fact_find` + `recommendation` intents β€” **FIXED**
**Severity:** P1
**Source:** `backend/orchestrator.py`
**Discovered:** 2026-05-15
Faithfulness Gate 4 (LLM judge) was being run on every turn including fact-find and recommendation, where there's no retrieved-chunks context to grade the reply against β€” burning judge calls and occasionally blocking valid replies. Now gated behind `intent ∈ {qa, comparison}` only. Recommendation replies still go through Gates 1-3 (retrieval floor, citation integrity, regex numeric grounding).
---
### KI-175 / KI-176 β€” NIM chain reorder + OpenRouter re-added as cross-provider diversity β€” **FIXED**
**Severity:** P2
**Source:** Chain definitions in `backend/providers/nvidia_nim_llm.py`
**Discovered:** 2026-05-15
KI-175 demoted NIM Nemotron 49B from primary to last resort across all three chains β€” Mistral Large 3 675B and Qwen 80B consistently outperform on the live audit. KI-176 re-added OpenRouter as a cross-provider diversity tier using OR's `models: [...]` array param for server-side fallback within the OR free pool. The NIM-only lock from [ADR-038](../60-decisions/ADR-038-nim-only-chains.md) was relaxed in scope (the `<FF>` trailer convention that motivated it no longer exists post-KI-167); only OR `:free` candidates with verified native JSON mode are included (Nemotron-3-Super 120B, Qwen 80B `:free`).
---
### KI-178 / KI-179 β€” Google Gemini Flash added as Tier 0 primary β€” **FIXED** (ADR-040)
**Severity:** P2
**Source:** `backend/providers/google_gemini_llm.py` (new); chain definitions in `nvidia_nim_llm.py`
**Discovered:** 2026-05-15
Operator obtained a Google AI Studio API key. KI-178 audited which OpenRouter free-tier models support `response_format` (only Nemotron-3-Super 120B + Qwen 80B + Gemma-4 31B). KI-179 added a new `LLMProvider` against Google AI Studio's REST API (`gemini-2.0-flash` + `gemini-2.5-flash`, native JSON mode via `response_mime_type=application/json`, 1500 req/day free quota). Promoted to Tier 0 primary on Brain Fast (Gemini 2.0 Flash) + Brain Main (Gemini 2.5 Flash). NIM stays as Tier 1 fallback with Mistral 675B as the strongest non-Gemini candidate; OpenRouter sits as Tier 2 diversity. Judge stays on NIM Mistral 675B (different family from Gemini β€” preserves brain ↔ judge family-diversity invariant). See [ADR-040](../60-decisions/ADR-040-google-gemini-primary.md).
---
### KI-019 β€” `kb/policies/*.md` not embedded (224 human-readable summaries left out of retrieval)
**Severity:** P3
**Source:** `kb/policies/<policy_id>.md` files exist (224 markdown summaries) but the ingest pipeline only reads from `rag/extracted/*.json`. The natural-language summary text is never embedded.
**Discovered:** Data architecture audit 2026-05-14
Each policy has a hand-written or auto-generated `kb/policies/<id>.md`
that summarizes the policy in 1-2 pages of natural English (e.g.
"Care Supreme covers in-patient hospitalization across India with a
36-month PED waiting…"). These would be EXCELLENT retrieval targets
for "what does X cover?" style questions because they're already
phrased for human reading. Currently retrieval matches against raw
PDF-extracted prose which is denser + less semantic.
**Fix plan:** Add a kb-markdown ingestion path in `rag/ingest.py` that
walks `kb/policies/*.md`, splits at H2 boundaries (each H2 β‰ˆ one
schema-field discussion), embeds each section, and writes with
`doc_type="summary"` so retrieval can prefer summary chunks when the
intent is high-level Q&A.
Expected impact: better grounding for natural-language "tell me about X"
questions; cleaner citations (one summary chunk vs. 3-4 raw chunks).