Files
boc/iom/defect/defect_api.py
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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())}