diff --git a/src/torq/agent/diagnose.py b/src/torq/agent/diagnose.py index f7ed10f..9370812 100644 --- a/src/torq/agent/diagnose.py +++ b/src/torq/agent/diagnose.py @@ -252,7 +252,18 @@ def diagnose(fault_code: str, machine: str = "", context: str = "") -> Diagnosis diag = _diagnose_react(fault_code, machine, context) except Exception: # noqa: BLE001 - any agent failure degrades to one-shot log.warning("ReAct diagnosis failed, falling back to single-shot", exc_info=True) - diag = _diagnose_oneshot(fault_code, machine, context) + try: + diag = _diagnose_oneshot(fault_code, machine, context) + except Exception: # noqa: BLE001 - LLM/retrieval down: degrade, never 500 the demo + log.error("Diagnosis unavailable (LLM/retrieval down), returning stub", exc_info=True) + diag = Diagnosis( + fault_code=fault_code, + machine=machine, + root_cause="Automated diagnosis unavailable - needs manual review", + confidence=0.0, + repair_steps=["Escalate to a technician for manual diagnosis."], + investigation=["Diagnosis service unreachable; work order created for manual handling."], + ) if ttl > 0: _CACHE[key] = (time.monotonic() + ttl, diag.model_copy(deep=True)) diff --git a/src/torq/api/routes.py b/src/torq/api/routes.py index 231e81c..ef4474e 100644 --- a/src/torq/api/routes.py +++ b/src/torq/api/routes.py @@ -115,6 +115,16 @@ def reject(wo_id: str): return wo +@router.post("/work-orders/{wo_id}/notify") +def notify_work_order(wo_id: str): + """Manually (re)send the work order to its matched technician via WhatsApp.""" + res = approval.notify_technician(wo_id) + if not res: + raise HTTPException(404, "work order not found") + wo, delivery = res + return {"work_order": wo, "delivery": delivery} + + @router.post("/work-orders/{wo_id}/outcome") def outcome(wo_id: str, o: OutcomeIn): wo = feedback.record_outcome( diff --git a/src/torq/dispatch/approval.py b/src/torq/dispatch/approval.py index 04a4dde..0437cdc 100644 --- a/src/torq/dispatch/approval.py +++ b/src/torq/dispatch/approval.py @@ -59,3 +59,33 @@ def approve(wo_id: str) -> tuple[WorkOrder, dict] | None: detail=f"{delivery.get('channel', '?')} to {tech.get('name', '?')}", wo_id=wo.id, ) return wo, delivery + + +def notify_technician(wo_id: str) -> tuple[WorkOrder, dict] | None: + """Manually (re)send a work order's WhatsApp to its matched technician. + + On-demand send for the supervisor/demo: works on any work order regardless + of status, so a message can be pushed (or re-pushed) at will. + """ + wo = models.get(wo_id) + if not wo: + return None + tech = routing.choose_technician(wo) + if not tech: + live.push_activity( + "dispatch_failed", wo.machine, wo.fault_code, + detail="no technician on shift", wo_id=wo.id, + ) + return wo, {"channel": "none", "error": "no technician available"} + + delivery = notify.dispatch(wo, tech) + wo.assigned_to = tech.get("name") + if wo.status in ("pending", "approved"): + wo.status = "dispatched" + wo.dispatched_at = _now() + models.save(wo) + live.push_activity( + "dispatched", wo.machine, wo.fault_code, + detail=f"{delivery.get('channel', '?')} to {tech.get('name', '?')}", wo_id=wo.id, + ) + return wo, delivery diff --git a/web/src/api.js b/web/src/api.js index 6e6ef5b..2b580c3 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -29,6 +29,7 @@ export const reportFault = (body) => export const approve = (id) => j(`/work-orders/${id}/approve`, { method: "POST" }); export const reject = (id) => j(`/work-orders/${id}/reject`, { method: "POST" }); +export const notify = (id) => j(`/work-orders/${id}/notify`, { method: "POST" }); export const recordOutcome = (id, body) => j(`/work-orders/${id}/outcome`, { method: "POST", diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index ef3b5e6..62458e5 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -386,7 +386,7 @@ function FaultsPerMachineChart({ data, t }) { ); } -function Drawer({ workOrder, onClose, t }) { +function Drawer({ workOrder, onClose, onNotify, busy, t }) { const w = workOrder; if (!w) return null; @@ -420,6 +420,10 @@ function Drawer({ workOrder, onClose, t }) { {t("dashboard.download_pdf")} + + {w.root_cause &&
{w.root_cause}
} {w.repair_steps && ( @@ -797,7 +801,13 @@ export default function Dashboard() { -