Dan Vancea commited on
Commit
0336d0f
Β·
1 Parent(s): 0cb0c83

Update backend.py

Browse files
Files changed (1) hide show
  1. backend.py +175 -90
backend.py CHANGED
@@ -1,17 +1,22 @@
1
  import os
 
2
  import pickle
 
3
  import tempfile
4
  from flask import Flask, request, jsonify
5
  from flask_cors import CORS
6
  from dotenv import load_dotenv
7
  from faster_whisper import WhisperModel
8
  import anthropic
 
9
 
10
  load_dotenv()
11
 
12
  app = Flask(__name__)
13
  CORS(app)
14
 
 
 
15
  print("Loading Whisper model (base)…")
16
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
17
  print("Whisper ready.")
@@ -22,23 +27,6 @@ SYSTEM_PROMPT = """You are a concise diagnostic assistant for HP Metal Jet S100
22
  You receive real-time sensor data and answer operator questions in 1-3 short sentences.
23
  Be direct and technical. Always respond in English."""
24
 
25
- # ── Q-table for RL maintenance recommendations ────────────────────────────────
26
- _QTABLE: dict | None = None
27
- _QTABLE_PATH = os.path.join(os.path.dirname(__file__), "q_table.pkl")
28
-
29
- def _load_qtable() -> None:
30
- global _QTABLE
31
- try:
32
- with open(_QTABLE_PATH, "rb") as f:
33
- _QTABLE = pickle.load(f)
34
- n_states = len(_QTABLE["Q"])
35
- print(f"Q-table loaded ({n_states} visited states).")
36
- except FileNotFoundError:
37
- print("q_table.pkl not found β€” run phase2.py to train and save the Q-table.")
38
- _QTABLE = None
39
-
40
- _load_qtable()
41
-
42
 
43
  @app.route("/api/transcribe", methods=["POST"])
44
  def transcribe():
@@ -59,89 +47,186 @@ def transcribe():
59
  finally:
60
  os.unlink(tmp_path)
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  @app.route("/api/chat", methods=["POST"])
64
  def chat():
65
- data = request.json
66
- query = data.get("query", "")
67
- ctx = data.get("context", {})
68
-
69
- printer_ctx = (
70
- f"Printer: {ctx.get('name')} (ID: {ctx.get('id')}, location: {ctx.get('location')})\n"
71
- f"Overall health: {ctx.get('health')}% | Uptime 30d: {ctx.get('uptime')}% | Jobs since service: {ctx.get('jobs')}\n"
72
- f"Components:\n"
 
 
 
 
 
 
 
 
 
 
 
 
73
  )
74
- for c in ctx.get("components", []):
75
- printer_ctx += f" - {c['name']} ({c['subsystem']}): health {c['health']}%, trend 8w {c['trend8']:+}pts, predicted failure in {c['predictDays']}d\n"
76
 
77
- message = claude.messages.create(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  model="claude-haiku-4-5-20251001",
79
- max_tokens=250,
80
- system=SYSTEM_PROMPT,
81
- messages=[
82
- {"role": "user", "content": f"{printer_ctx}\nOperator question: {query}"}
83
- ],
 
 
 
 
84
  )
85
 
86
- answer = message.content[0].text
87
- print(f"\n[CLAUDE] Q: {query}\nA: {answer}\n")
88
  return jsonify({"text": answer})
89
 
90
 
