bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
"""
|
|
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())}
|