Multi-stage evidence-grounded inference architecture replacing the monolithic 9B model extraction pipeline. CPU-first specialist services handle routine extraction while the 9B vLLM model is preserved for semantic adjudication of ambiguous cases. Key components: - Capability-aware inference gateway (OpenAI-compatible + Ollama) - Endpoint registry with DB migrations and REST API - Sentence-aware document segmenter (property tests) - Deterministic financial parsing with offset integrity - Symbol resolution with ambiguity detection - Specialist service (GLiNER2, dynamic batching, K8s deployment) - Company-specific sentiment (FinBERT, calibration) - Retrieval-based novelty and duplicate detection - Confidence calibration pipeline - Deterministic routing engine (property tests) - 9B adjudication layer with VRAM gating - Stock-specific impact model (features, labels, baseline, trained) - Pipeline orchestrator (state machine, queues, leases, feature flags) - Bounded parallelism (async workers, semaphore, load shedding) - Observability (tracing, metrics, alerts) - Compatibility adapter (v3→v2 golden mapping tests) - Shadow/canary promotion framework - Active learning and fine-tuning pipeline Test results: 1,161 tests pass, ruff lint clean. All 282 spec tasks completed.
184 lines
6.1 KiB
Python
184 lines
6.1 KiB
Python
"""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),
|
|
)
|