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,302 @@
"""Focused adjudication prompt building for Intelligence Pipeline v3.
Builds adjudication packets containing only relevant chunks and candidates,
uses strict JSON Schema with temperature zero, and enforces a bounded output
budget (max 1536 tokens for decisions, not summaries).
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
from services.intelligence_pipeline_v3.adjudication.schemas import (
AdjudicationCandidate,
AdjudicationQuestion,
ConflictDescription,
EvidencePacket,
QuestionCode,
)
from services.intelligence_pipeline_v3.segmenter.models import DocumentChunk
# --- Constants ---
MAX_OUTPUT_TOKENS: int = 1536
"""Maximum tokens for adjudication decisions output. Bounded to prevent
long summaries — the adjudicator produces decisions, not narratives."""
TEMPERATURE: float = 0.0
"""Temperature for adjudication requests. Zero for deterministic output."""
PROMPT_SCHEMA_VERSION: str = "1.0.0"
"""Version of the adjudication prompt schema format."""
PROVIDER_LINEAGE_KEY: str = "adjudication_v3"
"""Lineage identifier for adjudication prompts."""
# --- Models ---
class PromptMetadata(BaseModel):
"""Metadata for the adjudication prompt including version and lineage.
Tracks prompt version, schema version, and provider lineage for
reproducibility and auditing.
"""
prompt_version: str = Field(
default="1.0.0",
description="Version of the prompt template",
)
schema_version: str = Field(
default=PROMPT_SCHEMA_VERSION,
description="Version of the JSON Schema format used",
)
provider_lineage: str = Field(
default=PROVIDER_LINEAGE_KEY,
description="Identifier for the prompt provider/pipeline stage",
)
max_output_tokens: int = Field(
default=MAX_OUTPUT_TOKENS,
description="Maximum output token budget for this prompt",
)
temperature: float = Field(
default=TEMPERATURE,
description="Generation temperature",
)
class AdjudicationPacket(BaseModel):
"""Complete packet sent to the 9B adjudicator.
Contains only the information relevant to resolving the specific
ambiguity — relevant chunks, candidates, conflicts, and questions.
"""
document_id: str = Field(description="Source document identifier")
document_type: str = Field(description="Type of document")
relevant_chunks: list[DocumentChunk] = Field(
description="Only chunks relevant to the adjudication questions",
)
candidates: list[AdjudicationCandidate] = Field(
description="Candidates requiring adjudication",
)
conflicts: list[ConflictDescription] = Field(
default_factory=list,
description="Conflicts between candidates",
)
questions: list[AdjudicationQuestion] = Field(
description="Specific questions the adjudicator must answer",
)
evidence: list[EvidencePacket] = Field(
description="Evidence spans available for reference",
)
metadata: PromptMetadata = Field(
default_factory=PromptMetadata,
description="Prompt metadata for versioning and lineage",
)
# --- Output schema for strict JSON mode ---
def get_decision_json_schema() -> dict[str, Any]:
"""Return the strict JSON Schema for adjudication decisions.
Used as the `response_format.json_schema.schema` payload when
calling the 9B model with strict structured output.
"""
return {
"type": "object",
"properties": {
"decisions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"decision_id": {"type": "string"},
"question_code": {
"type": "string",
"enum": [code.value for code in QuestionCode],
},
"verdict": {
"type": "string",
"enum": [
"accept",
"reject",
"merge",
"split",
"reattribute",
],
},
"candidate_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
},
"evidence_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
},
"reasoning": {"type": "string"},
"resolved_value": {"type": "object"},
},
"required": [
"decision_id",
"question_code",
"verdict",
"candidate_ids",
"evidence_ids",
"reasoning",
],
"additionalProperties": False,
},
},
},
"required": ["decisions"],
"additionalProperties": False,
}
# --- Packet builder ---
def _get_relevant_chunk_ids(
candidates: list[AdjudicationCandidate],
conflicts: list[ConflictDescription],
questions: list[AdjudicationQuestion],
) -> set[str]:
"""Collect chunk IDs referenced by candidates, conflicts, and questions."""
chunk_ids: set[str] = set()
for candidate in candidates:
chunk_ids.update(candidate.source_chunk_ids)
return chunk_ids
def _filter_relevant_chunks(
document_chunks: list[DocumentChunk],
relevant_chunk_ids: set[str],
) -> list[DocumentChunk]:
"""Filter document chunks to include only those referenced by candidates."""
if not relevant_chunk_ids:
# If no specific chunks referenced, include all (fallback for
# cases where chunk IDs weren't specified in candidates)
return document_chunks
return [c for c in document_chunks if c.chunk_id in relevant_chunk_ids]
def _collect_evidence_ids(
candidates: list[AdjudicationCandidate],
conflicts: list[ConflictDescription],
) -> set[str]:
"""Collect all evidence IDs referenced by candidates and conflicts."""
evidence_ids: set[str] = set()
for candidate in candidates:
evidence_ids.update(candidate.evidence_ids)
for conflict in conflicts:
evidence_ids.update(conflict.evidence_ids)
return evidence_ids
def build_adjudication_packet(
document_id: str,
document_type: str,
document_chunks: list[DocumentChunk],
candidates: list[AdjudicationCandidate],
conflicts: list[ConflictDescription],
questions: list[AdjudicationQuestion],
evidence: list[EvidencePacket],
*,
question_codes: list[str] | None = None,
) -> AdjudicationPacket:
"""Build an adjudication packet with only relevant chunks and evidence.
Filters document_chunks to include only those referenced by the
candidates being adjudicated. Ensures the packet is focused and
within the bounded context the adjudicator expects.
Args:
document_id: Source document identifier.
document_type: Type of document (article, filing, transcript, etc.).
document_chunks: All available chunks for the document.
candidates: Candidates requiring adjudication.
conflicts: Conflicts between candidates.
questions: Specific questions to resolve.
evidence: Available evidence spans.
question_codes: Optional filter to limit questions by code.
Returns:
AdjudicationPacket with only relevant chunks included.
"""
# Filter questions by code if specified
filtered_questions = questions
if question_codes:
code_set = set(question_codes)
filtered_questions = [
q for q in questions if q.question_code.value in code_set
]
# Determine which chunks are relevant
relevant_chunk_ids = _get_relevant_chunk_ids(
candidates, conflicts, filtered_questions
)
relevant_chunks = _filter_relevant_chunks(document_chunks, relevant_chunk_ids)
# Filter evidence to only include those referenced by candidates/conflicts
referenced_evidence_ids = _collect_evidence_ids(candidates, conflicts)
if referenced_evidence_ids:
relevant_evidence = [
e for e in evidence if e.evidence_id in referenced_evidence_ids
]
else:
# Include all evidence if none specifically referenced
relevant_evidence = evidence
return AdjudicationPacket(
document_id=document_id,
document_type=document_type,
relevant_chunks=relevant_chunks,
candidates=candidates,
conflicts=conflicts,
questions=filtered_questions,
evidence=relevant_evidence,
metadata=PromptMetadata(),
)
def build_request_payload(packet: AdjudicationPacket) -> dict[str, Any]:
"""Build the full inference request payload for the adjudicator.
Returns a dict suitable for passing to the inference gateway,
including strict JSON Schema response format and temperature zero.
"""
system_prompt = (
"You are a semantic adjudicator for financial document extraction. "
"Resolve the ambiguities described in the questions using ONLY the "
"provided evidence spans. Every decision MUST reference evidence_ids "
"from the provided evidence. Do NOT estimate confidence, novelty, "
"impact magnitude, or time horizon — those are computed by separate "
"calibrated pipelines. Output valid JSON matching the required schema."
)
user_content = packet.model_dump_json()
return {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
"temperature": packet.metadata.temperature,
"max_tokens": packet.metadata.max_output_tokens,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "adjudication_response",
"strict": True,
"schema": get_decision_json_schema(),
},
},
}