"""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, )