"""Adjudication schemas for Intelligence Pipeline v3. Defines Pydantic models for the adjudication layer: - AdjudicationCandidate: a proposed entity/fact/event requiring adjudication - ConflictDescription: describes a conflict between candidates - AdjudicationQuestion: a specific question the adjudicator must resolve - EvidencePacket: evidence spans provided to the adjudicator - AdjudicationDecision: the adjudicator's resolution (excludes confidence, novelty, impact, and horizon — those come from calibrated pipelines) Every decision requires evidence_ids linking back to packet evidence. """ from __future__ import annotations from enum import Enum from typing import Any from pydantic import BaseModel, Field class CandidateType(str, Enum): """Type of candidate being adjudicated.""" ENTITY = "entity" EVENT = "event" FACT = "fact" RELATION = "relation" SENTIMENT = "sentiment" class AdjudicationCandidate(BaseModel): """A proposed extraction candidate that requires adjudication. Represents an entity, event, fact, relation, or sentiment that the fast-path could not resolve with sufficient confidence. """ candidate_id: str = Field(description="Unique identifier for this candidate") candidate_type: CandidateType = Field(description="Type of candidate") label: str = Field(description="Human-readable label or description") source_chunk_ids: list[str] = Field( default_factory=list, description="Chunk IDs where this candidate was found", ) evidence_ids: list[str] = Field( default_factory=list, description="Evidence span IDs supporting this candidate", ) metadata: dict[str, Any] = Field( default_factory=dict, description="Additional type-specific metadata", ) score: float = Field( default=0.0, ge=0.0, le=1.0, description="Specialist extraction score (0.0-1.0)", ) class ConflictType(str, Enum): """Type of conflict between candidates.""" CONTRADICTORY_VALUES = "contradictory_values" AMBIGUOUS_IDENTITY = "ambiguous_identity" OPPOSING_SENTIMENT = "opposing_sentiment" OVERLAPPING_EVENTS = "overlapping_events" CAUSAL_AMBIGUITY = "causal_ambiguity" class ConflictDescription(BaseModel): """Describes a conflict between two or more candidates. Used to inform the adjudicator about what needs resolution. """ conflict_id: str = Field(description="Unique identifier for this conflict") conflict_type: ConflictType = Field(description="Type of conflict") candidate_ids: list[str] = Field( min_length=2, description="IDs of conflicting candidates", ) description: str = Field(description="Human-readable conflict description") evidence_ids: list[str] = Field( default_factory=list, description="Evidence IDs relevant to this conflict", ) class QuestionCode(str, Enum): """Codes representing specific adjudication questions.""" RESOLVE_ENTITY_IDENTITY = "RESOLVE_ENTITY_IDENTITY" RESOLVE_EVENT_TYPE = "RESOLVE_EVENT_TYPE" RESOLVE_CAUSAL_DIRECTION = "RESOLVE_CAUSAL_DIRECTION" RESOLVE_NUMERIC_CONFLICT = "RESOLVE_NUMERIC_CONFLICT" RESOLVE_SENTIMENT_DIRECTION = "RESOLVE_SENTIMENT_DIRECTION" RESOLVE_TEMPORAL_ORDERING = "RESOLVE_TEMPORAL_ORDERING" RESOLVE_COMPANY_ATTRIBUTION = "RESOLVE_COMPANY_ATTRIBUTION" CONFIRM_CROSS_CHUNK_RELATION = "CONFIRM_CROSS_CHUNK_RELATION" class AdjudicationQuestion(BaseModel): """A specific question the adjudicator must answer. Each question references candidates and conflicts that need resolution. """ question_code: QuestionCode = Field(description="Structured question code") description: str = Field(description="Natural language question for the adjudicator") candidate_ids: list[str] = Field( default_factory=list, description="Candidate IDs this question applies to", ) conflict_ids: list[str] = Field( default_factory=list, description="Conflict IDs this question resolves", ) class EvidencePacket(BaseModel): """Evidence spans provided to the adjudicator. Contains the exact text and location of evidence the adjudicator can reference in its decisions. """ evidence_id: str = Field(description="Unique identifier for this evidence span") chunk_id: str = Field(description="Source chunk identifier") start_char: int = Field(ge=0, description="Start character offset within chunk") end_char: int = Field(gt=0, description="End character offset within chunk") text: str = Field(min_length=1, description="Evidence text content") source_document_id: str = Field(description="Parent document identifier") class DecisionVerdict(str, Enum): """Possible verdicts for an adjudication decision.""" ACCEPT = "accept" REJECT = "reject" MERGE = "merge" SPLIT = "split" REATTRIBUTE = "reattribute" class AdjudicationDecision(BaseModel): """The adjudicator's resolution for one or more candidates. IMPORTANT: This model intentionally EXCLUDES: - authoritative confidence (comes from calibration pipeline) - novelty (comes from retrieval-based novelty stage) - impact (comes from stock-specific impact model) - horizon (comes from impact model) The adjudicator resolves candidate identity, relationships, event interpretation, and supported qualitative direction only. Every decision MUST reference evidence_ids from the provided packet. """ decision_id: str = Field(description="Unique identifier for this decision") question_code: QuestionCode = Field(description="Which question this resolves") verdict: DecisionVerdict = Field(description="The adjudication verdict") candidate_ids: list[str] = Field( min_length=1, description="Candidate IDs this decision applies to", ) evidence_ids: list[str] = Field( min_length=1, description="Evidence IDs supporting this decision (required, non-empty)", ) reasoning: str = Field( description="Brief reasoning for the decision", ) resolved_value: dict[str, Any] = Field( default_factory=dict, description="The resolved value(s) if applicable", )