Spaces:
Sleeping
fix(observability): KI-001..006 — log silent failures + fail-CLOSED judge
Browse filesKI-001 (P0) — Gate 4 LLM judge now fails CLOSED by default
Previously when the judge LLM call raised (rate limit, parse, network),
Gate 4 returned supported=True ("fail-open") — an unsupported claim
that should have been blocked leaked through. In BFSI this is the
wrong default. New behaviour: log the failure + return supported=False
with reason "judge_unavailable_failclosed". Override with
FAITHFULNESS_FAIL_CLOSED=0 in dev/smoke if you need the old behaviour.
KI-002 (P1) — session_state._flush() now logs disk-write failures
Was swallowing the exception silently → user's profile disappeared on
Space restart with no signal. Now logs warning with session_id + error
type so HF Space logs show flush failure rate.
KI-003 (P1) — session_state._load_from_disk() logs schema-drift failures
Was returning None silently when the on-disk JSON didn't match the
current Profile dataclass. Now logs so we can detect when schema
evolves in a way that breaks existing sessions.
KI-004 (P1) — Indic translator failure now logs
Was falling through to send Indic text to English brain silently.
Logs the language + session + error so we can tune Sarvam fallback.
KI-005 (P1) — Profile-chunk upsert failure now logs
Was swallowing Chroma write failures silently → subsequent retrieval
saw stale profile. Logs the failure so we know when Chroma is
unhappy.
KI-006 (P2) — Profile extractor LLM failure now logs
Conversational mid-chat profile updates silently disabled on extractor
failure. Now logs so we know how often the extractor flakes.
None of these changes alter functional behaviour beyond KI-001 (which
flips the default for the BFSI-correct refusal posture). The other five
add observability only — silent failures still don't crash chat, but
they now show up in HF Space logs so we can track failure rates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- backend/faithfulness.py +13 -1
- backend/orchestrator.py +26 -6
- backend/session_state.py +17 -3
|
@@ -251,7 +251,19 @@ Verify."""
|
|
| 251 |
unsupported = list(data.get("unsupported_claims", []))
|
| 252 |
return supported, unsupported
|
| 253 |
except Exception as e:
|
| 254 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
return True, [f"judge_error_failopen: {type(e).__name__}"]
|
| 256 |
|
| 257 |
|
|
|
|
| 251 |
unsupported = list(data.get("unsupported_claims", []))
|
| 252 |
return supported, unsupported
|
| 253 |
except Exception as e:
|
| 254 |
+
# KI-001 — BFSI compliance posture: in production we FAIL CLOSED
|
| 255 |
+
# (block when the judge is unavailable) so an unsupported claim
|
| 256 |
+
# never leaks past Gate 4 just because NIM hiccupped. Set
|
| 257 |
+
# FAITHFULNESS_FAIL_CLOSED=0 in dev/smoke to revert to fail-open.
|
| 258 |
+
import logging
|
| 259 |
+
import os
|
| 260 |
+
logging.warning(
|
| 261 |
+
"faithfulness gate 4 judge failure (%s: %s)",
|
| 262 |
+
type(e).__name__, str(e)[:200],
|
| 263 |
+
)
|
| 264 |
+
fail_closed = os.environ.get("FAITHFULNESS_FAIL_CLOSED", "1") == "1"
|
| 265 |
+
if fail_closed:
|
| 266 |
+
return False, [f"judge_unavailable_failclosed: {type(e).__name__}"]
|
| 267 |
return True, [f"judge_error_failopen: {type(e).__name__}"]
|
| 268 |
|
| 269 |
|
|
@@ -145,8 +145,15 @@ async def handle_turn(
|
|
| 145 |
translated_query = await translate_to_english(user_text)
|
| 146 |
if translated_query and translated_query.strip() and translated_query != user_text:
|
| 147 |
user_text = translated_query # use English for retrieval + reasoning
|
| 148 |
-
except Exception:
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
# 1b. SESSION-STATE-AWARE FACT-FIND
|
| 152 |
# Load session state. If we're already in fact-find (awaiting an answer to a
|
|
@@ -304,10 +311,23 @@ async def handle_turn(
|
|
| 304 |
"health_conditions": session.profile.health_conditions,
|
| 305 |
}
|
| 306 |
await upsert_profile_chunk(session_id or "anonymous", profile_dict_for_chunk)
|
| 307 |
-
except Exception:
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
# 2. Retrieve — pass session_id so the user's profile chunk (stored in
|
| 313 |
# Chroma at POST /api/profile time) gets boosted to the top of the
|
|
|
|
| 145 |
translated_query = await translate_to_english(user_text)
|
| 146 |
if translated_query and translated_query.strip() and translated_query != user_text:
|
| 147 |
user_text = translated_query # use English for retrieval + reasoning
|
| 148 |
+
except Exception as e:
|
| 149 |
+
# KI-004 — surface translator failures in HF Space logs. The
|
| 150 |
+
# brain will still try with the original Indic text, but with
|
| 151 |
+
# degraded quality. The log lets us tune the Sarvam fallback.
|
| 152 |
+
import logging
|
| 153 |
+
logging.warning(
|
| 154 |
+
"indic translator failed (session=%s lang=%s): %s: %s",
|
| 155 |
+
session_id, language, type(e).__name__, str(e)[:200],
|
| 156 |
+
)
|
| 157 |
|
| 158 |
# 1b. SESSION-STATE-AWARE FACT-FIND
|
| 159 |
# Load session state. If we're already in fact-find (awaiting an answer to a
|
|
|
|
| 311 |
"health_conditions": session.profile.health_conditions,
|
| 312 |
}
|
| 313 |
await upsert_profile_chunk(session_id or "anonymous", profile_dict_for_chunk)
|
| 314 |
+
except Exception as e:
|
| 315 |
+
# KI-005 — log profile-chunk upsert failures so we can see
|
| 316 |
+
# when Chroma is locking or schema-drifting. The chat still
|
| 317 |
+
# ships; subsequent turns just won't see the latest profile.
|
| 318 |
+
import logging
|
| 319 |
+
logging.warning(
|
| 320 |
+
"profile-chunk upsert failed (session=%s): %s: %s",
|
| 321 |
+
session_id, type(e).__name__, str(e)[:200],
|
| 322 |
+
)
|
| 323 |
+
except Exception as e:
|
| 324 |
+
# KI-006 — log profile-extraction failures (extractor LLM down,
|
| 325 |
+
# malformed model output, etc.). The chat ships unaffected.
|
| 326 |
+
import logging
|
| 327 |
+
logging.warning(
|
| 328 |
+
"profile extractor failed (session=%s): %s: %s",
|
| 329 |
+
session_id, type(e).__name__, str(e)[:200],
|
| 330 |
+
)
|
| 331 |
|
| 332 |
# 2. Retrieve — pass session_id so the user's profile chunk (stored in
|
| 333 |
# Chroma at POST /api/profile time) gets boosted to the top of the
|
|
@@ -64,8 +64,14 @@ class SessionState:
|
|
| 64 |
tmp = target.with_suffix(".json.tmp")
|
| 65 |
tmp.write_text(json.dumps(payload, indent=2))
|
| 66 |
tmp.replace(target)
|
| 67 |
-
except Exception:
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
def set_awaiting(self, question_id: Optional[str]) -> None:
|
| 71 |
self.awaiting_question_id = question_id
|
|
@@ -111,7 +117,15 @@ def _load_from_disk(session_id: str) -> Optional[SessionState]:
|
|
| 111 |
free_form_session=bool(raw.get("free_form_session", False)),
|
| 112 |
last_touched=float(raw.get("last_touched", time.time())),
|
| 113 |
)
|
| 114 |
-
except Exception:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
return None
|
| 116 |
|
| 117 |
|
|
|
|
| 64 |
tmp = target.with_suffix(".json.tmp")
|
| 65 |
tmp.write_text(json.dumps(payload, indent=2))
|
| 66 |
tmp.replace(target)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
# KI-002 — Log silent failures so HF Space logs reveal them.
|
| 69 |
+
# Don't crash the request — disk hiccups shouldn't kill chat.
|
| 70 |
+
import logging
|
| 71 |
+
logging.warning(
|
| 72 |
+
"session_state flush failed for %s: %s: %s",
|
| 73 |
+
self.session_id, type(e).__name__, str(e)[:200],
|
| 74 |
+
)
|
| 75 |
|
| 76 |
def set_awaiting(self, question_id: Optional[str]) -> None:
|
| 77 |
self.awaiting_question_id = question_id
|
|
|
|
| 117 |
free_form_session=bool(raw.get("free_form_session", False)),
|
| 118 |
last_touched=float(raw.get("last_touched", time.time())),
|
| 119 |
)
|
| 120 |
+
except Exception as e:
|
| 121 |
+
# KI-003 — Log schema-drift / corrupt-JSON failures. The user will
|
| 122 |
+
# get a fresh session either way, but the log lets us detect when
|
| 123 |
+
# the Profile dataclass evolves in a way that breaks old sessions.
|
| 124 |
+
import logging
|
| 125 |
+
logging.warning(
|
| 126 |
+
"session_state load_from_disk failed for %s: %s: %s",
|
| 127 |
+
session_id, type(e).__name__, str(e)[:200],
|
| 128 |
+
)
|
| 129 |
return None
|
| 130 |
|
| 131 |
|