Spaces:
Sleeping
Sleeping
| """Utterance -> structured claims (Section 7). | |
| The extractor (cheap tier) turns a character utterance into structured | |
| propositions. It is given engine-only ground truth so it can also stamp each | |
| claim's ``engine_truth_value`` (true/false/unknown) — this powers confrontation | |
| and the guard. The player never sees these values. | |
| """ | |
| from __future__ import annotations | |
| from ..llm.client import LLMClient | |
| from ..llm.prompts import PromptRegistry | |
| from ..models import Claim | |
| class ClaimExtractor: | |
| def __init__(self, client: LLMClient, prompts: PromptRegistry) -> None: | |
| self.client = client | |
| self.prompts = prompts | |
| def extract( | |
| self, | |
| *, | |
| character: str, | |
| utterance: str, | |
| turn: int, | |
| truth_context: str, | |
| ) -> list[Claim]: | |
| prompt = self.prompts.render( | |
| "extractor/claims.md.j2", | |
| character=character, | |
| utterance=utterance, | |
| truth_context=truth_context, | |
| ) | |
| try: | |
| data, _ = self.client.complete_json( | |
| tier="extractor", task="claim_extract", user=prompt, | |
| ) | |
| except Exception: | |
| return [] | |
| rows = data.get("claims", data) if isinstance(data, dict) else data | |
| if not isinstance(rows, list): | |
| return [] | |
| claims: list[Claim] = [] | |
| for i, row in enumerate(rows): | |
| if not isinstance(row, dict): | |
| continue | |
| polarity = row.get("polarity", "neutral") | |
| if polarity not in ("affirm", "deny", "neutral"): | |
| polarity = "neutral" | |
| tv = row.get("engine_truth_value", row.get("truth_value", "unknown")) | |
| if tv not in ("true", "false", "unknown"): | |
| tv = "unknown" | |
| claims.append( | |
| Claim( | |
| claim_id=f"{character.lower().replace(' ', '_')}_t{turn}_{i}", | |
| topic=str(row.get("topic", "general")).strip().lower(), | |
| proposition=str(row.get("proposition", "")).strip(), | |
| turn=turn, | |
| polarity=polarity, | |
| engine_truth_value=tv, | |
| ) | |
| ) | |
| return [c for c in claims if c.proposition] | |
| def confirmed_testimony( | |
| self, *, question: str, reply: str, candidates: list[dict[str, str]] | |
| ) -> list[str]: | |
| """Of the candidate facts, which does this reply genuinely substantiate? | |
| Uses the cheap extractor tier for robust paraphrase-tolerant matching | |
| (names/times reworded). The engine still owns *whether* a clue is | |
| unlocked; this only judges whether the witness spoke to it. | |
| """ | |
| if not candidates: | |
| return [] | |
| prompt = self.prompts.render( | |
| "extractor/testimony.md.j2", | |
| question=question, reply=reply, candidates=candidates, | |
| ) | |
| try: | |
| data, _ = self.client.complete_json( | |
| tier="extractor", task="testimony_detect", user=prompt, | |
| ) | |
| except Exception: | |
| return [] | |
| ids = data.get("confirmed", []) if isinstance(data, dict) else [] | |
| valid = {c["id"] for c in candidates} | |
| return [cid for cid in ids if cid in valid] | |