Spaces:
Sleeping
Known Issues + Quality Sprint Log
β οΈ Historical defect log β not the present-state map. Entries below are dated incidents; many reference subsystems (
orchestrator,faithfulness.pyjudge,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 withsave_profile_field/retrieve_policies/mark_recommendation, structured+vector retrieval, smallnim_fallback). The log is retained as the quality trajectory of record; KI/ADR cross-references stay valid as history. Present-state authority: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:
- CLAIM METRICS (IRDAI primary-source numbers)
- AGGREGATOR RATINGS (Policybazaar, InsuranceDekho, MouthShut, Trustpilot)
- REDDIT/QUORA SENTIMENT (notable themes + sample post URLs)
- YOUTUBE COVERAGE (creator reviews + sentiment)
- RECENT NEWS (verified press coverage, one line per item)
- 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/*.jsonso 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+ thepolicies.duckdbADR 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.
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 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.
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).