""" defect_api.py — IOM Lager 7: FastAPI endpoints för felkoder REST API för: - Lista och söka felkoder - Klassificera observationer - Validera koder """ from __future__ import annotations from typing import Dict, List, Optional from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel, Field from defect_codes import REGISTRY, DefectCode from defect_classifier import CLASSIFIER, ClassificationResult app = FastAPI( title="IOM Lager 7 — Felklassificering", description="Hierarkiskt felkodsregister med AI-klassificering", version="1.0.0", ) # ────────────────────────────────────────────── # Schemas # ────────────────────────────────────────────── class DefectCodeOut(BaseModel): code: str name: str level: int parent: Optional[str] = None severity_hint: Optional[int] = None class Config: from_attributes = True class DefectHierarchyOut(BaseModel): code: str name: str severity_hint: Optional[int] children: Dict[str, Dict] class ClassificationRequest(BaseModel): text: str = Field(..., min_length=1, description="Beskrivning av observation") lang: str = Field(default="sv", description="Språk för respons") top_k: int = Field(default=3, ge=1, le=10) min_confidence: float = Field(default=0.3, ge=0.0, le=1.0) class ClassificationAlternative(BaseModel): code: str name: Optional[str] confidence: float class ClassificationResponse(BaseModel): code: str name: Optional[str] confidence: float matched_keyword: str severity_hint: Optional[int] alternatives: List[ClassificationAlternative] class ValidateRequest(BaseModel): code: str = Field(..., pattern=r"^\d{4}$") class ValidateResponse(BaseModel): valid: bool code: str name: Optional[str] warning: Optional[str] = None class BatchClassifyRequest(BaseModel): texts: List[str] = Field(..., min_length=1) lang: str = "sv" top_k: int = 3 min_confidence: float = 0.3 # ────────────────────────────────────────────── # Helpers # ────────────────────────────────────────────── def _to_defect_out(code: DefectCode, lang: str = "sv") -> DefectCodeOut: return DefectCodeOut( code=code.code, name=code.name(lang), level=code.level, parent=code.parent, severity_hint=code.severity_hint, ) # ────────────────────────────────────────────── # Endpoints # ────────────────────────────────────────────── @app.get("/", tags=["meta"]) async def root() -> Dict: return { "service": "IOM Lager 7 — Felklassificering", "version": "1.0.0", "codes_total": len(REGISTRY.all_codes()), "categories": len(REGISTRY.categories()), } @app.get("/codes", response_model=List[DefectCodeOut], tags=["koder"]) async def list_codes( lang: str = Query(default="sv", description="Språk för namn"), level: Optional[int] = Query(default=None, ge=1, le=2), parent: Optional[str] = Query(default=None, pattern=r"^\d{4}$"), ) -> List[DefectCodeOut]: """Lista alla felkoder, med filtrering på nivå eller förälder.""" codes = REGISTRY.all_codes() if level is not None: codes = [c for c in codes if c.level == level] if parent is not None: codes = [c for c in codes if c.parent == parent] return [_to_defect_out(c, lang) for c in codes] @app.get("/codes/hierarchy", response_model=Dict[str, DefectHierarchyOut], tags=["koder"]) async def get_hierarchy(lang: str = Query(default="sv")) -> Dict: """Hämta hierarkin av felkoder.""" return REGISTRY.to_dict(lang) @app.get("/codes/{code}", response_model=DefectCodeOut, tags=["koder"]) async def get_code(code: str, lang: str = Query(default="sv")) -> DefectCodeOut: """Hämta en specifik felkod.""" defect = REGISTRY.get(code) if not defect: raise HTTPException(status_code=404, detail=f"Felkod {code} hittades inte") return _to_defect_out(defect, lang) @app.get("/codes/{code}/children", response_model=List[DefectCodeOut], tags=["koder"]) async def get_children(code: str, lang: str = Query(default="sv")) -> List[DefectCodeOut]: """Hämta underkoder till en given kod.""" if code not in REGISTRY: raise HTTPException(status_code=404, detail=f"Felkod {code} hittades inte") children = REGISTRY.children(code) return [_to_defect_out(c, lang) for c in children] @app.get("/codes/search", response_model=List[DefectCodeOut], tags=["koder"]) async def search_codes( q: str = Query(..., min_length=1, description="Sökfras"), lang: str = Query(default="sv"), ) -> List[DefectCodeOut]: """Sök felkoder på namn.""" results = REGISTRY.search(q, lang) return [_to_defect_out(c, lang) for c in results] @app.get("/codes/severity/{severity}", response_model=List[DefectCodeOut], tags=["koder"]) async def by_severity( severity: int, lang: str = Query(default="sv"), ) -> List[DefectCodeOut]: """Hämta felkoder med en viss severity_hint.""" if not 1 <= severity <= 5: raise HTTPException(status_code=400, detail="severity måste vara 1-5") results = REGISTRY.by_severity(severity) return [_to_defect_out(c, lang) for c in results] # ── Klassificering ────────────────────────── @app.post("/classify", response_model=ClassificationResponse, tags=["klassificering"]) async def classify(request: ClassificationRequest) -> ClassificationResponse: """Klassificera en textbeskrivning till felkod.""" result = CLASSIFIER.classify( text=request.text, top_k=request.top_k, min_confidence=request.min_confidence, ) if not result: raise HTTPException( status_code=404, detail="Ingen matchande felkod hittades för given beskrivning", ) return ClassificationResponse(**result.to_dict(request.lang)) @app.post("/classify/batch", response_model=List[Optional[ClassificationResponse]], tags=["klassificering"]) async def classify_batch(request: BatchClassifyRequest) -> List: """Klassificera flera texter i en batch.""" results = CLASSIFIER.classify_batch( texts=request.texts, top_k=request.top_k, min_confidence=request.min_confidence, ) return [ ClassificationResponse(**r.to_dict(request.lang)) if r else None for r in results ] # ── Validering ────────────────────────────── @app.post("/validate", response_model=ValidateResponse, tags=["validering"]) async def validate(request: ValidateRequest) -> ValidateResponse: """Validera att en felkod finns och är lämplig.""" valid, warning = CLASSIFIER.validate_code(request.code) defect = REGISTRY.get(request.code) return ValidateResponse( valid=valid, code=request.code, name=defect.name("sv") if defect else None, warning=warning, ) # ── Health ────────────────────────────────── @app.get("/health", tags=["meta"]) async def health() -> Dict: return {"status": "ok", "codes_loaded": len(REGISTRY.all_codes())}