"""Module A: Knowledge Novelty — measures how much of the document Claude already knows.""" from __future__ import annotations import json import anthropic from kvl.ingestor import Document _CLAIM_PROMPT = """You are analyzing a document to evaluate how novel its knowledge is to AI language models. Extract exactly {n} specific, verifiable factual claims from the document below. Focus on: - Precise facts, findings, statistics, or assertions - Claims that are specific (not general background knowledge) - Claims that could be tested with a yes/no or short answer question Return ONLY a JSON array of objects with keys "claim" and "question", where "question" is the specific question whose answer is the claim. Example format: [ {{"claim": "The study found a 34% reduction in soil erosion.", "question": "What reduction in soil erosion did the study find?"}}, ... ] Document: {document}""" _JUDGE_PROMPT = """You are evaluating whether an AI model's answer demonstrates prior knowledge of a specific claim. Claim from document: {claim} AI model's closed-book answer: {answer} Rate how well the model's answer already captures the claim WITHOUT having seen the document. Score 0-1 where: - 1.0 = model's answer is accurate and complete — this is not novel knowledge - 0.5 = model has partial or approximate knowledge - 0.0 = model doesn't know this — this IS novel knowledge Return ONLY a JSON object: {{"score": , "reason": ""}}""" def _call_claude(client: anthropic.Anthropic, system: str, user: str, model: str = "claude-sonnet-4-6") -> str: msg = client.messages.create( model=model, max_tokens=2048, messages=[{"role": "user", "content": user}], system=system, ) return msg.content[0].text.strip() def _extract_claims(client: anthropic.Anthropic, doc: Document, n: int = 12) -> list[dict]: # Use the full document text but cap at ~6000 words to stay within limits text = " ".join(doc.raw.split()[:6000]) prompt = _CLAIM_PROMPT.format(n=n, document=text) raw = _call_claude(client, "You extract factual claims from documents.", prompt) # Strip markdown code fences if present raw = raw.strip() if raw.startswith("```"): raw = "\n".join(raw.split("\n")[1:]) raw = raw.rsplit("```", 1)[0] try: claims = json.loads(raw) return claims[:n] except json.JSONDecodeError: return [] def _closed_book_answer(client: anthropic.Anthropic, question: str) -> str: return _call_claude( client, "Answer the question using only your pre-trained knowledge. Do not make up information. If unsure, say so.", question, model="claude-haiku-4-5-20251001", # cheaper for bulk closed-book queries ) def _judge_novelty(client: anthropic.Anthropic, claim: str, answer: str) -> dict: raw = _call_claude( client, "You are an expert evaluator assessing AI knowledge coverage.", _JUDGE_PROMPT.format(claim=claim, answer=answer), ) raw = raw.strip() if raw.startswith("```"): raw = "\n".join(raw.split("\n")[1:]) raw = raw.rsplit("```", 1)[0] try: return json.loads(raw) except json.JSONDecodeError: return {"score": 0.5, "reason": "Could not parse judge response."} def _evaluate_claim(args): client, item = args answer = _closed_book_answer(client, item["question"]) judgment = _judge_novelty(client, item["claim"], answer) return { "claim": item["claim"], "question": item["question"], "model_answer": answer, "known_score": judgment["score"], "reason": judgment.get("reason", ""), } def evaluate(client: anthropic.Anthropic, doc: Document, progress_cb=None, max_workers: int = 6) -> dict: """Return novelty score (0-100) and detailed results.""" from concurrent.futures import ThreadPoolExecutor if progress_cb: progress_cb("Extracting factual claims from document...") claims = _extract_claims(client, doc) if not claims: return {"score": 50, "details": [], "summary": "Could not extract claims from document."} if progress_cb: progress_cb(f"Testing {len(claims)} claims in parallel...") with ThreadPoolExecutor(max_workers=max_workers) as pool: results = list(pool.map(_evaluate_claim, [(client, item) for item in claims])) avg_known = sum(r["known_score"] for r in results) / len(results) novelty_score = round((1 - avg_known) * 100) return { "score": novelty_score, "details": results, "summary": f"Tested {len(results)} claims. Model already knows ~{round(avg_known*100)}% of this knowledge.", }