"""Deterministic routing engine for Intelligence Pipeline v3. The RoutingEngine combines hard ambiguity/conflict rules with calibrated confidence thresholds to produce a deterministic route decision. The same inputs always produce the same output — no randomness or side effects. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any from uuid import UUID, uuid4 from services.intelligence_pipeline_v3.routing.reasons import ( RouteDecision, RoutingReason, ) from services.intelligence_pipeline_v3.routing.rules import evaluate_hard_rules from services.intelligence_pipeline_v3.routing.thresholds import ( FastPathThresholds, evaluate_thresholds, ) @dataclass(frozen=True) class RoutingDecision: """Immutable record of a routing decision with full context. Attributes ---------- id: Unique identifier for this decision. pipeline_run_id: The pipeline run this decision belongs to. document_id: The document being routed. route: The binary routing outcome (fast_path or adjudication). reasons: List of routing reasons explaining the decision. confidence_snapshot: Full feature snapshot at decision time for audit and recalibration. decided_at: UTC timestamp of the decision. """ id: UUID pipeline_run_id: UUID document_id: UUID route: RouteDecision reasons: list[RoutingReason] confidence_snapshot: dict[str, Any] decided_at: datetime @dataclass class RoutingEngine: """Deterministic routing engine. Evaluates hard rules first, then applies confidence thresholds. Same inputs always produce the same route — no randomness, no external state dependency beyond the provided arguments. Parameters ---------- thresholds: Fast-path threshold configuration. Defaults to conservative values. """ thresholds: FastPathThresholds = field(default_factory=FastPathThresholds) def route( self, pipeline_run_id: UUID, document_id: UUID, confidence_features: dict[str, Any], ambiguity_markers: dict[str, Any], document_type: str, event_type: str | None = None, ) -> RoutingDecision: """Produce a deterministic routing decision. Parameters ---------- pipeline_run_id: The pipeline run identifier. document_id: The document being routed. confidence_features: Field-level confidence features from the confidence pipeline. Must include ``calibrated_confidence`` (float 0-1). ambiguity_markers: Structural ambiguity markers from candidate generation. document_type: The document type (article, filing, transcript, etc.). event_type: Optional event type detected in the document. Returns ------- RoutingDecision Immutable decision record with route, reasons, and feature snapshot. """ # Step 1: Evaluate hard rules (any trigger = adjudication) hard_reasons = evaluate_hard_rules(confidence_features, ambiguity_markers) if hard_reasons: return self._build_decision( pipeline_run_id=pipeline_run_id, document_id=document_id, route=RouteDecision.ADJUDICATION, reasons=hard_reasons, confidence_features=confidence_features, ambiguity_markers=ambiguity_markers, ) # Step 2: Evaluate confidence thresholds calibrated_confidence = confidence_features.get("calibrated_confidence", 0.0) # Check evidence coverage threshold (hard threshold, not configurable per doc type) evidence_coverage = confidence_features.get("evidence_coverage", 1.0) if evidence_coverage < 0.5: return self._build_decision( pipeline_run_id=pipeline_run_id, document_id=document_id, route=RouteDecision.ADJUDICATION, reasons=[RoutingReason.EVIDENCE_COVERAGE_BELOW_THRESHOLD], confidence_features=confidence_features, ambiguity_markers=ambiguity_markers, ) # Apply calibrated confidence threshold threshold_decision = evaluate_thresholds( confidence=calibrated_confidence, document_type=document_type, event_type=event_type, thresholds=self.thresholds, ) if threshold_decision == RouteDecision.ADJUDICATION: return self._build_decision( pipeline_run_id=pipeline_run_id, document_id=document_id, route=RouteDecision.ADJUDICATION, reasons=[RoutingReason.CALIBRATED_CONFIDENCE_BELOW_THRESHOLD], confidence_features=confidence_features, ambiguity_markers=ambiguity_markers, ) # All checks passed — fast path accepted return self._build_decision( pipeline_run_id=pipeline_run_id, document_id=document_id, route=RouteDecision.FAST_PATH, reasons=[RoutingReason.FAST_PATH_ACCEPTED], confidence_features=confidence_features, ambiguity_markers=ambiguity_markers, ) def _build_decision( self, pipeline_run_id: UUID, document_id: UUID, route: RouteDecision, reasons: list[RoutingReason], confidence_features: dict[str, Any], ambiguity_markers: dict[str, Any], ) -> RoutingDecision: """Build an immutable routing decision with full snapshot.""" return RoutingDecision( id=uuid4(), pipeline_run_id=pipeline_run_id, document_id=document_id, route=route, reasons=reasons, confidence_snapshot={ "confidence_features": confidence_features, "ambiguity_markers": ambiguity_markers, "thresholds_version": self.thresholds.version, }, decided_at=datetime.now(timezone.utc), )