rohitsar567 commited on
Commit
5de86d2
·
verified ·
1 Parent(s): 629dfe2

Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.

Browse files
backend/faithfulness.py CHANGED
@@ -192,7 +192,20 @@ Be strict. The bot's job is to NOT hallucinate. If a claim is ambiguously suppor
192
  """
193
 
194
 
195
- async def _gate_llm_judge(reply: str, chunks: list[RetrievedChunk]) -> tuple[bool, list[str]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  if not reply or len(reply) < 30:
197
  return True, []
198
  if not chunks:
@@ -209,6 +222,17 @@ REPLY:
209
 
210
  Verify."""
211
 
 
 
 
 
 
 
 
 
 
 
 
212
  try:
213
  judge = _get_judge()
214
  res = await judge.chat(
@@ -219,6 +243,8 @@ Verify."""
219
  temperature=0.0,
220
  max_tokens=400,
221
  response_format={"type": "json_object"},
 
 
222
  )
223
  data = json.loads(res.text)
224
  supported = bool(data.get("supported", False))
@@ -244,8 +270,14 @@ async def check_faithfulness(
244
  chunks: list[RetrievedChunk],
245
  user_text: str = "",
246
  run_llm_judge: bool = True,
 
247
  ) -> FaithfulnessVerdict:
248
- """Run all gates. Return verdict with reasons + a safe reply to show user if blocked."""
 
 
 
 
 
249
  verdict = FaithfulnessVerdict(passed=True)
250
 
251
  # Gate 1 — retrieval floor
@@ -280,7 +312,7 @@ async def check_faithfulness(
280
  HIGH_CONFIDENCE_FLOOR = 0.50
281
  top_score = max((c.score for c in chunks), default=0.0) if chunks else 0.0
282
  if verdict.passed and run_llm_judge and top_score < HIGH_CONFIDENCE_FLOOR:
283
- ok4, unsupported = await _gate_llm_judge(reply, chunks)
284
  if not ok4:
285
  verdict.passed = False
286
  verdict.reasons.append("gate4_llm_judge: claims unsupported")
 
192
  """
193
 
194
 
195
+ async def _gate_llm_judge(
196
+ reply: str,
197
+ chunks: list[RetrievedChunk],
198
+ brain_model_used: Optional[str] = None,
199
+ ) -> tuple[bool, list[str]]:
200
+ """LLM judge for faithfulness Gate 4 with cross-family independence guard.
201
+
202
+ `brain_model_used` is the EXACT model id that produced `reply` (e.g.
203
+ 'qwen/qwen3-next-80b-a3b-instruct'). It and its family are excluded from
204
+ the judge's chain so the brain never grades its own homework. If the
205
+ exclusion would empty the chain, NimChainLLM relaxes the family
206
+ constraint but still enforces exact-model exclusion — strictly weaker
207
+ than letting the same model grade itself.
208
+ """
209
  if not reply or len(reply) < 30:
210
  return True, []
211
  if not chunks:
 
222
 
223
  Verify."""
224
 
225
+ # Compute exclusion set for cross-grading independence
226
+ exclude_models: list[str] = []
227
+ exclude_families: list[str] = []
228
+ if brain_model_used:
229
+ exclude_models.append(brain_model_used)
230
+ try:
231
+ from backend.providers.nvidia_nim_llm import NimChainLLM
232
+ exclude_families.append(NimChainLLM._family_of(brain_model_used))
233
+ except Exception:
234
+ pass # family helper unavailable → still enforce exact-model exclusion
235
+
236
  try:
237
  judge = _get_judge()
238
  res = await judge.chat(
 
243
  temperature=0.0,
244
  max_tokens=400,
245
  response_format={"type": "json_object"},
246
+ exclude_models=exclude_models or None,
247
+ exclude_families=exclude_families or None,
248
  )
249
  data = json.loads(res.text)
250
  supported = bool(data.get("supported", False))
 
270
  chunks: list[RetrievedChunk],
271
  user_text: str = "",
272
  run_llm_judge: bool = True,
273
+ brain_model_used: Optional[str] = None,
274
  ) -> FaithfulnessVerdict:
275
+ """Run all gates. Return verdict with reasons + a safe reply to show user if blocked.
276
+
277
+ `brain_model_used` is forwarded to Gate 4 so the judge can never be the
278
+ same model (or same family) as the brain that produced `reply` —
279
+ enforces the cross-grading independence invariant.
280
+ """
281
  verdict = FaithfulnessVerdict(passed=True)
282
 
283
  # Gate 1 — retrieval floor
 
312
  HIGH_CONFIDENCE_FLOOR = 0.50
313
  top_score = max((c.score for c in chunks), default=0.0) if chunks else 0.0
314
  if verdict.passed and run_llm_judge and top_score < HIGH_CONFIDENCE_FLOOR:
315
+ ok4, unsupported = await _gate_llm_judge(reply, chunks, brain_model_used=brain_model_used)
316
  if not ok4:
317
  verdict.passed = False
318
  verdict.reasons.append("gate4_llm_judge: claims unsupported")
backend/orchestrator.py CHANGED
@@ -238,6 +238,11 @@ async def handle_turn(
238
  raw = llm_result.text
239
  reply = strip_think_tags(raw)
240
 
 
 
 
 
 
241
  # 5. FAITHFULNESS GATE — every reply runs through 4-gate verification.
242
  # If any gate fails, replace the reply with a safe refusal. The original
243
  # blocked reply is logged to logs/hallucinations.jsonl for audit.
@@ -246,6 +251,7 @@ async def handle_turn(
246
  chunks=chunks,
247
  user_text=user_text,
248
  run_llm_judge=True,
 
249
  )
250
 
251
  # 5a. CROSS-CHECK RETRY — if faithfulness blocked AND the failure isn't
@@ -262,8 +268,11 @@ async def handle_turn(
262
  secondary = NvidiaNimLLM(model=NIM_JUDGE_MODEL)
263
  second = await secondary.chat(messages=messages, temperature=0.1, max_tokens=1500)
264
  second_reply = strip_think_tags(second.text)
 
 
265
  second_verdict = await check_faithfulness(
266
  reply=second_reply, chunks=chunks, user_text=user_text, run_llm_judge=True,
 
267
  )
268
  if second_verdict.passed:
269
  reply = second_reply
 
238
  raw = llm_result.text
239
  reply = strip_think_tags(raw)
240
 
241
+ # Capture the EXACT model that produced the reply — flows into the
242
+ # faithfulness LLM-judge so it can never grade its own homework
243
+ # (same model OR same family is excluded from the judge chain).
244
+ brain_model_actual = getattr(llm_result, "model", None) or getattr(pick.provider, "model", None)
245
+
246
  # 5. FAITHFULNESS GATE — every reply runs through 4-gate verification.
247
  # If any gate fails, replace the reply with a safe refusal. The original
248
  # blocked reply is logged to logs/hallucinations.jsonl for audit.
 
251
  chunks=chunks,
252
  user_text=user_text,
253
  run_llm_judge=True,
254
+ brain_model_used=brain_model_actual,
255
  )
256
 
257
  # 5a. CROSS-CHECK RETRY — if faithfulness blocked AND the failure isn't
 
268
  secondary = NvidiaNimLLM(model=NIM_JUDGE_MODEL)
269
  second = await secondary.chat(messages=messages, temperature=0.1, max_tokens=1500)
270
  second_reply = strip_think_tags(second.text)
271
+ # Cross-check brain was NIM_JUDGE_MODEL — pass its id so the
272
+ # judge for THIS retry also excludes that model+family.
273
  second_verdict = await check_faithfulness(
274
  reply=second_reply, chunks=chunks, user_text=user_text, run_llm_judge=True,
275
+ brain_model_used=getattr(second, "model", None) or NIM_JUDGE_MODEL,
276
  )
277
  if second_verdict.passed:
278
  reply = second_reply
backend/providers/nvidia_nim_llm.py CHANGED
@@ -311,21 +311,64 @@ class NimChainLLM(LLMProvider):
311
  )
312
  return NvidiaNimLLM(model=model_id, api_key=self.api_key, timeout=timeout)
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  async def chat(
315
  self,
316
  messages: list[ChatMessage],
317
  temperature: float = 0.2,
318
  max_tokens: int = 1024,
319
  response_format: Optional[dict] = None,
 
 
320
  ) -> LLMResult:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  # Filter out models known-down (from background probe loop). If all
322
  # models are down, filter_chain returns the full chain unchanged so
323
  # we still try — the infrastructure may have recovered between probes.
 
 
324
  try:
325
  from backend import llm_health
326
- chain_to_try = llm_health.filter_chain(self.chain)
327
  except Exception:
328
- chain_to_try = self.chain # health monitor failure must never block calls
329
 
330
  chain_primary = self.chain[0] if self.chain else None
331
  call_t0 = time.time()
 
311
  )
312
  return NvidiaNimLLM(model=model_id, api_key=self.api_key, timeout=timeout)
313
 
314
+ @staticmethod
315
+ def _family_of(model_id: str) -> str:
316
+ """Coarse family bucket for cross-grading-independence checks.
317
+
318
+ Two models in the SAME family must never be paired as brain ↔ judge
319
+ because they share weights / training corpus / decision surface, so
320
+ the judge would effectively grade its own siblings' output.
321
+ Families: 'qwen', 'mistral', 'meta', 'openai', 'deepseek', 'moonshot',
322
+ 'minimax', 'nvidia', 'unknown'.
323
+ """
324
+ m = model_id.lower()
325
+ # Strip provider prefix first so 'groq:llama-3.3-70b' → 'meta' (it IS Meta Llama)
326
+ if ":" in m:
327
+ m = m.split(":", 1)[1]
328
+ if "qwen" in m: return "qwen"
329
+ if "mistral" in m: return "mistral"
330
+ if "llama" in m or m.startswith("meta/"): return "meta"
331
+ if "gpt-oss" in m or m.startswith("openai/"): return "openai"
332
+ if "deepseek" in m: return "deepseek"
333
+ if "kimi" in m or m.startswith("moonshot"): return "moonshot"
334
+ if "minimax" in m: return "minimax"
335
+ if "nemotron" in m or m.startswith("nvidia/"): return "nvidia"
336
+ return "unknown"
337
+
338
  async def chat(
339
  self,
340
  messages: list[ChatMessage],
341
  temperature: float = 0.2,
342
  max_tokens: int = 1024,
343
  response_format: Optional[dict] = None,
344
+ exclude_models: Optional[list[str]] = None,
345
+ exclude_families: Optional[list[str]] = None,
346
  ) -> LLMResult:
347
+ # Apply caller's exclusion list FIRST (brain doesn't grade own homework).
348
+ # exclude_models: skip exact model IDs (handles same-model collision)
349
+ # exclude_families: skip everything in those families (handles weight-sharing siblings)
350
+ chain = self.chain
351
+ if exclude_models or exclude_families:
352
+ excl_m = set(exclude_models or [])
353
+ excl_f = set(exclude_families or [])
354
+ chain = [m for m in chain
355
+ if m not in excl_m and self._family_of(m) not in excl_f]
356
+ if not chain:
357
+ # Every candidate excluded — relax family constraint, keep exact model
358
+ # constraint (the strict one). Better to use a same-family model than
359
+ # to fail the request entirely.
360
+ chain = [m for m in self.chain if m not in excl_m]
361
+
362
  # Filter out models known-down (from background probe loop). If all
363
  # models are down, filter_chain returns the full chain unchanged so
364
  # we still try — the infrastructure may have recovered between probes.
365
+ # Use the post-exclusion `chain` (NOT self.chain) so brain↔judge
366
+ # independence is preserved even after health filtering.
367
  try:
368
  from backend import llm_health
369
+ chain_to_try = llm_health.filter_chain(chain)
370
  except Exception:
371
+ chain_to_try = chain # health monitor failure must never block calls
372
 
373
  chain_primary = self.chain[0] if self.chain else None
374
  call_t0 = time.time()