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:
@@ -0,0 +1,151 @@
|
||||
"""API router for specialist inference endpoints.
|
||||
|
||||
All endpoints live under /api/specialist/ prefix for consistency with
|
||||
the broader Intelligence Pipeline v3 routing conventions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from services.specialist.models import (
|
||||
BatchResponse,
|
||||
ClassificationRequest,
|
||||
ExtractionRequest,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/specialist", tags=["specialist"])
|
||||
|
||||
# Engine is injected at app startup via app.state
|
||||
_engine = None
|
||||
|
||||
|
||||
def set_engine(engine) -> None: # noqa: ANN001
|
||||
"""Set the engine reference used by all route handlers."""
|
||||
global _engine
|
||||
_engine = engine
|
||||
|
||||
|
||||
def _get_engine():
|
||||
"""Get the current engine or raise 503."""
|
||||
if _engine is None or not _engine.is_ready:
|
||||
raise HTTPException(status_code=503, detail="Specialist engine not ready")
|
||||
return _engine
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entity extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/entities", response_model=BatchResponse)
|
||||
async def extract_entities(request: ExtractionRequest) -> BatchResponse:
|
||||
"""Batch entity extraction — returns spans with character offsets and scores."""
|
||||
engine = _get_engine()
|
||||
|
||||
if len(request.texts) > engine.max_batch_size:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Batch size {len(request.texts)} exceeds maximum {engine.max_batch_size}",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
results = engine.extract_entities(request.texts, request.schema_labels)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
return BatchResponse(
|
||||
results=results,
|
||||
model_version=engine.model_name,
|
||||
schema_version="specialist-v1",
|
||||
processing_time_ms=round(elapsed_ms, 2),
|
||||
batch_id=request.batch_id,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/classify", response_model=BatchResponse)
|
||||
async def classify_events(request: ClassificationRequest) -> BatchResponse:
|
||||
"""Batch event classification — returns labels with confidence scores."""
|
||||
engine = _get_engine()
|
||||
|
||||
if len(request.texts) > engine.max_batch_size:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Batch size {len(request.texts)} exceeds maximum {engine.max_batch_size}",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
results = engine.classify_texts(request.texts, request.schema_labels)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
return BatchResponse(
|
||||
results=results,
|
||||
model_version=engine.model_name,
|
||||
schema_version="specialist-v1",
|
||||
processing_time_ms=round(elapsed_ms, 2),
|
||||
batch_id=request.batch_id,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Relation extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/relations", response_model=BatchResponse)
|
||||
async def extract_relations(request: ExtractionRequest) -> BatchResponse:
|
||||
"""Batch relation extraction — returns subject-relation-object triples with spans."""
|
||||
engine = _get_engine()
|
||||
|
||||
if len(request.texts) > engine.max_batch_size:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Batch size {len(request.texts)} exceeds maximum {engine.max_batch_size}",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
results = engine.extract_relations(request.texts, request.schema_labels)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
return BatchResponse(
|
||||
results=results,
|
||||
model_version=engine.model_name,
|
||||
schema_version="specialist-v1",
|
||||
processing_time_ms=round(elapsed_ms, 2),
|
||||
batch_id=request.batch_id,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structured extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/extract", response_model=BatchResponse)
|
||||
async def extract_structured(request: ExtractionRequest) -> BatchResponse:
|
||||
"""Structured fact extraction — returns key-value pairs with spans."""
|
||||
engine = _get_engine()
|
||||
|
||||
if len(request.texts) > engine.max_batch_size:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Batch size {len(request.texts)} exceeds maximum {engine.max_batch_size}",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
results = engine.extract_structured(request.texts, request.schema_labels)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
return BatchResponse(
|
||||
results=results,
|
||||
model_version=engine.model_name,
|
||||
schema_version="specialist-v1",
|
||||
processing_time_ms=round(elapsed_ms, 2),
|
||||
batch_id=request.batch_id,
|
||||
)
|
||||
Reference in New Issue
Block a user