From 0aa1d9fa82af78698a52d4139936b4951678a589 Mon Sep 17 00:00:00 2001 From: Adam Ridene Date: Sun, 19 Jul 2026 02:21:32 +0100 Subject: [PATCH] refactor: make manual operator reporting the primary fault ingestion path Legacy machines without digital connectivity are the norm in mid-size plants. Previously the system treated MQTT as the default and manual input as a demo simulation; this inverts that model. Changes ------- - source field on WorkOrder and FaultIn, defaulting to 'manual' - ManualFaultForm always visible, machine as + + + + +
+ + +
+
+ +
+ + +
+ + + + +
+
+
Fault reported
+
Diagnosis in progress. Work order will be queued for supervisor review.
+ Report another fault +
+ + + + + + \ No newline at end of file diff --git a/src/torq/pipeline.py b/src/torq/pipeline.py index 0134456..d982c74 100644 --- a/src/torq/pipeline.py +++ b/src/torq/pipeline.py @@ -4,6 +4,7 @@ from torq.agent.diagnose import diagnose from torq.agent.schemas import WorkOrder +from torq.db import models from torq.dispatch import approval from torq.events import live from torq.workorder.generate import build_work_order @@ -15,13 +16,19 @@ def handle_fault( context: str = "", translate: bool = True, fault_arrived_at: str | None = None, + source: str = "manual", ) -> WorkOrder: """Fault event -> diagnosis -> work order -> queued for supervisor approval.""" if fault_arrived_at is None: fault_arrived_at = datetime.now(timezone.utc).isoformat() + + existing = models.find_recent(machine, fault_code) if machine else None + if existing: + return existing + live.push_activity("fault_received", machine, fault_code) live.push_activity("diagnosing", machine, fault_code, detail="reading manuals + repair history") diag = diagnose(fault_code, machine, context) - wo = build_work_order(diag, translate=translate, fault_arrived_at=fault_arrived_at) + wo = build_work_order(diag, translate=translate, fault_arrived_at=fault_arrived_at, source=source) live.push_activity("work_order_created", machine, fault_code, detail=diag.root_cause, wo_id=wo.id) return approval.submit(wo) diff --git a/src/torq/workorder/generate.py b/src/torq/workorder/generate.py index 660efda..d9994ca 100644 --- a/src/torq/workorder/generate.py +++ b/src/torq/workorder/generate.py @@ -68,7 +68,8 @@ def _translate(en_text: str) -> dict[str, str]: def build_work_order( - diag: Diagnosis, translate: bool = True, fault_arrived_at: str | None = None + diag: Diagnosis, translate: bool = True, fault_arrived_at: str | None = None, + source: str = "manual", ) -> WorkOrder: en = _render_en(diag) content = {"en": en} @@ -88,5 +89,6 @@ def build_work_order( investigation=diag.investigation, content=content, confidence=diag.confidence, + source=source, fault_arrived_at=fault_arrived_at, ) diff --git a/uv.lock b/uv.lock index 9817848..051dfa8 100644 --- a/uv.lock +++ b/uv.lock @@ -204,6 +204,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + [[package]] name = "certifi" version = "2026.6.17" @@ -2908,6 +2917,7 @@ name = "torq" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "cachetools" }, { name = "chonkie" }, { name = "fastapi" }, { name = "fpdf2" }, @@ -2933,6 +2943,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "cachetools", specifier = ">=5.5.0" }, { name = "chonkie", specifier = ">=1.7.0" }, { name = "fastapi", specifier = ">=0.139.2" }, { name = "fpdf2", specifier = ">=2.8.7" }, diff --git a/web/src/api.js b/web/src/api.js index c6a710f..52bc52a 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -43,4 +43,6 @@ export const recordOutcome = (id, body) => body: JSON.stringify(body), }); +export const getMachines = () => j("/machines").catch(() => []); + export const getHealth = () => j("/health").catch(() => null); diff --git a/web/src/i18n.jsx b/web/src/i18n.jsx index 7b55a8a..be9f1c3 100644 --- a/web/src/i18n.jsx +++ b/web/src/i18n.jsx @@ -33,11 +33,11 @@ const LOCALES = { "landing.feature_4_title": "Self Learning", "landing.feature_4_desc": "Every fix enriches the knowledge base. The model gets smarter with each resolved fault.", "landing.feature_5_title": "Works With Your Setup", - "landing.feature_5_desc": "Report faults from the dashboard, connect machines that send digital alerts, or bridge older equipment — no special hardware required.", + "landing.feature_5_desc": "Report faults from the dashboard — select a machine, enter the code, describe what you see. No sensors, no PLCs, no hardware required.", "landing.how_title": "How it works", "landing.how_subtitle": "Three steps from alarm to resolution.", "landing.step_1_title": "A fault comes in", - "landing.step_1_desc": "A technician reports it from the dashboard, a connected machine sends an alert, or a bridge picks it up from older controllers. TORQ starts immediately, no matter how the fault arrives.", + "landing.step_1_desc": "A floor operator reports it from the dashboard — select the machine, enter the fault code, describe what you see. TORQ starts immediately. No sensors, no PLCs, no hardware required.", "landing.step_2_title": "AI diagnoses against manuals + history", "landing.step_2_desc": "Hybrid search (dense + BM25 + reranker) retrieves relevant documentation. A reasoning LLM produces a grounded diagnosis with citations.", "landing.step_3_title": "Supervisor approves → technician routed", @@ -98,7 +98,7 @@ const LOCALES = { "faq.q3": "What machines are supported?", "faq.a3": "Any machine. If your equipment sends digital alerts, TORQ can pick them up. If your machines are fully offline, operators can submit faults directly through the dashboard form or from any connected device.", "faq.q4": "How do I integrate TORQ with my plant?", - "faq.a4": "Four ways: (1) Use the dashboard form to report a fault manually. (2) Send fault data from your existing maintenance or ERP system. (3) Connect machines that already produce digital fault signals. (4) Run a small bridge on a device like a Raspberry Pi to connect older controllers \u2014 no special hardware required.", + "faq.a4": "Three tiers, no hardware needed for Tier 1: (1) Use the dashboard form to report a fault manually \u2014 select a machine, enter the code, describe what you see. (2) Connect machines that publish MQTT fault events (a USD 20 ESP32 bridge adapts legacy equipment). (3) Use the REST API to integrate with your existing CMMS, ERP, or custom maintenance tool.", "faq.q5": "Is my data secure?", "faq.a5": "TORQ runs on your infrastructure. LLM calls go to your endpoint. Vector DB can be self-hosted. No data leaves your network.", "faq.q6": "What languages are supported?", @@ -130,12 +130,14 @@ const LOCALES = { "dashboard.close": "close", "dashboard.download_pdf": "Download PDF (EN/FR/AR)", "dashboard.grounded_in": "Grounded in", - "dashboard.simulate": "Simulate fault (E-471, CM-350 Line 2)", "dashboard.report_fault": "Report a fault", - "dashboard.machine_placeholder": "Machine (e.g. CM-350 Line 2)", + "dashboard.manual_form_hint": "Select a machine, enter the fault code, and describe what you see or hear.", + "dashboard.machine_placeholder": "Select machine…", "dashboard.fault_code_placeholder": "Fault code (e.g. E-471)", "dashboard.context_placeholder": "Context / observations (optional)", "dashboard.submit_fault": "Submit fault", + "dashboard.operator_link": "Mobile report", + "dashboard.operator_link_hint": "Open on any phone →", "dashboard.no_trend_data": "No trend data yet", "dashboard.no_machine_data": "No machine data yet", "dashboard.no_recent_faults": "No recent faults detected", @@ -179,7 +181,7 @@ const LOCALES = { "dashboard.toast_dispatched": "Dispatched to", "dashboard.toast_rejected": "Rejected", "dashboard.toast_fixed": "Marked as fixed", - "dashboard.toast_simulated": "Fault simulated", + "dashboard.toast_fault_reported": "Fault reported", "dashboard.stage_fault_received": "Fault detected", "dashboard.stage_diagnosing": "Diagnosing", "dashboard.stage_work_order_created": "Work order ready", @@ -337,12 +339,14 @@ const LOCALES = { "dashboard.close": "fermer", "dashboard.download_pdf": "Télécharger PDF (EN/FR/AR)", "dashboard.grounded_in": "Basé sur", - "dashboard.simulate": "Simuler défaut (E-471, CM-350 Line 2)", "dashboard.report_fault": "Signaler un défaut", - "dashboard.machine_placeholder": "Machine (ex. CM-350 Line 2)", + "dashboard.manual_form_hint": "Sélectionnez une machine, saisissez le code défaut, et décrivez ce que vous voyez ou entendez.", + "dashboard.machine_placeholder": "Sélectionner une machine…", "dashboard.fault_code_placeholder": "Code défaut (ex. E-471)", "dashboard.context_placeholder": "Contexte / observations (optionnel)", "dashboard.submit_fault": "Soumettre le défaut", + "dashboard.operator_link": "Signalement mobile", + "dashboard.operator_link_hint": "Ouvrir sur un téléphone →", "dashboard.no_trend_data": "Aucune donnée de tendance", "dashboard.no_machine_data": "Aucune donnée machine", "dashboard.no_recent_faults": "Aucun défaut récent détecté", @@ -386,7 +390,7 @@ const LOCALES = { "dashboard.toast_dispatched": "Envoyé à", "dashboard.toast_rejected": "Rejeté", "dashboard.toast_fixed": "Marqué comme réparé", - "dashboard.toast_simulated": "Défaut simulé", + "dashboard.toast_fault_reported": "Défaut signalé", "dashboard.stage_fault_received": "Défaut détecté", "dashboard.stage_diagnosing": "Diagnostic en cours", "dashboard.stage_work_order_created": "Ordre de travail prêt", @@ -544,12 +548,14 @@ const LOCALES = { "dashboard.close": "إغلاق", "dashboard.download_pdf": "تنزيل PDF (EN/FR/AR)", "dashboard.grounded_in": "مستند على", - "dashboard.simulate": "محاكاة خلل (E-471, CM-350 Line 2)", "dashboard.report_fault": "الإبلاغ عن خلل", - "dashboard.machine_placeholder": "الآلة (مثل CM-350 Line 2)", + "dashboard.manual_form_hint": "اختر آلة، أدخل رمز الخلل، وصف ما تراه أو تسمعه.", + "dashboard.machine_placeholder": "اختر آلة…", "dashboard.fault_code_placeholder": "رمز الخلل (مثل E-471)", "dashboard.context_placeholder": "السياق / الملاحظات (اختياري)", "dashboard.submit_fault": "إرسال الخلل", + "dashboard.operator_link": "إبلاغ من الهاتف", + "dashboard.operator_link_hint": "فتح على أي هاتف →", "dashboard.no_trend_data": "لا توجد بيانات اتجاه بعد", "dashboard.no_machine_data": "لا توجد بيانات آلة بعد", "dashboard.no_recent_faults": "لم يتم اكتشاف أعطال حديثة", @@ -593,7 +599,7 @@ const LOCALES = { "dashboard.toast_dispatched": "تم الإرسال إلى", "dashboard.toast_rejected": "مرفوض", "dashboard.toast_fixed": "تم الإصلاح", - "dashboard.toast_simulated": "تم محاكاة الخلل", + "dashboard.toast_fault_reported": "تم الإبلاغ عن الخلل", "dashboard.stage_fault_received": "تم اكتشاف العطل", "dashboard.stage_diagnosing": "جارٍ التشخيص", "dashboard.stage_work_order_created": "أمر العمل جاهز", diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index 4eb3312..291f655 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -19,18 +19,6 @@ const FIX = { time_to_fix_min: 30, }; -// Varied demo faults across skills (E=electromechanical, J=packaging, C/P/A=general) -// so repeated clicks exercise different machines and route to different techs. -const SIM_FAULTS = [ - { fault_code: "E-471", machine: "CM-350 Line 2", context: "Motor tripped after hours running." }, - { fault_code: "E-201", machine: "CM-350 Line 1", context: "Overcurrent on start, grinding noise." }, - { fault_code: "J-108", machine: "PK-9 Line 3", context: "Film feed jammed at the roller nip." }, - { fault_code: "J-233", machine: "PK-9 Line 3", context: "Seal temperature low, weak seals." }, - { fault_code: "C-207", machine: "Chiller 6", context: "Condenser water flow dropping, efficiency down." }, - { fault_code: "P-410", machine: "Pump 3", context: "High vibration and bearing noise." }, - { fault_code: "A-120", machine: "AHU 2", context: "Filter differential pressure over setpoint." }, -]; - function Tile({ label, value }) { return (
@@ -44,6 +32,12 @@ function Badge({ status }) { return {status}; } +function SourceBadge({ source }) { + if (!source) return null; + const icon = source === "manual" ? "👤" : "📡"; + return {icon} {source}; +} + // A work order is "open" until it reaches a terminal state. const OPEN_STATUS = new Set(["pending", "approved", "dispatched"]); const fmtDate = (iso) => (iso ? new Date(iso).toLocaleString() : "-"); @@ -548,35 +542,39 @@ function StatusFilter({ value, onChange, t }) { } function ManualFaultForm({ onReport, busy, t }) { - const [open, setOpen] = useState(false); const [machine, setMachine] = useState(""); const [faultCode, setFaultCode] = useState(""); const [context, setContext] = useState(""); + const [machines, setMachines] = useState([]); + + useEffect(() => { + api.getMachines().then(setMachines).catch(() => {}); + }, []); const handleSubmit = (e) => { e.preventDefault(); if (!machine || !faultCode) return; - onReport({ machine, fault_code: faultCode, context }); + onReport({ machine, fault_code: faultCode, context, source: "manual" }); setMachine(""); setFaultCode(""); setContext(""); - setOpen(false); }; return (
- - {open && ( -
- +
+ setFaultCode(e.target.value)} required /> -