"""Audit and review data models. Supports evidence display with offsets, specialist probabilities, routing reasons, adjudicator decisions, impact-model outputs, and immutable reviewer correction events. """ from __future__ import annotations import enum from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any from uuid import UUID, uuid4 class ReviewStatus(str, enum.Enum): """Status of a document's review.""" PENDING = "pending" REVIEWED = "reviewed" CORRECTED = "corrected" CONFIRMED = "confirmed" class CorrectionType(str, enum.Enum): """Types of reviewer corrections.""" CORRECT = "correct" INCORRECT = "incorrect" UNSUPPORTED = "unsupported" AMBIGUOUS = "ambiguous" VALUE_OVERRIDE = "value_override" @dataclass(frozen=True) class CorrectionEvent: """Immutable reviewer correction event. Corrections are append-only audit events. They feed the active-learning dataset only through the approved export process. """ event_id: UUID record_id: UUID field_name: str correction_type: CorrectionType original_value: Any corrected_value: Any | None reviewer_id: str timestamp: datetime notes: str = "" @classmethod def create( cls, record_id: UUID, field_name: str, correction_type: CorrectionType, original_value: Any, corrected_value: Any | None = None, reviewer_id: str = "", notes: str = "", ) -> CorrectionEvent: return cls( event_id=uuid4(), record_id=record_id, field_name=field_name, correction_type=correction_type, original_value=original_value, corrected_value=corrected_value, reviewer_id=reviewer_id, timestamp=datetime.now(timezone.utc), notes=notes, ) @dataclass class AuditRecord: """Complete audit record for a processed document. Contains source evidence, specialist outputs, routing reasons, adjudicator decisions, and impact predictions displayed separately. """ record_id: UUID document_id: str run_id: UUID timestamp: datetime # Source evidence with offsets evidence_spans: list[dict[str, Any]] = field(default_factory=list) # Specialist stage outputs (probabilities, scores) specialist_outputs: dict[str, Any] = field(default_factory=dict) # Routing decision and reasons routing_reasons: list[str] = field(default_factory=list) route_decision: str = "" # Adjudicator decision (if applicable) adjudicator_decision: dict[str, Any] | None = None # Impact model outputs impact_outputs: dict[str, Any] = field(default_factory=dict) # Model lineage lineage: dict[str, Any] = field(default_factory=dict) # Review status review_status: ReviewStatus = ReviewStatus.PENDING corrections: list[CorrectionEvent] = field(default_factory=list) @classmethod def create( cls, document_id: str, run_id: UUID, evidence_spans: list[dict[str, Any]] | None = None, specialist_outputs: dict[str, Any] | None = None, routing_reasons: list[str] | None = None, route_decision: str = "", adjudicator_decision: dict[str, Any] | None = None, impact_outputs: dict[str, Any] | None = None, lineage: dict[str, Any] | None = None, ) -> AuditRecord: return cls( record_id=uuid4(), document_id=document_id, run_id=run_id, timestamp=datetime.now(timezone.utc), evidence_spans=evidence_spans or [], specialist_outputs=specialist_outputs or {}, routing_reasons=routing_reasons or [], route_decision=route_decision, adjudicator_decision=adjudicator_decision, impact_outputs=impact_outputs or {}, lineage=lineage or {}, ) def add_correction(self, correction: CorrectionEvent) -> None: """Add an immutable correction event.""" self.corrections.append(correction) self.review_status = ReviewStatus.CORRECTED def mark_reviewed(self) -> None: """Mark the record as reviewed without corrections.""" if self.review_status == ReviewStatus.PENDING: self.review_status = ReviewStatus.REVIEWED def mark_confirmed(self) -> None: """Mark the record as confirmed correct.""" self.review_status = ReviewStatus.CONFIRMED @dataclass class ReviewFilter: """Filter criteria for audit records. Supports filtering by confidence, unsupported claims, adjudication status, review status, and date ranges. """ min_confidence: float | None = None max_confidence: float | None = None has_unsupported_claims: bool | None = None is_adjudicated: bool | None = None review_status: ReviewStatus | None = None document_type: str | None = None company_id: UUID | None = None from_date: datetime | None = None to_date: datetime | None = None def matches(self, record: AuditRecord) -> bool: """Check if a record matches this filter.""" if self.is_adjudicated is not None: has_adj = record.adjudicator_decision is not None if has_adj != self.is_adjudicated: return False if self.review_status is not None: if record.review_status != self.review_status: return False if self.from_date is not None: if record.timestamp < self.from_date: return False if self.to_date is not None: if record.timestamp > self.to_date: return False if self.has_unsupported_claims is not None: has_unsupported = any( c.correction_type == CorrectionType.UNSUPPORTED for c in record.corrections ) if has_unsupported != self.has_unsupported_claims: return False return True