feat: Intelligence Pipeline v3 — full implementation

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.
This commit is contained in:
Celes Renata
2026-07-13 02:14:59 +00:00
parent 84634a365e
commit a72f336ad1
227 changed files with 50403 additions and 0 deletions
@@ -0,0 +1,32 @@
"""Confidence feature pipeline for Intelligence Pipeline v3.
Provides calibrated extraction confidence from specialist scores,
symbol resolution, evidence validation, schema completeness,
model agreement, and historical calibration data. Replaces
generative model self-reported confidence with empirically
calibrated probabilities.
"""
from services.intelligence_pipeline_v3.confidence.artifacts import (
load_artifact,
save_artifact,
)
from services.intelligence_pipeline_v3.confidence.calibrator import ConfidenceCalibrator
from services.intelligence_pipeline_v3.confidence.defaults import get_default_confidence
from services.intelligence_pipeline_v3.confidence.features import ConfidenceFeatureExtractor
from services.intelligence_pipeline_v3.confidence.models import (
CalibrationArtifactMetadata,
ConfidenceFeatures,
ConfidenceResult,
)
__all__ = [
"CalibrationArtifactMetadata",
"ConfidenceCalibrator",
"ConfidenceFeatureExtractor",
"ConfidenceFeatures",
"ConfidenceResult",
"get_default_confidence",
"load_artifact",
"save_artifact",
]
@@ -0,0 +1,186 @@
"""Calibration artifact persistence.
Handles versioned save/load of fitted calibrator objects alongside
metadata including training provenance, quality metrics, and version.
"""
from __future__ import annotations
import json
import logging
import pickle
from pathlib import Path
from services.intelligence_pipeline_v3.confidence.calibrator import ConfidenceCalibrator
from services.intelligence_pipeline_v3.confidence.models import CalibrationArtifactMetadata
logger = logging.getLogger(__name__)
ARTIFACT_FILE = "calibrator.pkl"
METADATA_FILE = "metadata.json"
def save_artifact(
calibrator: ConfidenceCalibrator,
version: str,
path: str | Path,
) -> Path:
"""Save a fitted calibrator and metadata to a versioned directory.
Creates the directory structure:
<path>/<version>/calibrator.pkl
<path>/<version>/metadata.json
Parameters
----------
calibrator
A fitted ConfidenceCalibrator instance.
version
Version string for this artifact (e.g., "v1.0.0").
path
Base directory for artifact storage.
Returns
-------
Path
Path to the versioned artifact directory.
Raises
------
ValueError
If the calibrator has not been fitted.
"""
if not calibrator.is_fitted:
raise ValueError("Cannot save an unfitted calibrator")
artifact_dir = Path(path) / version
artifact_dir.mkdir(parents=True, exist_ok=True)
# Save the calibrator model
calibrator_path = artifact_dir / ARTIFACT_FILE
with open(calibrator_path, "wb") as f:
pickle.dump(calibrator, f, protocol=pickle.HIGHEST_PROTOCOL)
# Save metadata
metadata = calibrator.metadata
if metadata is None:
metadata = CalibrationArtifactMetadata(
version=version,
method=calibrator.method, # type: ignore[arg-type]
training_count=0,
training_range="unknown",
ece=0.0,
brier_score=0.0,
)
metadata_path = artifact_dir / METADATA_FILE
with open(metadata_path, "w") as f:
json.dump(metadata.model_dump(mode="json"), f, indent=2, default=str)
logger.info(
"Saved calibration artifact: version=%s, method=%s, path=%s",
version,
calibrator.method,
artifact_dir,
)
return artifact_dir
def load_artifact(path: str | Path) -> ConfidenceCalibrator:
"""Load a calibrator from a versioned artifact directory.
Expects the directory to contain calibrator.pkl and metadata.json.
Parameters
----------
path
Path to the versioned artifact directory (e.g., <base>/v1.0.0/).
Returns
-------
ConfidenceCalibrator
The loaded and ready-to-use calibrator.
Raises
------
FileNotFoundError
If the artifact directory or files don't exist.
ValueError
If the loaded object is not a ConfidenceCalibrator.
"""
artifact_dir = Path(path)
calibrator_path = artifact_dir / ARTIFACT_FILE
if not calibrator_path.exists():
raise FileNotFoundError(
f"Calibrator artifact not found at {calibrator_path}"
)
with open(calibrator_path, "rb") as f:
calibrator = pickle.load(f) # noqa: S301
if not isinstance(calibrator, ConfidenceCalibrator):
raise ValueError(
f"Loaded object is not a ConfidenceCalibrator: {type(calibrator)}"
)
logger.info(
"Loaded calibration artifact: version=%s, method=%s, path=%s",
calibrator.version,
calibrator.method,
artifact_dir,
)
return calibrator
def load_metadata(path: str | Path) -> CalibrationArtifactMetadata:
"""Load only the metadata for an artifact without loading the full model.
Parameters
----------
path
Path to the versioned artifact directory.
Returns
-------
CalibrationArtifactMetadata
The artifact metadata.
Raises
------
FileNotFoundError
If the metadata file doesn't exist.
"""
metadata_path = Path(path) / METADATA_FILE
if not metadata_path.exists():
raise FileNotFoundError(f"Metadata not found at {metadata_path}")
with open(metadata_path) as f:
data = json.load(f)
return CalibrationArtifactMetadata(**data)
def list_versions(base_path: str | Path) -> list[str]:
"""List all available artifact versions in a base directory.
Parameters
----------
base_path
Base directory containing versioned subdirectories.
Returns
-------
list[str]
Sorted list of version strings.
"""
base = Path(base_path)
if not base.exists():
return []
versions = []
for item in base.iterdir():
if item.is_dir() and (item / ARTIFACT_FILE).exists():
versions.append(item.name)
return sorted(versions)
@@ -0,0 +1,406 @@
"""Confidence calibrator using isotonic or Platt scaling.
Maps confidence feature vectors to calibrated correctness probabilities.
Supports training on held-out Gold_Corpus data, cross-validation for
method comparison, and versioned artifact tracking.
"""
from __future__ import annotations
import logging
from typing import Literal
import numpy as np
from services.intelligence_pipeline_v3.confidence.models import (
CalibrationArtifactMetadata,
ConfidenceFeatures,
)
logger = logging.getLogger(__name__)
DEFAULT_VERSION = "uncalibrated"
class ConfidenceCalibrator:
"""Calibrates confidence features to correctness probabilities.
Supports isotonic regression and Platt (logistic) scaling.
The calibrator is fitted on labeled Gold_Corpus data where labels
indicate whether the extraction was correct (True) or not (False).
Parameters
----------
method
Calibration method: "isotonic" for non-parametric monotone fit,
"platt" for logistic regression scaling.
"""
def __init__(self, method: Literal["isotonic", "platt"] = "isotonic") -> None:
self._method: Literal["isotonic", "platt"] = method
self._version: str = DEFAULT_VERSION
self._fitted: bool = False
self._model: object | None = None
self._metadata: CalibrationArtifactMetadata | None = None
self._training_count: int = 0
@property
def method(self) -> str:
"""Return the calibration method."""
return self._method
@property
def version(self) -> str:
"""Return the calibration artifact version."""
return self._version
@property
def is_fitted(self) -> bool:
"""Return whether the calibrator has been fitted."""
return self._fitted
@property
def metadata(self) -> CalibrationArtifactMetadata | None:
"""Return the artifact metadata if fitted."""
return self._metadata
def fit(
self,
features: list[ConfidenceFeatures],
labels: list[bool],
method: str | None = None,
version: str = "v1.0.0",
training_range: str = "unknown",
) -> None:
"""Train the calibrator on labeled feature/correctness pairs.
Parameters
----------
features
List of confidence feature vectors from training data.
labels
True if the extraction was correct, False otherwise.
method
Override method for this fit (isotonic or platt).
If None, uses the instance default.
version
Version string for the resulting artifact.
training_range
Description of the training data date range.
Raises
------
ValueError
If features and labels have different lengths or are empty.
"""
if not features or not labels:
raise ValueError("features and labels must not be empty")
if len(features) != len(labels):
raise ValueError(
f"features ({len(features)}) and labels ({len(labels)}) must have the same length"
)
if method is not None:
if method not in ("isotonic", "platt"):
raise ValueError(f"method must be 'isotonic' or 'platt', got '{method}'")
self._method = method # type: ignore[assignment]
# Convert features to matrix
X = np.array([f.to_vector() for f in features], dtype=np.float64)
y = np.array(labels, dtype=np.float64)
if self._method == "isotonic":
self._fit_isotonic(X, y)
else:
self._fit_platt(X, y)
self._version = version
self._training_count = len(features)
self._fitted = True
# Compute calibration quality on training data (for metadata)
predictions = self._predict_batch(X)
ece = _compute_ece(predictions, y)
brier = _compute_brier(predictions, y)
self._metadata = CalibrationArtifactMetadata(
version=version,
method=self._method,
training_count=len(features),
training_range=training_range,
ece=ece,
brier_score=brier,
)
logger.info(
"ConfidenceCalibrator fitted: method=%s, n=%d, version=%s, ECE=%.4f, Brier=%.4f",
self._method,
len(features),
version,
ece,
brier,
)
def predict(self, features: ConfidenceFeatures) -> float:
"""Return calibrated probability of extraction correctness.
Parameters
----------
features
Confidence feature vector for a single extraction.
Returns
-------
float
Calibrated probability in [0, 1].
"""
if not self._fitted:
# Return a neutral default when uncalibrated
return 0.5
X = np.array([features.to_vector()], dtype=np.float64)
predictions = self._predict_batch(X)
return float(np.clip(predictions[0], 0.0, 1.0))
def predict_batch(self, features_list: list[ConfidenceFeatures]) -> list[float]:
"""Return calibrated probabilities for a batch of feature vectors.
Parameters
----------
features_list
List of confidence feature vectors.
Returns
-------
list[float]
Calibrated probabilities in [0, 1].
"""
if not self._fitted:
return [0.5] * len(features_list)
X = np.array([f.to_vector() for f in features_list], dtype=np.float64)
predictions = self._predict_batch(X)
return [float(np.clip(p, 0.0, 1.0)) for p in predictions]
def evaluate(
self,
features: list[ConfidenceFeatures],
labels: list[bool],
) -> tuple[float, float]:
"""Evaluate ECE and Brier score on held-out data.
Parameters
----------
features
Held-out feature vectors.
labels
True correctness labels.
Returns
-------
tuple[float, float]
(ECE, Brier_score) on the held-out set.
"""
if not features or not labels:
raise ValueError("features and labels must not be empty")
if len(features) != len(labels):
raise ValueError("features and labels must have the same length")
X = np.array([f.to_vector() for f in features], dtype=np.float64)
y = np.array(labels, dtype=np.float64)
if self._fitted:
predictions = self._predict_batch(X)
else:
predictions = np.full(len(y), 0.5)
ece = _compute_ece(predictions, y)
brier = _compute_brier(predictions, y)
return ece, brier
def _fit_isotonic(self, X: np.ndarray, y: np.ndarray) -> None:
"""Fit isotonic regression on aggregated feature scores."""
from sklearn.isotonic import IsotonicRegression
# Aggregate features into a single score for isotonic monotone fit
aggregated = X.mean(axis=1)
iso = IsotonicRegression(y_min=0.0, y_max=1.0, out_of_bounds="clip")
iso.fit(aggregated, y)
self._model = iso
def _fit_platt(self, X: np.ndarray, y: np.ndarray) -> None:
"""Fit logistic regression (Platt scaling) on the full feature vector."""
from sklearn.linear_model import LogisticRegression
y_int = y.astype(np.int32)
if len(np.unique(y_int)) < 2:
# Not enough class diversity — store a dummy model
self._model = _ConstantPredictor(float(y.mean()))
return
lr = LogisticRegression(solver="lbfgs", max_iter=1000, C=1.0)
lr.fit(X, y_int)
self._model = lr
def _predict_batch(self, X: np.ndarray) -> np.ndarray:
"""Internal prediction dispatch."""
if self._model is None:
return np.full(X.shape[0], 0.5)
if self._method == "isotonic":
# Isotonic uses aggregated score
aggregated = X.mean(axis=1)
return self._model.predict(aggregated) # type: ignore[union-attr]
else:
# Platt uses full feature vector
if isinstance(self._model, _ConstantPredictor):
return self._model.predict(X)
return self._model.predict_proba(X)[:, 1] # type: ignore[union-attr]
class _ConstantPredictor:
"""Fallback predictor when training data has only one class."""
def __init__(self, value: float) -> None:
self._value = value
def predict(self, X: np.ndarray) -> np.ndarray:
return np.full(X.shape[0], self._value)
def _compute_ece(
predictions: np.ndarray,
labels: np.ndarray,
n_bins: int = 10,
) -> float:
"""Compute Expected Calibration Error.
Partitions predictions into equal-width bins and computes the
weighted average of |avg_predicted - avg_actual| per bin.
Parameters
----------
predictions
Predicted probabilities.
labels
True binary labels (0 or 1).
n_bins
Number of equal-width bins.
Returns
-------
float
ECE value in [0, 1].
"""
if len(predictions) == 0:
return 0.0
bin_boundaries = np.linspace(0.0, 1.0, n_bins + 1)
ece = 0.0
n = len(predictions)
for i in range(n_bins):
lower = bin_boundaries[i]
upper = bin_boundaries[i + 1]
if i == n_bins - 1:
# Include right boundary in last bin
mask = (predictions >= lower) & (predictions <= upper)
else:
mask = (predictions >= lower) & (predictions < upper)
bin_count = mask.sum()
if bin_count == 0:
continue
avg_predicted = predictions[mask].mean()
avg_actual = labels[mask].mean()
ece += (bin_count / n) * abs(avg_predicted - avg_actual)
return float(ece)
def _compute_brier(predictions: np.ndarray, labels: np.ndarray) -> float:
"""Compute Brier score (mean squared error of probability predictions).
Parameters
----------
predictions
Predicted probabilities.
labels
True binary labels (0 or 1).
Returns
-------
float
Brier score in [0, 1].
"""
if len(predictions) == 0:
return 0.0
return float(np.mean((predictions - labels) ** 2))
def compare_methods(
features: list[ConfidenceFeatures],
labels: list[bool],
n_folds: int = 5,
) -> dict[str, dict[str, float]]:
"""Compare isotonic and Platt methods using k-fold cross-validation.
Parameters
----------
features
Full set of training features.
labels
Full set of correctness labels.
n_folds
Number of cross-validation folds.
Returns
-------
dict
Mapping of method name to {"ece": float, "brier": float} averages.
"""
if len(features) < n_folds * 2:
raise ValueError(
f"Need at least {n_folds * 2} samples for {n_folds}-fold CV, got {len(features)}"
)
results: dict[str, list[tuple[float, float]]] = {
"isotonic": [],
"platt": [],
}
indices = np.arange(len(features))
fold_size = len(features) // n_folds
for fold in range(n_folds):
val_start = fold * fold_size
val_end = val_start + fold_size if fold < n_folds - 1 else len(features)
val_indices = indices[val_start:val_end]
train_indices = np.concatenate([indices[:val_start], indices[val_end:]])
train_features = [features[i] for i in train_indices]
train_labels = [labels[i] for i in train_indices]
val_features = [features[i] for i in val_indices]
val_labels = [labels[i] for i in val_indices]
for method_name in ("isotonic", "platt"):
cal = ConfidenceCalibrator(method=method_name) # type: ignore[arg-type]
cal.fit(
train_features,
train_labels,
version=f"cv-fold-{fold}",
training_range="cross-validation",
)
ece, brier = cal.evaluate(val_features, val_labels)
results[method_name].append((ece, brier))
return {
method: {
"ece": float(np.mean([r[0] for r in scores])),
"brier": float(np.mean([r[1] for r in scores])),
}
for method, scores in results.items()
}
@@ -0,0 +1,142 @@
"""Conservative confidence defaults for underrepresented classes.
When calibration data is insufficient for a specific document type or
event class, returns conservative values (0.3-0.5) and marks the result
as under-calibrated per Requirement 10.7.
"""
from __future__ import annotations
import logging
from services.intelligence_pipeline_v3.confidence.models import ConfidenceResult
logger = logging.getLogger(__name__)
# Conservative default probabilities by document type.
# These are intentionally low (0.3-0.5) to avoid overconfidence
# when insufficient calibration data exists.
_DOCUMENT_TYPE_DEFAULTS: dict[str, float] = {
"news": 0.45,
"filing": 0.40,
"transcript": 0.40,
"press_release": 0.45,
"macro_event": 0.35,
"unknown": 0.30,
}
# Conservative default probabilities by event class.
# More complex or rare event types get lower defaults.
_EVENT_CLASS_DEFAULTS: dict[str, float] = {
"earnings_beat": 0.50,
"earnings_miss": 0.50,
"guidance_raise": 0.45,
"guidance_cut": 0.45,
"merger_acquisition": 0.40,
"product_launch": 0.45,
"regulatory_action": 0.40,
"management_change": 0.45,
"legal_proceeding": 0.40,
"supply_chain": 0.35,
"rating_change": 0.45,
"dividend_change": 0.45,
"buyback": 0.45,
"macro_policy": 0.35,
"geopolitical": 0.30,
"sector_rotation": 0.35,
"unknown": 0.30,
}
# Features used when returning conservative defaults
_DEFAULT_FEATURES_USED = [
"document_type_prior",
"event_class_prior",
]
def get_default_confidence(
document_type: str,
event_class: str,
) -> ConfidenceResult:
"""Return a conservative confidence result for underrepresented classes.
Used when calibration data is insufficient for the given document type
and event class combination. Returns conservative probabilities (0.3-0.5)
and marks the result as under-calibrated.
Parameters
----------
document_type
The document type (news, filing, transcript, etc.).
event_class
The classified event type (earnings_beat, merger_acquisition, etc.).
Returns
-------
ConfidenceResult
A conservative confidence result with under_calibrated=True.
"""
doc_default = _DOCUMENT_TYPE_DEFAULTS.get(
document_type, _DOCUMENT_TYPE_DEFAULTS["unknown"]
)
event_default = _EVENT_CLASS_DEFAULTS.get(
event_class, _EVENT_CLASS_DEFAULTS["unknown"]
)
# Take the minimum of document and event defaults for extra conservatism
probability = min(doc_default, event_default)
logger.debug(
"Using conservative default confidence: doc_type=%s (%.2f), event=%s (%.2f) -> %.2f",
document_type,
doc_default,
event_class,
event_default,
probability,
)
return ConfidenceResult(
probability=probability,
features_used=_DEFAULT_FEATURES_USED,
is_calibrated=False,
under_calibrated=True,
calibration_version="conservative-default-v1",
)
def is_underrepresented(
document_type: str,
event_class: str,
min_samples: int = 30,
known_counts: dict[tuple[str, str], int] | None = None,
) -> bool:
"""Check if a document_type + event_class combination is underrepresented.
Parameters
----------
document_type
The document type.
event_class
The event class.
min_samples
Minimum number of calibration samples to consider a class well-represented.
known_counts
Optional mapping of (doc_type, event_class) -> sample count.
If None, treats any unknown combination as underrepresented.
Returns
-------
bool
True if the class has insufficient calibration data.
"""
if known_counts is None:
# Without explicit counts, use heuristic: unknown types are underrepresented
if document_type not in _DOCUMENT_TYPE_DEFAULTS:
return True
if event_class not in _EVENT_CLASS_DEFAULTS:
return True
return False
key = (document_type, event_class)
count = known_counts.get(key, 0)
return count < min_samples
@@ -0,0 +1,221 @@
"""Confidence feature extraction from upstream pipeline stages.
Computes field-level features from extraction, resolution, evidence,
sentiment, and cross-stage agreement to produce a ConfidenceFeatures
vector for calibration or conservative defaults.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from services.intelligence_pipeline_v3.confidence.models import ConfidenceFeatures
logger = logging.getLogger(__name__)
@dataclass
class ExtractionStageResult:
"""Subset of extraction results relevant to confidence features.
This is an adapter interface — callers populate it from
the full extraction/specialist output.
"""
entity_scores: list[float]
"""Per-entity confidence scores from specialist extractor."""
relation_scores: list[float]
"""Per-relation confidence scores."""
total_facts: int
"""Total facts extracted."""
valid_numeric_facts: int
"""Facts that passed deterministic parser validation."""
populated_fields: int
"""Schema fields that have values."""
expected_fields: int
"""Total expected schema fields for this document type."""
@dataclass
class ResolutionStageResult:
"""Subset of resolution results relevant to confidence features."""
ambiguity_margins: list[float]
"""Per-mention ambiguity margins (gap between top-2 candidates)."""
@dataclass
class EvidenceStageResult:
"""Subset of evidence verification results relevant to confidence features."""
total_claims: int
"""Total extracted claims/facts."""
supported_claims: int
"""Claims backed by valid evidence spans."""
@dataclass
class SentimentStageResult:
"""Subset of sentiment results relevant to confidence features."""
max_class_probabilities: list[float]
"""Per-company maximum class probability after calibration."""
calibration_version: str
"""Version of sentiment calibration artifact used."""
@dataclass
class AgreementStageResult:
"""Cross-stage agreement analysis results."""
agreement_ratio: float
"""Fraction of facts that agree across independent extraction paths."""
novelty_certainty: float
"""Certainty of the novelty/duplicate classification (0-1)."""
hard_case_score: float
"""Score indicating presence of known difficult patterns."""
class ConfidenceFeatureExtractor:
"""Extracts confidence features from upstream pipeline stage results.
Produces a normalized ConfidenceFeatures vector that can be passed
to the calibrator or used to determine conservative defaults.
"""
def extract_features(
self,
extraction_result: ExtractionStageResult,
resolution_result: ResolutionStageResult,
evidence_result: EvidenceStageResult,
sentiment_result: SentimentStageResult,
agreement_result: AgreementStageResult | None = None,
document_type: str = "unknown",
) -> ConfidenceFeatures:
"""Compute confidence features from all upstream stage results.
Parameters
----------
extraction_result
Entity/relation/fact extraction outputs with scores.
resolution_result
Symbol resolution outputs with ambiguity margins.
evidence_result
Evidence verification outputs with coverage stats.
sentiment_result
Sentiment classification outputs with calibrated probabilities.
agreement_result
Optional cross-stage agreement analysis. Defaults used if None.
document_type
Document type string for type-specific calibration.
Returns
-------
ConfidenceFeatures
Normalized feature vector ready for calibration.
"""
# Entity span score: average of entity scores, or 0 if none
entity_span_score = (
sum(extraction_result.entity_scores) / len(extraction_result.entity_scores)
if extraction_result.entity_scores
else 0.0
)
# Alias resolution margin: average of per-mention margins
alias_resolution_margin = (
sum(resolution_result.ambiguity_margins)
/ len(resolution_result.ambiguity_margins)
if resolution_result.ambiguity_margins
else 1.0 # No ambiguity if no mentions to resolve
)
# Numeric parser validity: fraction of valid numeric facts
numeric_parser_validity = (
extraction_result.valid_numeric_facts / extraction_result.total_facts
if extraction_result.total_facts > 0
else 1.0 # No numeric facts = no parser failures
)
# Evidence coverage: fraction of claims with valid evidence
evidence_coverage = (
evidence_result.supported_claims / evidence_result.total_claims
if evidence_result.total_claims > 0
else 0.0
)
# Relation score: average relation confidence
relation_score = (
sum(extraction_result.relation_scores)
/ len(extraction_result.relation_scores)
if extraction_result.relation_scores
else 0.0
)
# Sentiment calibration confidence: average max class probability
sentiment_calibration_confidence = (
sum(sentiment_result.max_class_probabilities)
/ len(sentiment_result.max_class_probabilities)
if sentiment_result.max_class_probabilities
else 0.5 # Neutral default when no sentiment data
)
# Document completeness: fraction of expected fields populated
document_completeness = (
extraction_result.populated_fields / extraction_result.expected_fields
if extraction_result.expected_fields > 0
else 0.0
)
# Cross-stage agreement features (use defaults if not provided)
if agreement_result is not None:
cross_stage_agreement = agreement_result.agreement_ratio
duplicate_novelty_certainty = agreement_result.novelty_certainty
known_hard_case_patterns = agreement_result.hard_case_score
else:
cross_stage_agreement = 0.5 # Neutral default
duplicate_novelty_certainty = 0.5
known_hard_case_patterns = 0.0
# Validate document type
valid_types = {
"news",
"filing",
"transcript",
"press_release",
"macro_event",
"unknown",
}
if document_type not in valid_types:
logger.warning(
"Unknown document_type '%s', defaulting to 'unknown'", document_type
)
document_type = "unknown"
return ConfidenceFeatures(
entity_span_score=_clamp(entity_span_score),
alias_resolution_margin=_clamp(alias_resolution_margin),
numeric_parser_validity=_clamp(numeric_parser_validity),
evidence_coverage=_clamp(evidence_coverage),
relation_score=_clamp(relation_score),
sentiment_calibration_confidence=_clamp(sentiment_calibration_confidence),
cross_stage_agreement=_clamp(cross_stage_agreement),
duplicate_novelty_certainty=_clamp(duplicate_novelty_certainty),
document_completeness=_clamp(document_completeness),
document_type=document_type,
known_hard_case_patterns=_clamp(known_hard_case_patterns),
)
def _clamp(value: float, low: float = 0.0, high: float = 1.0) -> float:
"""Clamp value to [low, high]."""
return max(low, min(high, value))
@@ -0,0 +1,179 @@
"""Pydantic models for confidence calibration pipeline.
Defines feature vectors, calibration artifact metadata, and
confidence results used throughout the confidence pipeline.
"""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Literal
from pydantic import BaseModel, Field, field_validator
class ConfidenceFeatures(BaseModel):
"""Feature vector for confidence estimation.
Each feature is a normalized float derived from upstream pipeline
stages: extraction, resolution, evidence verification, sentiment,
and cross-stage agreement analysis.
"""
entity_span_score: float = Field(
ge=0.0,
le=1.0,
description="Best entity span confidence from specialist extractor.",
)
alias_resolution_margin: float = Field(
ge=0.0,
le=1.0,
description="Gap between top-2 alias candidates. 1.0 = unambiguous.",
)
numeric_parser_validity: float = Field(
ge=0.0,
le=1.0,
description="Fraction of numeric facts that passed parser validation.",
)
evidence_coverage: float = Field(
ge=0.0,
le=1.0,
description="Fraction of extracted facts backed by valid evidence spans.",
)
relation_score: float = Field(
ge=0.0,
le=1.0,
description="Average confidence of extracted relations.",
)
sentiment_calibration_confidence: float = Field(
ge=0.0,
le=1.0,
description="Calibrated sentiment model confidence (max class probability).",
)
cross_stage_agreement: float = Field(
ge=0.0,
le=1.0,
description="Agreement ratio between independently derived facts across stages.",
)
duplicate_novelty_certainty: float = Field(
ge=0.0,
le=1.0,
description="Certainty of the novelty/duplicate classification.",
)
document_completeness: float = Field(
ge=0.0,
le=1.0,
description="Fraction of expected schema fields that were populated.",
)
document_type: str = Field(
description="Document type (news, filing, transcript, press_release, macro_event).",
)
known_hard_case_patterns: float = Field(
ge=0.0,
le=1.0,
description="Score indicating presence of known hard patterns (multi-company, contradictions).",
)
@field_validator("document_type")
@classmethod
def document_type_valid(cls, v: str) -> str:
valid_types = {
"news",
"filing",
"transcript",
"press_release",
"macro_event",
"unknown",
}
if v not in valid_types:
raise ValueError(f"document_type must be one of {valid_types}, got '{v}'")
return v
def to_vector(self) -> list[float]:
"""Convert features to a flat numeric vector for calibration models.
document_type is encoded as a categorical index.
"""
type_map = {
"news": 0.0,
"filing": 0.2,
"transcript": 0.4,
"press_release": 0.6,
"macro_event": 0.8,
"unknown": 1.0,
}
return [
self.entity_span_score,
self.alias_resolution_margin,
self.numeric_parser_validity,
self.evidence_coverage,
self.relation_score,
self.sentiment_calibration_confidence,
self.cross_stage_agreement,
self.duplicate_novelty_certainty,
self.document_completeness,
type_map.get(self.document_type, 1.0),
self.known_hard_case_patterns,
]
class CalibrationArtifactMetadata(BaseModel):
"""Metadata for a versioned calibration artifact.
Stored alongside the serialized calibrator to track provenance,
training conditions, and quality metrics.
"""
version: str = Field(description="Artifact version string (e.g., 'v1.0.0').")
method: Literal["isotonic", "platt"] = Field(
description="Calibration method used."
)
training_count: int = Field(
ge=0, description="Number of samples used for training."
)
training_range: str = Field(
description="Date range of training data (e.g., '2024-01-01 to 2024-06-30')."
)
ece: float = Field(
ge=0.0,
le=1.0,
description="Expected Calibration Error on held-out data.",
)
brier_score: float = Field(
ge=0.0,
le=1.0,
description="Brier score on held-out data.",
)
created_at: datetime = Field(
default_factory=lambda: datetime.now(tz=timezone.utc),
description="When the artifact was created.",
)
class ConfidenceResult(BaseModel):
"""Final confidence output for an extraction record.
Contains the calibrated probability, feature breakdown, and
metadata about whether calibration was applied or defaults used.
"""
probability: float = Field(
ge=0.0,
le=1.0,
description="Calibrated probability of extraction correctness.",
)
features_used: list[str] = Field(
description="Names of features that contributed to this confidence score.",
)
is_calibrated: bool = Field(
default=True,
description="Whether a trained calibrator was used (vs conservative default).",
)
under_calibrated: bool = Field(
default=False,
description="True if class has insufficient calibration data and conservative default was applied.",
)
calibration_version: str = Field(
default="uncalibrated",
description="Version of the calibration artifact used.",
)