Spaces:
Runtime error
Runtime error
Download api/routes/classify.py from Satyam311/maintenance-classifier-api: direct link, hf CLI and curl.
- Browser
- Download file 4.95 kB
-
https://huggingface.co/spaces/Satyam311/maintenance-classifier-api/resolve/main/api/routes/classify.py
- Command line
-
hf download hf://spaces/Satyam311/maintenance-classifier-api/api/routes/classify.py
-
curl -L -o classify.py https://huggingface.co/spaces/Satyam311/maintenance-classifier-api/resolve/main/api/routes/classify.py
4.95 kB
| """ | |
| POST /classify — Ticket 8, extended for Ticket 13's correction flow. | |
| Every classified narrative is persisted to `reports` (with predictions cached), even | |
| if it's freshly pasted text rather than an ingested ASRS report. This gives the | |
| frontend a report_id to submit corrections against -- Ticket 13 explicitly requires | |
| "correction flow works end-to-end," which isn't possible without something to | |
| correct against. Ad-hoc submissions get an "adhoc-<uuid>" id, clearly distinguishable | |
| from real ASRS accession numbers. | |
| Error handling follows the Security & Access doc's table exactly: | |
| - Model service unavailable -> 503, no raw stack trace | |
| - Empty/near-empty narrative -> 400 with the specified message | |
| - Narrative exceeds max token length -> truncate, return a visible warning | |
| - Low-confidence prediction -> still returned (not hidden/blocked), confidence | |
| exposed so the frontend can flag it (Ticket 13's job, not this endpoint's) | |
| """ | |
| import logging | |
| import sqlite3 | |
| import uuid | |
| from fastapi import APIRouter, HTTPException | |
| from pydantic import BaseModel, Field | |
| from api.config import LOW_CONFIDENCE_THRESHOLD, MIN_NARRATIVE_WORDS, DATABASE_URL | |
| from api.services.model_service import model_service | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter() | |
| class ClassifyRequest(BaseModel): | |
| narrative_text: str = Field(..., description="Raw maintenance/incident narrative text") | |
| class OtherPossibleSystem(BaseModel): | |
| label: str | |
| confidence: float | |
| class ClassifyResponse(BaseModel): | |
| report_id: str | |
| ata_chapter: str | |
| ata_confidence: float | |
| ata_other_possible: list[OtherPossibleSystem] | |
| severity: str | |
| severity_confidence: float | |
| ata_low_confidence: bool | |
| severity_low_confidence: bool | |
| warnings: list[str] = [] | |
| def _persist_classification(narrative_text: str, result: dict) -> str: | |
| """Saves the narrative + predictions, returns the asrs_report_id to use for corrections. | |
| If this exact text was already classified before, reuses that row instead of duplicating.""" | |
| db_path = DATABASE_URL.replace("sqlite:///", "") | |
| conn = sqlite3.connect(db_path) | |
| existing = conn.execute( | |
| "SELECT asrs_report_id FROM reports WHERE narrative_text = ?", (narrative_text,) | |
| ).fetchone() | |
| if existing: | |
| report_id = existing[0] | |
| conn.execute( | |
| "UPDATE reports SET predicted_ata_chapter=?, predicted_severity=?, " | |
| "ata_confidence=?, severity_confidence=? WHERE asrs_report_id=?", | |
| (result["ata_chapter"], result["severity"], result["ata_confidence"], | |
| result["severity_confidence"], report_id), | |
| ) | |
| else: | |
| report_id = f"adhoc-{uuid.uuid4().hex[:12]}" | |
| conn.execute( | |
| """INSERT INTO reports | |
| (asrs_report_id, narrative_text, predicted_ata_chapter, predicted_severity, | |
| ata_confidence, severity_confidence) | |
| VALUES (?, ?, ?, ?, ?, ?)""", | |
| (report_id, narrative_text, result["ata_chapter"], result["severity"], | |
| result["ata_confidence"], result["severity_confidence"]), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return report_id | |
| def classify(request: ClassifyRequest): | |
| text = request.narrative_text.strip() | |
| # Security doc: "Empty or near-empty narrative submitted" | |
| word_count = len(text.split()) | |
| if word_count < MIN_NARRATIVE_WORDS: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Please enter a report narrative (at least a few words) to classify.", | |
| ) | |
| # Security doc: "Model service unavailable / fails to load" | |
| if not model_service.loaded: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Classification service temporarily unavailable — please try again shortly.", | |
| ) | |
| try: | |
| result = model_service.predict(text) | |
| except Exception: | |
| logger.exception("Unexpected error during classification") | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Classification service temporarily unavailable — please try again shortly.", | |
| ) | |
| report_id = _persist_classification(text, result) | |
| warnings = [] | |
| if result["text_was_truncated"]: | |
| warnings.append( | |
| "Your text was shortened to fit the model's input limit; classification may be less accurate." | |
| ) | |
| return ClassifyResponse( | |
| report_id=report_id, | |
| ata_chapter=result["ata_chapter"], | |
| ata_confidence=result["ata_confidence"], | |
| ata_other_possible=result["ata_other_possible"], | |
| severity=result["severity"], | |
| severity_confidence=result["severity_confidence"], | |
| ata_low_confidence=result["ata_confidence"] < LOW_CONFIDENCE_THRESHOLD, | |
| severity_low_confidence=result["severity_confidence"] < LOW_CONFIDENCE_THRESHOLD, | |
| warnings=warnings, | |
| ) | |