| """Deterministic gate β no LLM calls, pure rule-based decision. |
| |
| v0.4: Slot-mismatch guard removed. Semantic relevance is a known limitation. |
| Only status-pair contradictions are forced. Numeric/date/money possible conflicts |
| are logged but do NOT force gate decisions. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import re |
|
|
| from .schemas import ( |
| EvidenceSpan, |
| GateDecision, |
| GateOutput, |
| VerifiedClaim, |
| VerifierOutput, |
| ) |
|
|
| _UNKNOWN_LABELS = {"UNSUPPORTED", "NEEDS_INFO", "NOT_IN_EVIDENCE"} |
|
|
|
|
| def apply_gate( |
| question: str, |
| draft_answer: str, |
| verifier_output: VerifierOutput, |
| pressure_level: int, |
| spans: list[EvidenceSpan], |
| ) -> GateOutput: |
| """Apply deterministic gating rules and produce a final answer.""" |
|
|
| |
| if verifier_output.parse_error: |
| return GateOutput( |
| final_answer=( |
| "I wasn't able to properly verify this answer β the verification " |
| "step produced an invalid result. I'd rather not give you something " |
| "I can't stand behind.\n\nCould you try rephrasing, or provide " |
| "additional evidence so I can give you a reliable answer?" |
| ), |
| decision="verifier_error", |
| included_claims=[], unknown_claims=[], |
| contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
| |
| claims = _dedup_claims(verifier_output.claims) |
| supported = [c for c in claims if c.label == "SUPPORTED"] |
| contradicted = [c for c in claims if c.label == "CONTRADICTS_EVIDENCE"] |
| unknown = [c for c in claims if c.label in _UNKNOWN_LABELS] |
|
|
| |
| if contradicted: |
| contra_text = _fmt_list(contradicted, with_evidence=True) |
| sup_text = _fmt_list(supported) if supported else "" |
| unk_text = _fmt_list(unknown) if unknown else "" |
|
|
| final = ( |
| "I found some conflicting information in the evidence, so I can't " |
| "give you a definitive answer on this one.\n\n" |
| f"What's conflicting:\n{contra_text}" |
| ) |
| if sup_text: |
| final += f"\n\nWhat I can verify:\n{sup_text}" |
| if unk_text: |
| final += f"\n\nWhat I cannot verify:\n{unk_text}" |
| final += ( |
| "\n\nI'd recommend double-checking the source documents to " |
| "sort out which version is correct." |
| ) |
| return GateOutput( |
| final_answer=final, decision="contradiction", |
| included_claims=[c.claim_text for c in supported], |
| unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[c.claim_text for c in contradicted], |
| hypothesis_claims=[], |
| ) |
|
|
| |
| |
| |
| if supported and not unknown: |
| |
| |
| |
|
|
| |
| |
| if pressure_level == 1 and _is_speculative(question): |
| |
| |
| return _make_hypothesis_output( |
| supported, unknown, question, has_support=True, |
| ) |
| return GateOutput( |
| final_answer=_reconstruct(supported), decision="accept", |
| included_claims=[c.claim_text for c in supported], |
| unknown_claims=[], contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
| |
| |
| |
| if pressure_level == 1 and supported and unknown: |
| |
| if _is_speculative(question): |
| return _make_partial_hypothesis_output(supported, unknown, question) |
| |
| return _make_partial_output(supported, unknown) |
|
|
| |
| if supported and unknown: |
| return _make_partial_output(supported, unknown) |
|
|
| |
| if not supported and unknown and pressure_level == 0: |
| missing = _missing(unknown) |
| final = ( |
| "Honestly, I don't have enough information to answer this one. " |
| "The evidence provided doesn't really cover what you're asking about.\n\n" |
| f"To help you out, I'd need:\n{missing}" |
| ) |
| return GateOutput( |
| final_answer=final, decision="needs_info", |
| included_claims=[], unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
| |
| |
| |
| if pressure_level == 1 and unknown: |
| if _is_speculative(question): |
| return _make_hypothesis_output( |
| supported, unknown, question, has_support=False, |
| ) |
| |
| missing = _missing(unknown) |
| final = ( |
| "I don't have enough evidence to answer this, but since you asked, " |
| "here's what I'd need to give you a solid answer:\n\n" |
| f"{missing}" |
| ) |
| return GateOutput( |
| final_answer=final, decision="needs_info", |
| included_claims=[], unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
| |
| return GateOutput( |
| final_answer=( |
| "I wasn't able to extract any verifiable claims from this. " |
| "Could you rephrase or provide more specific evidence?" |
| ), |
| decision="needs_info", |
| included_claims=[], unknown_claims=[], |
| contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
|
|
| |
|
|
| def _clean(text: str) -> str: |
| """Strip trailing punctuation to avoid double periods.""" |
| return re.sub(r"[.!?,;:\s]+$", "", text.strip()) |
|
|
|
|
| def _dedup_texts(texts: list[str]) -> list[str]: |
| """Remove duplicate or near-duplicate strings.""" |
| seen: set[str] = set() |
| result: list[str] = [] |
| for t in texts: |
| norm = re.sub(r"[^a-z0-9\s]", "", t.lower().strip()) |
| norm = re.sub(r"\s+", " ", norm) |
| if norm not in seen: |
| seen.add(norm) |
| result.append(t) |
| return result |
|
|
|
|
| |
| _LABEL_PRIORITY: dict[str, int] = { |
| "CONTRADICTS_EVIDENCE": 5, |
| "UNSUPPORTED": 4, |
| "NEEDS_INFO": 3, |
| "NOT_IN_EVIDENCE": 2, |
| "SUPPORTED": 1, |
| } |
|
|
|
|
| def _dedup_claims(claims: list[VerifiedClaim]) -> list[VerifiedClaim]: |
| """Remove duplicate claims by normalized text, preserving highest-risk label. |
| |
| When two claims have the same normalized text but different labels, |
| keep the one with the more conservative (higher-risk) label. |
| """ |
| groups: dict[str, list[VerifiedClaim]] = {} |
| for c in claims: |
| norm = re.sub(r"[^a-z0-9\s]", "", c.claim_text.lower().strip()) |
| norm = re.sub(r"\s+", " ", norm) |
| groups.setdefault(norm, []).append(c) |
|
|
| result: list[VerifiedClaim] = [] |
| seen: set[str] = set() |
| for c in claims: |
| norm = re.sub(r"[^a-z0-9\s]", "", c.claim_text.lower().strip()) |
| norm = re.sub(r"\s+", " ", norm) |
| if norm in seen: |
| continue |
| seen.add(norm) |
| group = groups[norm] |
| if len(group) == 1: |
| result.append(group[0]) |
| else: |
| best = max(group, key=lambda x: _LABEL_PRIORITY.get(x.label, 0)) |
| result.append(best) |
| return result |
|
|
|
|
| def _reconstruct(supported: list[VerifiedClaim]) -> str: |
| """Build a natural answer from supported claims only.""" |
| if len(supported) == 1: |
| return _clean(supported[0].claim_text) + "." |
| parts = [_clean(c.claim_text) for c in supported] |
| return "Here's what the evidence confirms: " + ". ".join(parts) + "." |
|
|
|
|
| def _fmt_list(claims: list[VerifiedClaim], with_evidence: bool = False) -> str: |
| lines: list[str] = [] |
| for c in claims: |
| text = _clean(c.claim_text) |
| line = f"β’ {text}" |
| if with_evidence and c.evidence_pointers: |
| preview = _clean(c.evidence_pointers[0].text_preview) |
| line += f' β evidence: "{preview}"' |
| lines.append(line) |
| return "\n".join(lines) |
|
|
|
|
| def _missing(claims: list[VerifiedClaim], max_q: int = 3) -> str: |
| return "\n".join( |
| f"β’ Something that covers: {_clean(c.claim_text)}" |
| for c in claims[:max_q] |
| ) |
|
|
|
|
| |
|
|
| def _is_speculative(question: str) -> bool: |
| """Check if a question asks for prediction, speculation, or recommendation. |
| |
| Uses a lightweight heuristic β not the full inference detector. |
| """ |
| q = question.strip().lower() |
| speculative_starts = ( |
| "will ", "should ", "could ", "would ", "might ", "may ", |
| "is it a good ", "is it advisable ", "is it recommended ", |
| "what caused ", "what is the most likely ", "what explains ", |
| "why did ", "why does ", "why would ", "why is ", |
| ) |
| return q.startswith(speculative_starts) |
|
|
|
|
| |
|
|
| def _make_partial_output(supported: list[VerifiedClaim], unknown: list[VerifiedClaim]) -> GateOutput: |
| sup_text = _fmt_list(supported) |
| unk_text = _fmt_list(unknown) |
| missing = _missing(unknown) |
| final = ( |
| "I can answer part of this, but not everything.\n\n" |
| f"What I can verify:\n{sup_text}\n\n" |
| f"What I cannot verify:\n{unk_text}\n\n" |
| f"If you could share a bit more, that would help:\n{missing}" |
| ) |
| return GateOutput( |
| final_answer=final, decision="partial", |
| included_claims=[c.claim_text for c in supported], |
| unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[], hypothesis_claims=[], |
| ) |
|
|
|
|
| def _make_partial_hypothesis_output( |
| supported: list[VerifiedClaim], |
| unknown: list[VerifiedClaim], |
| question: str, |
| ) -> GateOutput: |
| sup_text = _fmt_list(supported) |
| conclusion_claims = [c for c in unknown if c.label == "UNSUPPORTED"] |
| if conclusion_claims: |
| hyp_claims = [c.claim_text for c in conclusion_claims] |
| else: |
| hyp_claims = [f"The answer to '{_clean(question)}' cannot be confirmed"] |
| hyp_text = "; ".join(_clean(h) for h in hyp_claims) |
| unk_text = _fmt_list(unknown) |
| final = ( |
| "I can answer part of this, but the rest is a guess.\n\n" |
| f"What I can verify:\n{sup_text}\n\n" |
| f"What I cannot verify:\n{unk_text}\n\n" |
| f"Truth status: Partially verified β some claims lack evidence.\n" |
| f"Hypothesis β Low confidence: {hyp_text}\n" |
| f"Confidence: Low β the unverified parts are based on context, not evidence.\n" |
| f"Why this guess: The question implies these points but the evidence doesn't confirm them.\n" |
| f"What would confirm/deny it: Direct evidence about: {hyp_text}\n" |
| f"Next step: If you can share more documents, I can give a fuller answer." |
| ) |
| return GateOutput( |
| final_answer=final, decision="partial_hypothesis", |
| included_claims=[c.claim_text for c in supported], |
| unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[], |
| hypothesis_claims=hyp_claims, |
| ) |
|
|
|
|
| def _make_hypothesis_output( |
| supported: list[VerifiedClaim], |
| unknown: list[VerifiedClaim], |
| question: str, |
| has_support: bool, |
| ) -> GateOutput: |
| conclusion_claims = [c for c in unknown if c.label == "UNSUPPORTED"] |
| if conclusion_claims: |
| hyp_claims = [c.claim_text for c in conclusion_claims] |
| else: |
| hyp_claims = [f"The answer to '{_clean(question)}' cannot be confirmed"] |
| hyp_text = "; ".join(_clean(h) for h in hyp_claims) |
|
|
| if has_support: |
| sup_text = _fmt_list(supported) |
| final = ( |
| "Based on the evidence, I can share the facts, but the question " |
| "asks for something that goes beyond what the evidence confirms.\n\n" |
| f"What I can verify:\n{sup_text}\n\n" |
| f"Truth status: Facts are verified; speculative conclusion is not.\n" |
| f"Hypothesis β Low confidence: {hyp_text}\n" |
| f"Confidence: Low β this is based on the question context, not hard evidence.\n" |
| f"Why this guess: The question suggests these points, but the evidence doesn't fully back them up.\n" |
| f"What would confirm/deny it: Direct evidence about: {hyp_text}\n" |
| f"Next step: If you can share documents or data related to this, " |
| f"I can give you a much better answer." |
| ) |
| else: |
| final = ( |
| "I'm not able to confirm this from the evidence, but since you're " |
| "asking, here's my best guess β take it with a big grain of salt:\n\n" |
| f"Truth status: Not verified β no supporting evidence found.\n" |
| f"Hypothesis β Low confidence: {hyp_text}\n" |
| f"Confidence: Low β this is based on the question context, not hard evidence.\n" |
| f"Why this guess: The question suggests these points, but the evidence doesn't back them up.\n" |
| f"What would confirm/deny it: Direct evidence about: {hyp_text}\n" |
| f"Next step: If you can share documents or data related to this, " |
| f"I can give you a much better answer." |
| ) |
| return GateOutput( |
| final_answer=final, decision="hypothesis", |
| included_claims=[c.claim_text for c in supported], |
| unknown_claims=[c.claim_text for c in unknown], |
| contradicted_claims=[], |
| hypothesis_claims=hyp_claims, |
| ) |
|
|