91
- @app.route("/api/recommend", methods=["POST"])
92
- def recommend():
93
- """
94
- RL maintenance recommendation.
95
-
96
- Request body: { "health": [h0, h1, ..., h8] }
97
- health values are floats in [0, 1], one per component in OBS_COMPS order:
98
- recoater_blade, nozzle_plate, heating_elements, temperature_sensors,
99
- insulation_panels, firing_resistors, cleaning_interface, recoater_motor, linear_rail
100
-
101
- Response: {
102
- "action": int, // 0 = no-op, 1-9 = maintain component
103
- "component": str|null, // backend component key, null when action=0
104
- "reason": str
105
- }
106
- """
107
- if _QTABLE is None:
108
- # Try reloading in case phase2.py has been run since startup
109
- _load_qtable()
110
- if _QTABLE is None:
111
- return jsonify({
112
- "action": 0,
113
- "component": None,
114
- "reason": "Q-table not available β€” run phase2.py first",
115
- }), 200
116
-
117
- data = request.json or {}
118
- health = data.get("health", [])
119
- obs_comps = _QTABLE["OBS_COMPS"]
120
-
121
- if len(health) != len(obs_comps):
122
- return jsonify({
123
- "error": f"expected {len(obs_comps)} health values, got {len(health)}"
124
- }), 400
125
-
126
- N_BINS = _QTABLE["N_BINS"]
127
- state = tuple(min(N_BINS - 1, int(h * N_BINS)) for h in health)
128
-
129
- Q = _QTABLE["Q"]
130
- n_actions = _QTABLE["n_actions"]
131
- q_vals = Q.get(state, [0.0] * n_actions)
132
- action = int(q_vals.index(max(q_vals)))
133
-
134
- if action == 0:
135
- return jsonify({"action": 0, "component": None, "reason": "Q-table: no maintenance needed"})
136
-
137
- comp_name, _attr, recovery = _QTABLE["ACTIONS"][action]
138
- return jsonify({
139
- "action": action,
140
- "component": comp_name,
141
- "recovery": recovery,
142
- "reason": f"Q-table: state={state} β†’ action={action}",
143
- })
144
-
145
-
146
  if __name__ == "__main__":
147
  app.run(host="0.0.0.0", port=7860, debug=False)
 
1
  import os
2
+ import json
3
  import pickle
4
+ import re
5
  import tempfile
6
  from flask import Flask, request, jsonify
7
  from flask_cors import CORS
8
  from dotenv import load_dotenv
9
  from faster_whisper import WhisperModel
10
  import anthropic
11
+ from supabase import create_client
12
 
13
  load_dotenv()
14
 
15
  app = Flask(__name__)
16
  CORS(app)
17
 
18
+ supabase = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_KEY"])
19
+
20
  print("Loading Whisper model (base)…")
21
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
22
  print("Whisper ready.")
 
27
  You receive real-time sensor data and answer operator questions in 1-3 short sentences.
