LLMs generate fluent text even when making up facts. A response might say “late fee is 5%” when the document says “1.5%” - both sound plausible, but only one is correct.From README.md:29-30:
“The problem: LLMs generate fluent text even when making up facts. We need to validate each claim against source documents.”
The LLMJudge validates every factual claim in the generated response against retrieved document sections.
# Instead of asking "is this response correct?", decompose the problem:1. Extract each factual claim from the response2. Find evidence in documents3. Detect contradictions4. Calculate confidence
# Pattern: Any sentence containing numbers or percentagesnumber_patterns = re.findall(r'([^.]*\d+(?:\.\d+)?%?[^.]*\.)', response)for match in number_patterns: claims.append({"text": match.strip(), "type": "quantitative"})
Location:components.py:152-155Examples:
“Client shall pay a late fee of 1.5% per month.”
“Payment is due within 30 days.”
From README.md:38:
“Quantitative claims (numbers, percentages) are easy to verify and dangerous if wrong. ‘1.5% late fee’ is verifiable; ‘5% late fee’ is a detectable hallucination.”
# Pattern: Sentences with time-related keywordstime_patterns = re.findall( r'([^.]*(?:within|after|before|\d+\s*days?|\d+\s*months?|\d+\s*years?)[^.]*\.)', response, re.IGNORECASE)for match in time_patterns: if match.strip() not in [c["text"] for c in claims]: claims.append({"text": match.strip(), "type": "temporal"})
Location:components.py:158-161Examples:
“Either party may terminate upon 30 days’ written notice.”
“Confidentiality obligations survive for 3 years.”
# Pattern: Sentences with obligation keywordsobligation_patterns = re.findall( r'([^.]*(?:shall|must|will|is required)[^.]*\.)', response, re.IGNORECASE)for match in obligation_patterns: if match.strip() not in [c["text"] for c in claims]: claims.append({"text": match.strip(), "type": "obligation"})
Location:components.py:164-167From README.md:38:
“Obligations (shall/must) indicate contract terms that should exist in the document.”
Examples:
“ABC Corporation shall indemnify Client against third-party claims.”
“Client must maintain confidentiality of proprietary information.”
# If no structured claims found, split by sentencesif not claims: sentences = re.split(r'(?<=[.!?])\s+', response) for sent in sentences: if len(sent) > 20: # Skip very short sentences claims.append({"text": sent.strip(), "type": "general"})
For quantitative claims, exact number matches are required:
numbers_in_claim = re.findall(r'\d+(?:\.\d+)?%?', claim)for section in context: content = section.get("content", "") numbers_in_content = re.findall(r'\d+(?:\.\d+)?%?', content) for num in numbers_in_claim: if num in numbers_in_content: # Find the sentence containing this number sentences = re.split(r'(?<=[.!?])\s+', content) for sent in sentences: if num in sent: return sent.strip() # Supporting evidence found
Location:components.py:188-195
Strict number matching prevents subtle hallucinations like “1.5%” becoming “5%” or “30 days” becoming “60 days”.
“Contradiction detection compares numbers in claims against numbers in context. If the response says ‘late fee is 5%’ but the document says ‘late fee of 1.5%’, that’s a contradiction.”
claim_percentages = re.findall(r'(\d+(?:\.\d+)?)\s*%', claim)for section in context: content = section.get("content", "") content_percentages = re.findall(r'(\d+(?:\.\d+)?)\s*%', content) if content_percentages: # Check if discussing the same topic if ("late" in claim_lower or "fee" in claim_lower) and \ ("late" in content_lower or "fee" in content_lower): for claim_pct in claim_percentages: if claim_pct not in content_percentages: return True # CONTRADICTION!
Location:components.py:221-228From README.md:41:
“I check that both discuss the same topic (late/fee keywords) before flagging.”
claim_days = re.findall(r'(\d+)\s*days?', claim_lower)for section in context: content = section.get("content", "") content_days_list = re.findall(r'(\d+)\s*\)?\s*days?', content_lower) if content_days_list: payment_keywords = ["payment", "pay", "due", "within", "invoice", "receipt"] claim_has_payment = any(kw in claim_lower for kw in payment_keywords) content_has_payment = any(kw in content_lower for kw in payment_keywords) if claim_has_payment and content_has_payment: for claim_d in claim_days: if claim_d not in content_days_list: return True # CONTRADICTION!
Location:components.py:231-240
Context-aware contradiction detection prevents false positives. “30 days” in payment terms vs “3 years” in confidentiality is NOT a contradiction because they discuss different topics.
“Confidence scoring: contradictions get heavy penalty (0.8 per claim) because stating something wrong is serious. Unsupported claims get moderate penalty (0.3) because they might be valid inferences.”
Section: Late Payment Penalties (page 8)"If payment is not received within thirty (30) days, Client shall be assesseda late fee of 1.5% per month (18% annually) on the outstanding balance."
# nodes.py:47-55def should_retry(state: DocMindState) -> str: verdict = state.get("judge_verdict", {}) retry_count = state.get("retry_count", 0) # Retry if hallucinated and haven't exceeded max retries (2 attempts max) if verdict.get("is_hallucinated", False) and retry_count < 2: log_retry_attempt(retry_count + 1, 2) return "retry" return "output"
Location:nodes.py:47-55From README.md:47-48:
“If the judge detects hallucination, the system retries retrieval. Maximum 2 retries to avoid infinite loops. If retrieval fails twice, the information probably doesn’t exist in the documents.”
from components import LLMJudgejudge = LLMJudge()# Test claim extractionresponse = "The late fee is 1.5% per month. Payment is due within 30 days."claims = judge._extract_claims(response)assert len(claims) == 2assert claims[0]["type"] == "quantitative"assert claims[1]["type"] == "temporal"# Test contradiction detectionclaim = "Late fee is 5% per month"context = [{"content": "late fee of 1.5% per month"}]assert judge._check_contradiction(claim, context) == True# Test evaluationverdict = await judge.evaluate(response, context)assert verdict["is_hallucinated"] == Trueassert verdict["confidence_score"] < 0.5