Chains
Legacy API
The chains module is being replaced by the pluggable adapter architecture. New code should use the adapter classes in extractors/, gatherers/, verifiers/, and formatters/ packages instead. See the Adapters API Reference for the modern interface.
from truthfulness_evaluator.llm.chains.extraction import SimpleClaimExtractionChain
extractor = SimpleClaimExtractionChain(model="gpt-4o-mini")
claims = await extractor.extract(
document="Python was created in 1991...",
source_path="README.md",
max_claims=10
)
Output: list[Claim]
Uses structured output (ClaimExtractionOutput):
class ClaimExtractionOutput(BaseModel):
claims: list[ExtractedClaim]
class ExtractedClaim(BaseModel):
text: str
claim_type: str # "explicit", "implicit", "inferred"
from truthfulness_evaluator.llm.chains.extraction import TripletExtractionChain
extractor = TripletExtractionChain(model="gpt-4o-mini")
claims = await extractor.extract(document, source_path)
Extracts subject-relation-object triplets.
Verification
VerificationChain
from truthfulness_evaluator.llm.chains.verification import VerificationChain
verifier = VerificationChain(model_name="gpt-4o")
result = await verifier.verify(claim, evidence)
Output: VerificationResult
Uses structured output (VerificationOutput):
class VerificationOutput(BaseModel):
verdict: str # "SUPPORTS", "REFUTES", "NOT_ENOUGH_INFO"
confidence: float # 0.0 to 1.0
reasoning: str
key_evidence: str | None
Consensus
ConsensusChain
from truthfulness_evaluator.llm.chains.consensus import ConsensusChain
consensus = ConsensusChain(
model_names=["gpt-4o", "gpt-4o-mini"],
weights={"gpt-4o": 0.6, "gpt-4o-mini": 0.4},
confidence_threshold=0.7
)
result = await consensus.verify(claim, evidence)
Each model votes; votes are tallied with the given weights. The leading verdict is
committed only if it isn't tied for the lead and its weighted agreement fraction meets
confidence_threshold — otherwise the result is NOT_ENOUGH_INFO. Reported confidence is
that agreement fraction, not an average of the models' self-reported scores.
Evidence Processing
EvidenceProcessor
from truthfulness_evaluator.llm.chains.evidence import EvidenceProcessor
processor = EvidenceProcessor(model="gpt-4o-mini")
# Analyze evidence relevance
evidence, summary = await processor.analyze_evidence(claim, evidence_list)
# Synthesize multiple pieces
synthesis = await processor.synthesize_evidence(claim, evidence_list)
Output: Analyzed evidence with scores + summary text
Uses structured output (EvidenceAnalysisOutput):
class EvidenceAnalysisOutput(BaseModel):
evidence_analysis: list[EvidenceAnalysisItem]
summary: str
class EvidenceAnalysisItem(BaseModel):
index: int
relevance: float
supports: bool | None
credibility: float
reasoning: str
Custom Chains
Build your own:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Define output structure
class MyOutput(BaseModel):
result: str
confidence: float
# Create chain
prompt = ChatPromptTemplate.from_template("Verify: {claim}")
llm = ChatOpenAI(model="gpt-4o").with_structured_output(MyOutput)
chain = prompt | llm
# Use
result = await chain.ainvoke({"claim": "Python was created in 1991"})
API Reference
Fallback claim extraction using structured LLM outputs.
Source code in src/truthfulness_evaluator/llm/chains/extraction.py
| class SimpleClaimExtractionChain:
"""Fallback claim extraction using structured LLM outputs."""
def __init__(self, model: str = "gpt-4o-mini"):
self.model = model
self._llm = None
@property
def llm(self):
"""Lazy initialization of LLM with structured output."""
if self._llm is None:
base_llm = create_chat_model(self.model, temperature=0)
# Use structured output
self._llm = base_llm.with_structured_output(ClaimExtractionOutput)
return self._llm
async def extract(
self, document: str, source_path: str, max_claims: Optional[int] = None
) -> list[Claim]:
"""Extract claims using structured LLM output."""
# Strip example blocks before extraction to avoid false positives
# from illustrative content in fenced code blocks and inline code.
cleaned_document = strip_example_blocks(document)
chain = CLAIM_EXTRACTION_PROMPT | self.llm
try:
result: ClaimExtractionOutput = await chain.ainvoke({"text": cleaned_document})
claims = []
for i, extracted in enumerate(result.claims):
if max_claims and i >= max_claims:
break
if extracted.text.strip():
claims.append(
Claim(
id=f"claim_{i:03d}",
text=extracted.text.strip(),
source_document=source_path,
claim_type=(
extracted.claim_type
if extracted.claim_type in ["explicit", "implicit", "inferred"]
else "explicit"
),
)
)
return claims
except Exception as e:
logger.warning(f"Simple extraction failed: {e}")
return []
|
Lazy initialization of LLM with structured output.
Extract claims using structured LLM output.
Source code in src/truthfulness_evaluator/llm/chains/extraction.py
| async def extract(
self, document: str, source_path: str, max_claims: Optional[int] = None
) -> list[Claim]:
"""Extract claims using structured LLM output."""
# Strip example blocks before extraction to avoid false positives
# from illustrative content in fenced code blocks and inline code.
cleaned_document = strip_example_blocks(document)
chain = CLAIM_EXTRACTION_PROMPT | self.llm
try:
result: ClaimExtractionOutput = await chain.ainvoke({"text": cleaned_document})
claims = []
for i, extracted in enumerate(result.claims):
if max_claims and i >= max_claims:
break
if extracted.text.strip():
claims.append(
Claim(
id=f"claim_{i:03d}",
text=extracted.text.strip(),
source_document=source_path,
claim_type=(
extracted.claim_type
if extracted.claim_type in ["explicit", "implicit", "inferred"]
else "explicit"
),
)
)
return claims
except Exception as e:
logger.warning(f"Simple extraction failed: {e}")
return []
|
Extract claims as subject-relation-object triplets using structured output.
Source code in src/truthfulness_evaluator/llm/chains/extraction.py
| class TripletExtractionChain:
"""Extract claims as subject-relation-object triplets using structured output."""
def __init__(self, model: str = "gpt-4o-mini"):
self.model = model
self._llm = None
@property
def llm(self):
"""Lazy initialization of LLM with structured output."""
if self._llm is None:
base_llm = create_chat_model(self.model, temperature=0)
self._llm = base_llm.with_structured_output(TripletExtractionOutput)
return self._llm
async def extract(
self, document: str, source_path: str, max_claims: Optional[int] = None
) -> list[Claim]:
"""Extract claims as triplets using structured output."""
# Strip example blocks before extraction to avoid false positives
# from illustrative content in fenced code blocks and inline code.
cleaned_document = strip_example_blocks(document)
chain = TRIPLET_EXTRACTION_PROMPT | self.llm
try:
result: TripletExtractionOutput = await chain.ainvoke({"text": cleaned_document})
claims = []
for i, triplet in enumerate(result.triplets):
if max_claims and i >= max_claims:
break
claim_text = f"{triplet.subject} {triplet.relation} {triplet.object}".strip()
if claim_text:
claims.append(
Claim(
id=f"claim_{i:03d}",
text=claim_text,
source_document=source_path,
context=triplet.context,
claim_type="explicit",
)
)
return claims
except Exception as e:
logger.warning(f"Triplet extraction failed: {e}")
return []
|
Lazy initialization of LLM with structured output.
Extract claims as triplets using structured output.
Source code in src/truthfulness_evaluator/llm/chains/extraction.py
| async def extract(
self, document: str, source_path: str, max_claims: Optional[int] = None
) -> list[Claim]:
"""Extract claims as triplets using structured output."""
# Strip example blocks before extraction to avoid false positives
# from illustrative content in fenced code blocks and inline code.
cleaned_document = strip_example_blocks(document)
chain = TRIPLET_EXTRACTION_PROMPT | self.llm
try:
result: TripletExtractionOutput = await chain.ainvoke({"text": cleaned_document})
claims = []
for i, triplet in enumerate(result.triplets):
if max_claims and i >= max_claims:
break
claim_text = f"{triplet.subject} {triplet.relation} {triplet.object}".strip()
if claim_text:
claims.append(
Claim(
id=f"claim_{i:03d}",
text=claim_text,
source_document=source_path,
context=triplet.context,
claim_type="explicit",
)
)
return claims
except Exception as e:
logger.warning(f"Triplet extraction failed: {e}")
return []
|
Verification
truthfulness_evaluator.llm.chains.verification.VerificationChain
Single-model verification chain with structured outputs.
Source code in src/truthfulness_evaluator/llm/chains/verification.py
| class VerificationChain:
"""Single-model verification chain with structured outputs."""
def __init__(self, model_name: str = "gpt-4o"):
self.model_name = model_name
self._llm = None
@property
def llm(self):
"""Lazy initialization of LLM with structured output."""
if self._llm is None:
base_llm = create_chat_model(self.model_name, temperature=0)
# Use structured output
self._llm = base_llm.with_structured_output(VerificationOutput)
return self._llm
async def verify(self, claim: Claim, evidence: list[Evidence]) -> VerificationResult:
"""Verify a claim against evidence using structured output."""
# Build evidence text with relevance indicators
if evidence:
evidence_parts = []
for i, e in enumerate(evidence[:4], 1): # Top 4 evidence items
support_indicator = ""
if e.supports_claim is True:
support_indicator = "[SUPPORTS]"
elif e.supports_claim is False:
support_indicator = "[REFUTES]"
else:
support_indicator = "[NEUTRAL]"
evidence_parts.append(
f"\n--- Evidence {i} ({e.source_type}) {support_indicator} ---\n"
f"Source: {e.source}\n"
f"Relevance: {e.relevance_score:.0%}\n"
f"Content: {e.content[:600]}"
)
evidence_text = "\n".join(evidence_parts)
else:
evidence_text = "No evidence provided."
chain = VERIFICATION_PROMPT | self.llm
try:
result: VerificationOutput = await chain.ainvoke(
{"claim": claim.text, "evidence": evidence_text}
)
# Normalize verdict
verdict = result.verdict.upper()
if verdict not in ["SUPPORTS", "REFUTES", "NOT_ENOUGH_INFO"]:
verdict = "NOT_ENOUGH_INFO"
# Combine reasoning with key evidence
full_explanation = result.reasoning
if result.key_evidence:
full_explanation += f"\n\nKey evidence: {result.key_evidence}"
# Adjust confidence based on evidence quality
confidence = max(0.0, min(1.0, result.confidence))
if not evidence:
confidence = min(confidence, 0.3)
return VerificationResult(
claim_id=claim.id,
verdict=verdict,
confidence=confidence,
evidence=evidence,
explanation=full_explanation,
model_votes={self.model_name: verdict},
)
except Exception as e:
# Fallback result on error
return VerificationResult(
claim_id=claim.id,
verdict="NOT_ENOUGH_INFO",
confidence=0.0,
evidence=evidence,
explanation=f"Verification failed: {str(e)}",
model_votes={self.model_name: "NOT_ENOUGH_INFO"},
)
|
llm
property
Lazy initialization of LLM with structured output.
verify(claim, evidence)
async
Verify a claim against evidence using structured output.
Source code in src/truthfulness_evaluator/llm/chains/verification.py
| async def verify(self, claim: Claim, evidence: list[Evidence]) -> VerificationResult:
"""Verify a claim against evidence using structured output."""
# Build evidence text with relevance indicators
if evidence:
evidence_parts = []
for i, e in enumerate(evidence[:4], 1): # Top 4 evidence items
support_indicator = ""
if e.supports_claim is True:
support_indicator = "[SUPPORTS]"
elif e.supports_claim is False:
support_indicator = "[REFUTES]"
else:
support_indicator = "[NEUTRAL]"
evidence_parts.append(
f"\n--- Evidence {i} ({e.source_type}) {support_indicator} ---\n"
f"Source: {e.source}\n"
f"Relevance: {e.relevance_score:.0%}\n"
f"Content: {e.content[:600]}"
)
evidence_text = "\n".join(evidence_parts)
else:
evidence_text = "No evidence provided."
chain = VERIFICATION_PROMPT | self.llm
try:
result: VerificationOutput = await chain.ainvoke(
{"claim": claim.text, "evidence": evidence_text}
)
# Normalize verdict
verdict = result.verdict.upper()
if verdict not in ["SUPPORTS", "REFUTES", "NOT_ENOUGH_INFO"]:
verdict = "NOT_ENOUGH_INFO"
# Combine reasoning with key evidence
full_explanation = result.reasoning
if result.key_evidence:
full_explanation += f"\n\nKey evidence: {result.key_evidence}"
# Adjust confidence based on evidence quality
confidence = max(0.0, min(1.0, result.confidence))
if not evidence:
confidence = min(confidence, 0.3)
return VerificationResult(
claim_id=claim.id,
verdict=verdict,
confidence=confidence,
evidence=evidence,
explanation=full_explanation,
model_votes={self.model_name: verdict},
)
except Exception as e:
# Fallback result on error
return VerificationResult(
claim_id=claim.id,
verdict="NOT_ENOUGH_INFO",
confidence=0.0,
evidence=evidence,
explanation=f"Verification failed: {str(e)}",
model_votes={self.model_name: "NOT_ENOUGH_INFO"},
)
|
Consensus
truthfulness_evaluator.llm.chains.consensus.ConsensusChain
Multi-model consensus via weighted, agreement-based voting.
Source code in src/truthfulness_evaluator/llm/chains/consensus.py
| class ConsensusChain:
"""Multi-model consensus via weighted, agreement-based voting."""
def __init__(
self,
model_names: list[str],
weights: dict[str, float] | None = None,
confidence_threshold: float = 0.7,
):
self.model_names = model_names
self.weights = weights or {m: 1.0 / len(model_names) for m in model_names}
# Minimum weighted agreement for the leading verdict to be committed;
# below it (or on a tie) the ensemble abstains. With N equal-weight
# models a value of 0.7 effectively requires near-unanimity (e.g. 3
# models must all agree, since 2/3 = 0.67 < 0.7); lower it to accept
# simple majorities.
self.confidence_threshold = confidence_threshold
self._chains: list[VerificationChain] | None = None
@property
def chains(self) -> list[VerificationChain]:
"""Lazy initialization of the per-model verification chains."""
if self._chains is None:
self._chains = [VerificationChain(m) for m in self.model_names]
return self._chains
async def verify(self, claim: Claim, evidence: list[Evidence]) -> VerificationResult:
"""Verify a claim by polling every model and aggregating by agreement."""
results = await asyncio.gather(*[chain.verify(claim, evidence) for chain in self.chains])
votes = {self.model_names[i]: r.verdict for i, r in enumerate(results)}
self_reported = {self.model_names[i]: r.confidence for i, r in enumerate(results)}
# Weighted tally of verdicts.
tally: Counter = Counter()
for model, verdict in votes.items():
tally[verdict] += self.weights.get(model, 1.0 / len(self.model_names))
total_weight = sum(tally.values()) or 1.0
ranked = tally.most_common()
leading_verdict, leading_weight = ranked[0]
agreement = leading_weight / total_weight
# A shared top weight is unresolved agreement -> abstain.
tie = len(ranked) > 1 and ranked[1][1] == leading_weight
abstained = tie or agreement < self.confidence_threshold
final_verdict = ABSTAIN if abstained else leading_verdict
# Confidence reflects consensus strength, not the models' unreliable
# self-reported scores.
confidence = round(agreement, 4)
all_evidence: list[Evidence] = []
for r in results:
all_evidence.extend(r.evidence)
vote_lines = [
f"{m}: {votes[m]} (self-reported {self_reported[m]:.0%})" for m in self.model_names
]
explanation_parts = [
f"Consensus: {final_verdict} ({agreement:.0%} weighted agreement)",
"Model votes:",
*vote_lines,
]
if final_verdict == ABSTAIN and leading_verdict != ABSTAIN:
reason = "tie" if tie else f"agreement below {self.confidence_threshold:.0%} threshold"
explanation_parts.append(f"(Abstained: {reason}.)")
logger.debug(
"Consensus %s at %.0f%% agreement; votes=%s", final_verdict, agreement * 100, votes
)
return VerificationResult(
claim_id=claim.id,
verdict=final_verdict,
confidence=confidence,
evidence=all_evidence[:5],
explanation="\n".join(explanation_parts),
model_votes=votes,
)
|
chains: list[VerificationChain]
property
Lazy initialization of the per-model verification chains.
verify(claim, evidence)
async
Verify a claim by polling every model and aggregating by agreement.
Source code in src/truthfulness_evaluator/llm/chains/consensus.py
| async def verify(self, claim: Claim, evidence: list[Evidence]) -> VerificationResult:
"""Verify a claim by polling every model and aggregating by agreement."""
results = await asyncio.gather(*[chain.verify(claim, evidence) for chain in self.chains])
votes = {self.model_names[i]: r.verdict for i, r in enumerate(results)}
self_reported = {self.model_names[i]: r.confidence for i, r in enumerate(results)}
# Weighted tally of verdicts.
tally: Counter = Counter()
for model, verdict in votes.items():
tally[verdict] += self.weights.get(model, 1.0 / len(self.model_names))
total_weight = sum(tally.values()) or 1.0
ranked = tally.most_common()
leading_verdict, leading_weight = ranked[0]
agreement = leading_weight / total_weight
# A shared top weight is unresolved agreement -> abstain.
tie = len(ranked) > 1 and ranked[1][1] == leading_weight
abstained = tie or agreement < self.confidence_threshold
final_verdict = ABSTAIN if abstained else leading_verdict
# Confidence reflects consensus strength, not the models' unreliable
# self-reported scores.
confidence = round(agreement, 4)
all_evidence: list[Evidence] = []
for r in results:
all_evidence.extend(r.evidence)
vote_lines = [
f"{m}: {votes[m]} (self-reported {self_reported[m]:.0%})" for m in self.model_names
]
explanation_parts = [
f"Consensus: {final_verdict} ({agreement:.0%} weighted agreement)",
"Model votes:",
*vote_lines,
]
if final_verdict == ABSTAIN and leading_verdict != ABSTAIN:
reason = "tie" if tie else f"agreement below {self.confidence_threshold:.0%} threshold"
explanation_parts.append(f"(Abstained: {reason}.)")
logger.debug(
"Consensus %s at %.0f%% agreement; votes=%s", final_verdict, agreement * 100, votes
)
return VerificationResult(
claim_id=claim.id,
verdict=final_verdict,
confidence=confidence,
evidence=all_evidence[:5],
explanation="\n".join(explanation_parts),
model_votes=votes,
)
|
Evidence Processing
truthfulness_evaluator.llm.chains.evidence.EvidenceProcessor
Process and analyze evidence for claims using structured outputs.
Source code in src/truthfulness_evaluator/llm/chains/evidence.py
| class EvidenceProcessor:
"""Process and analyze evidence for claims using structured outputs."""
def __init__(self, model: str = "gpt-4o-mini"):
self.model = model
self._llm = None
@property
def llm(self):
"""Lazy initialization of LLM with structured output."""
if self._llm is None:
base_llm = create_chat_model(self.model, temperature=0)
self._llm = base_llm.with_structured_output(EvidenceAnalysisOutput)
return self._llm
async def analyze_evidence(
self, claim: Claim, evidence_list: list[Evidence]
) -> tuple[list[Evidence], str]:
"""
Analyze evidence and determine which pieces are relevant.
Returns:
Tuple of (filtered_evidence, analysis_summary)
"""
if not evidence_list:
return [], "No evidence provided"
# Build evidence text
evidence_text = "\n\n---\n\n".join(
[
f"[{i}] Source: {e.source}\nType: {e.source_type}\nContent: {e.content[:800]}"
for i, e in enumerate(evidence_list[:5]) # Top 5 pieces
]
)
chain = EVIDENCE_ANALYSIS_PROMPT | self.llm
try:
result: EvidenceAnalysisOutput = await chain.ainvoke(
{"claim": claim.text, "evidence": evidence_text}
)
# Update evidence with analysis
for analysis in result.evidence_analysis:
idx = analysis.index
if 0 <= idx < len(evidence_list):
evidence_list[idx].relevance_score = max(0.0, min(1.0, analysis.relevance))
evidence_list[idx].supports_claim = analysis.supports
evidence_list[idx].credibility_score = max(0.0, min(1.0, analysis.credibility))
# Sort by relevance
evidence_list.sort(key=lambda e: e.relevance_score, reverse=True)
return evidence_list, result.summary
except Exception as e:
# If analysis fails, return original evidence
return evidence_list, f"Analysis failed: {str(e)}"
async def synthesize_evidence(self, claim: Claim, evidence_list: list[Evidence]) -> str:
"""
Synthesize multiple pieces of evidence into a coherent summary.
Returns:
Summary text
"""
if not evidence_list:
return "No evidence available"
# Filter to high-relevance evidence
good_evidence = [e for e in evidence_list if e.relevance_score >= 0.5][:3]
if not good_evidence:
return "No highly relevant evidence found"
# Simple synthesis without LLM for speed
parts = []
for e in good_evidence:
support = (
"supports"
if e.supports_claim
else "refutes" if e.supports_claim is False else "is neutral on"
)
parts.append(
f"{e.source} ({e.source_type}) {support} the claim with {e.relevance_score:.0%} relevance"
)
return "; ".join(parts)
|
llm
property
Lazy initialization of LLM with structured output.
analyze_evidence(claim, evidence_list)
async
Analyze evidence and determine which pieces are relevant.
Returns:
| Type |
Description |
tuple[list[Evidence], str]
|
Tuple of (filtered_evidence, analysis_summary)
|
Source code in src/truthfulness_evaluator/llm/chains/evidence.py
| async def analyze_evidence(
self, claim: Claim, evidence_list: list[Evidence]
) -> tuple[list[Evidence], str]:
"""
Analyze evidence and determine which pieces are relevant.
Returns:
Tuple of (filtered_evidence, analysis_summary)
"""
if not evidence_list:
return [], "No evidence provided"
# Build evidence text
evidence_text = "\n\n---\n\n".join(
[
f"[{i}] Source: {e.source}\nType: {e.source_type}\nContent: {e.content[:800]}"
for i, e in enumerate(evidence_list[:5]) # Top 5 pieces
]
)
chain = EVIDENCE_ANALYSIS_PROMPT | self.llm
try:
result: EvidenceAnalysisOutput = await chain.ainvoke(
{"claim": claim.text, "evidence": evidence_text}
)
# Update evidence with analysis
for analysis in result.evidence_analysis:
idx = analysis.index
if 0 <= idx < len(evidence_list):
evidence_list[idx].relevance_score = max(0.0, min(1.0, analysis.relevance))
evidence_list[idx].supports_claim = analysis.supports
evidence_list[idx].credibility_score = max(0.0, min(1.0, analysis.credibility))
# Sort by relevance
evidence_list.sort(key=lambda e: e.relevance_score, reverse=True)
return evidence_list, result.summary
except Exception as e:
# If analysis fails, return original evidence
return evidence_list, f"Analysis failed: {str(e)}"
|
synthesize_evidence(claim, evidence_list)
async
Synthesize multiple pieces of evidence into a coherent summary.
Returns:
Source code in src/truthfulness_evaluator/llm/chains/evidence.py
| async def synthesize_evidence(self, claim: Claim, evidence_list: list[Evidence]) -> str:
"""
Synthesize multiple pieces of evidence into a coherent summary.
Returns:
Summary text
"""
if not evidence_list:
return "No evidence available"
# Filter to high-relevance evidence
good_evidence = [e for e in evidence_list if e.relevance_score >= 0.5][:3]
if not good_evidence:
return "No highly relevant evidence found"
# Simple synthesis without LLM for speed
parts = []
for e in good_evidence:
support = (
"supports"
if e.supports_claim
else "refutes" if e.supports_claim is False else "is neutral on"
)
parts.append(
f"{e.source} ({e.source_type}) {support} the claim with {e.relevance_score:.0%} relevance"
)
return "; ".join(parts)
|