28
  Be direct and technical. Always respond in English."""
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  @app.route("/api/transcribe", methods=["POST"])
32
  def transcribe():
 
47
  finally:
48
  os.unlink(tmp_path)
49
 
50
+ #END STUFF
51
+
52
+ _SQL_SYSTEM = """You are a read-only PostgreSQL query generator for a fleet of HP Metal Jet S100 industrial 3D metal printers.
53
+ Your only job is to output a single valid SQL SELECT statement that fetches exactly the data needed to answer the operator's question.
54
+
55
+ DATABASE SCHEMA
56
+ ===============
57
+ printers(
58
+ id CHAR(20) PRIMARY KEY,
59
+ last_repair TIMESTAMP
60
+ )
61
+
62
+ snapshots(
63
+ id CHAR(20) REFERENCES printers(id), -- printer identifier
64
+ time_step_id INT, -- monotonically increasing step counter
65
+ recoater_blade FLOAT, -- Recoating System (0.0 = failed, 1.0 = perfect)
66
+ nozzle_plate FLOAT, -- Printhead Array
67
+ heating_elements FLOAT, -- Thermal Control
68
+ temperature_sensors FLOAT, -- Thermal Control
69
+ insulation_panels FLOAT, -- Thermal Control
70
+ firing_resistors FLOAT, -- Printhead Array
71
+ cleaning_interface FLOAT, -- Printhead Array
72
+ recoater_motor FLOAT, -- Recoating System
73
+ linear_rail FLOAT, -- Recoating System
74
+ PRIMARY KEY (id, time_step_id)
75
+ )
76
+
77
+ conditions(
78
+ id CHAR(20) REFERENCES printers(id),
79
+ timestamp TIMESTAMP,
80
+ ambient_temperature_c FLOAT, -- degrees Celsius
81
+ build_chamber_temp_c FLOAT, -- degrees Celsius
82
+ ambient_humidity_pct FLOAT, -- percentage 0-100
83
+ powder_contamination_level FLOAT, -- fraction 0-1
84
+ build_volume_cm3 FLOAT, -- cm3
85
+ recoating_speed_mm_s FLOAT, -- mm/s
86
+ maintenance_level FLOAT, -- fraction 0-1, higher = better maintained
87
+ PRIMARY KEY (id, timestamp)
88
+ )
89
+
90
+ SUBSYSTEM GROUPINGS
91
+ ===================
92
+ Thermal Control : heating_elements, temperature_sensors, insulation_panels
93
+ Printhead Array : nozzle_plate, firing_resistors, cleaning_interface
94
+ Recoating System : recoater_blade, recoater_motor, linear_rail
95
+
96
+ HEALTH THRESHOLDS (all snapshot columns use the same scale)
97
+ ===========================================================
98
+ >= 0.60 = healthy
99
+ 0.30 to 0.59 = warning
100
+ < 0.30 = critical
101
+
102
+ QUERY RULES
103
+ ===========
104
+ 1. Output ONLY the raw SQL β€” no markdown fences, no explanations, no semicolons.
105
+ 2. Always filter snapshots and conditions by id = '<printer_id>' unless the question explicitly asks for fleet-wide comparison.
106
+ 3. Current state β†’ SELECT all 9 health columns FROM snapshots WHERE id = '<printer_id>' ORDER BY time_step_id DESC LIMIT 1
107
+ 4. Trend / rate β†’ fetch the last N rows ORDER BY time_step_id ASC (use 50-100 rows for rate, 200+ for long-term trend).
108
+ 5. Anomaly β†’ compute AVG and STDDEV per component over a window, compare to latest value.
109
+ 6. Fleet queries β†’ omit the id filter, GROUP BY id, rank by computed health score.
110
+ 7. Maintenance β†’ join printers on id to access last_repair; join conditions on id for environmental context.
111
+ 8. Never use INSERT, UPDATE, DELETE, DROP, CREATE, TRUNCATE, or any DDL/DML.
112
+ 9. Default LIMIT 100; for full history queries up to LIMIT 500.
113
+ 10. When unsure which columns are relevant, select all 9 health columns β€” the answering model will filter.
114
+ 11. Use readable aliases: recoater_blade AS "Recoater Blade", etc."""
115
+
116
+ _ANSWER_SYSTEM = """You are the digital co-pilot for HP Metal Jet S100 industrial 3D metal printers.
117
+ You receive raw data fetched from a live sensor database and answer operator questions accurately and directly.
118
+
119
+ TONE AND LENGTH
120
+ ===============
121
+ - Floor operators need fast, actionable answers. Be direct and technical.
122
+ - Simple state questions: 1-2 sentences.
123
+ - Trend, anomaly, or multi-component questions: up to a short paragraph.
124
+ - Never pad with filler phrases like "Based on the data provided..." β€” start with the answer.
125
+ - Always respond in English.
126
+
127
+ SEVERITY TAG (mandatory β€” always open your response with one)
128
+ =============================================================
129
+ [CRITICAL] β€” any component below 0.30, imminent failure predicted, or immediate action required
130
+ [WARNING] β€” any component between 0.30 and 0.59, accelerating degradation, or anomaly detected
131
+ [INFO] β€” all components healthy, routine query, or informational answer
132
+
133
+ COMPONENT AND SUBSYSTEM REFERENCE
134
+ ==================================
135
+ Thermal Control : heating_elements, temperature_sensors, insulation_panels
136
+ Printhead Array : nozzle_plate, firing_resistors, cleaning_interface
137
+ Recoating System : recoater_blade, recoater_motor, linear_rail
138
+
139
+ Health scale: 0.0 = completely failed, 1.0 = perfect condition
140
+ Thresholds : >= 0.60 healthy, 0.30-0.59 warning, < 0.30 critical
141
+
142
+ GROUNDING RULES (non-negotiable)
143
+ =================================
144
+ 1. Answer only from the data rows provided. Never use training knowledge to fill gaps.
145
+ 2. Cite the specific value and time_step_id or timestamp for every number you state.
146
+ 3. If the data is empty or NULL, say explicitly: "No data available for [component] β€” cannot answer."
147
+ 4. Never extrapolate predictions beyond the available data window.
148
+ 5. If the question is outside the scope of the telemetry, say so clearly.
149
+
150
+ RESPONSE PATTERNS BY QUESTION TYPE
151
+ ====================================
152
+ Current state β†’ state the health value(s), their severity, and what it means operationally.
153
+ Trend β†’ state direction (improving/degrading), magnitude (e.g. dropped 0.12 over 50 steps), and whether the rate is accelerating.
154
+ Anomaly β†’ name the component, the step where the drop occurred, the before/after values, and whether it is isolated or correlated with others.
155
+ Prediction β†’ state how many steps remain at current rate before crossing 0.30; do not guess beyond the data window.
156
+ Root cause β†’ trace the sequence: which component moved first, which followed, and whether conditions (temperature, contamination) correlate.
157
+ Fleet β†’ rank printers by overall health score; name the worst component across the fleet.
158
+ Action/priority β†’ rank by (lowest health + fastest degradation rate); name the single most urgent repair first.
159
+ Maintenance β†’ state the last_repair timestamp and how many steps have elapsed since then.
160
+ Aggregation β†’ provide the exact computed values (avg, min, max, stddev) with the time window they cover.
161
+ No data β†’ "No data available for [X]. Check that the printer ID is correct and that snapshots exist for this time range."
162
+ """
163
+
164
+
165
+ def _extract_sql(text: str) -> str:
166
+ text = text.strip()
167
+ # strip markdown code fences if the model adds them anyway
168
+ text = re.sub(r"^```[a-z]*\n?", "", text, flags=re.IGNORECASE)
169
+ text = re.sub(r"\n?```$", "", text)
170
+ return text.strip().rstrip(";")
171
+
172
 
173
  @app.route("/api/chat", methods=["POST"])
174
  def chat():
175
+ data = request.json or {}
176
+ prompt = data.get("prompt", "").strip()
177
+ printer_id = data.get("printer_id", "")
178
+
179
+ if not prompt:
180
+ return jsonify({"error": "prompt is required"}), 400
181
+
182
+ # ── Step 1: ask LLM to generate a SQL query for the needed data ──────────
183
+ sql_resp = claude.messages.create(
184
+ model="claude-haiku-4-5-20251001",
185
+ max_tokens=400,
186
+ system=_SQL_SYSTEM,
187
+ messages=[{
188
+ "role": "user",
189
+ "content": (
190
+ f"Database schema:\n{_SCHEMA_SUMMARY}\n\n"
191
+ f"printer_id: {printer_id}\n"
192
+ f"User question: {prompt}"
193
+ ),
194
+ }],
195
  )
196
+ sql = _extract_sql(sql_resp.content[0].text)
197
+ print(f"\n[SQL] {sql}\n")
198
 
199
+ # ── Step 2: run the query against Supabase ───────────────────────────────
200
+ try:
201
+ rpc_result = supabase.rpc("run_readonly_query", {"query_text": sql}).execute()
202
+ db_data = rpc_result.data
203
+ if isinstance(db_data, str):
204
+ db_data = json.loads(db_data)
205
+ db_text = json.dumps(db_data, indent=2)
206
+ except Exception as exc:
207
+ print(f"[DB ERROR] {exc}")
208
+ db_text = f"Query failed: {exc}"
209
+
210
+ print(f"[DB RESULT] {db_text[:500]}\n")
211
+
212
+ # ── Step 3: ask LLM to answer using the retrieved data ───────────────────
213
+ answer_resp = claude.messages.create(
214
  model="claude-haiku-4-5-20251001",
215
+ max_tokens=300,
216
+ system=_ANSWER_SYSTEM,
217
+ messages=[{
218
+ "role": "user",
219
+ "content": (
220
+ f"Data fetched from database:\n{db_text}\n\n"
221
+ f"Operator question: {prompt}"
222
+ ),
223
+ }],
224
  )
225
 
226
+ answer = answer_resp.content[0].text
227
+ print(f"[CLAUDE ANSWER] {answer}\n")
228
  return jsonify({"text": answer})
229
 
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  if __name__ == "__main__":
232
  app.run(host="0.0.0.0", port=7860, debug=False)