"""Data models for evidence verification results and rejected candidates. Provides structured types for offset verification, entity-evidence association, numeric consistency checks, rejected-candidate storage, and verification reports. """ from __future__ import annotations from datetime import datetime, timezone from enum import Enum from pydantic import BaseModel, Field class RejectionReason(str, Enum): """Structured reason codes for candidate rejection during verification.""" INVALID_OFFSET = "invalid_offset" TEXT_MISMATCH = "text_mismatch" ENTITY_NOT_IN_EVIDENCE = "entity_not_in_evidence" NUMERIC_INCONSISTENCY = "numeric_inconsistency" UNSUPPORTED_CLAIM = "unsupported_claim" SCHEMA_VIOLATION = "schema_violation" CONFIDENCE_BELOW_THRESHOLD = "confidence_below_threshold" class OffsetVerification(BaseModel): """Result of verifying a single evidence span's offsets against source text.""" span_id: str = Field(description="ID of the evidence span being verified.") valid: bool = Field(description="Whether the span text matches source at offsets.") reason: str | None = Field( default=None, description="Explanation when invalid (e.g., 'text mismatch at offset 42').", ) class AssociationVerification(BaseModel): """Result of verifying that an entity appears in at least one linked evidence span.""" entity_id: str = Field(description="ID of the entity being verified.") valid: bool = Field(description="Whether entity text was found in any linked span.") reason: str | None = Field( default=None, description="Explanation when invalid (e.g., 'entity text not in any linked span').", ) class NumericVerification(BaseModel): """Result of verifying a numeric fact against its evidence spans.""" fact_id: str = Field(description="ID of the numeric fact being verified.") valid: bool = Field(description="Whether the numeric value was found in evidence text.") found_value: str | None = Field( default=None, description="The value found in the evidence text, if any.", ) expected_value: str = Field(description="The expected normalized value from the fact.") reason: str | None = Field( default=None, description="Explanation when invalid (e.g., 'value 3.14 not found in evidence').", ) class RejectedCandidate(BaseModel): """A candidate that was rejected during verification, stored for audit and learning.""" candidate_type: str = Field( description="Type of candidate: entity, fact, event, relation, sentiment." ) candidate_data: dict = Field( description="Serialized candidate data for audit trail." ) rejection_reason: RejectionReason = Field( description="Structured reason code for rejection." ) stage: str = Field( description="Pipeline stage where rejection occurred (e.g., 'offset_verification')." ) timestamp: datetime = Field( default_factory=lambda: datetime.now(tz=timezone.utc), description="When the rejection was recorded.", ) class VerificationReport(BaseModel): """Summary report from a full verification pass over extraction candidates.""" total_candidates: int = Field(ge=0, description="Total candidates evaluated.") verified: int = Field(ge=0, description="Candidates that passed verification.") rejected: int = Field(ge=0, description="Candidates that failed verification.") coverage_rate: float = Field( ge=0.0, le=1.0, description="Proportion of candidates with valid evidence support.", ) rejection_breakdown: dict[str, int] = Field( default_factory=dict, description="Count of rejections per RejectionReason code.", )