Spaces:
Runtime error
Runtime error
| """Validate model output and guarantee the UI never crashes. | |
| If the model returns malformed JSON, or names a fault that is not in the | |
| rule-derived candidate set, we fall back to the top deterministic candidate. | |
| The deterministic layer is the floor; the model can refine but not fabricate. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from dataclasses import dataclass | |
| from typing import List | |
| from fault_rules import Candidate | |
| VALID_URGENCY = {"CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"} | |
| class DiagnosisResult: | |
| fault: str | |
| urgency: str | |
| checks: List[str] | |
| safety: str | |
| confidence: int | |
| grounded: bool # True if the model's fault matched a candidate | |
| def to_dict(self) -> dict: | |
| return self.__dict__ | |
| def _extract_json(text: str) -> dict | None: | |
| if not text: | |
| return None | |
| match = re.search(r"\{.*\}", text, re.DOTALL) | |
| if not match: | |
| return None | |
| try: | |
| return json.loads(match.group()) | |
| except Exception: | |
| return None | |
| _FALLBACK_CANDIDATE = Candidate( | |
| name="Inconclusive", urgency="LOW", weight=0.0, | |
| evidence="No deterministic candidate was available.", | |
| ) | |
| # Hard caps so a runaway model response can never break the UI layout. | |
| MAX_FAULT_LEN = 80 | |
| MAX_TEXT_LEN = 240 | |
| def _clip(text: str, limit: int) -> str: | |
| text = " ".join(str(text).split()) # collapse whitespace/newlines | |
| return text if len(text) <= limit else text[: limit - 1].rstrip() + "…" | |
| def validate(raw_text: str, candidates: List[Candidate]) -> DiagnosisResult: | |
| # rank_candidates guarantees >=1, but never trust the caller blindly. | |
| top = candidates[0] if candidates else _FALLBACK_CANDIDATE | |
| candidates = candidates or [top] | |
| parsed = _extract_json(raw_text) | |
| if not isinstance(parsed, dict): | |
| return DiagnosisResult( | |
| fault=_clip(top.name, MAX_FAULT_LEN), urgency=top.urgency, | |
| checks=["Re-record a longer, clearer sample.", | |
| "Compare against the appliance's normal sound.", | |
| "If unsure, consult a technician."], | |
| safety="None", confidence=int(top.weight * 100), grounded=True, | |
| ) | |
| fault = str(parsed.get("fault", top.name)).strip() or top.name | |
| urgency = str(parsed.get("urgency", top.urgency)).strip().upper() | |
| if urgency not in VALID_URGENCY: | |
| urgency = top.urgency | |
| checks = parsed.get("checks", []) | |
| if not isinstance(checks, list) or not checks: | |
| checks = ["Inspect the most likely component first.", | |
| "Listen again after checking.", | |
| "Escalate to a technician if it persists."] | |
| checks = [_clip(c, MAX_TEXT_LEN) for c in checks if str(c).strip()][:3] | |
| if not checks: | |
| checks = ["Inspect the most likely component first."] | |
| safety = _clip(parsed.get("safety", "None"), MAX_TEXT_LEN) or "None" | |
| try: | |
| confidence = int(float(parsed.get("confidence", top.weight * 100))) | |
| except (TypeError, ValueError): | |
| confidence = int(top.weight * 100) | |
| confidence = max(0, min(100, confidence)) | |
| candidate_names = {c.name.lower() for c in candidates} | |
| grounded = fault.lower() in candidate_names or top.name == "Inconclusive" | |
| if not grounded: | |
| # Model named something outside the evidence — snap back to the floor. | |
| fault = top.name | |
| urgency = top.urgency | |
| confidence = min(confidence, int(top.weight * 100)) | |
| grounded = True | |
| # An Inconclusive verdict should never carry HIGH/CRITICAL urgency. | |
| if fault.lower() == "inconclusive": | |
| urgency = "LOW" | |
| return DiagnosisResult( | |
| fault=_clip(fault, MAX_FAULT_LEN), urgency=urgency, checks=checks, | |
| safety=safety, confidence=confidence, grounded=grounded, | |
| ) | |