DigitalTransG2 commited on
Commit
b348b42
·
verified ·
1 Parent(s): 178fbc3

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +791 -0
app.py ADDED
@@ -0,0 +1,791 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ """
3
+ Streamlit First Aid Assistant
4
+ - Single-file Streamlit app
5
+ - Saves victim records to ./data/victims.json
6
+ - Uses OPENAI_API_KEY (env var) or temporary UI key in session (not persisted)
7
+ - Adjusted for safer defaults and improved UI/UX
8
+ """
9
+
10
+ import os
11
+ import json
12
+ import traceback
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ import streamlit as st
16
+
17
+
18
+ # -----------------------
19
+ # Paths / Configuration
20
+ # -----------------------
21
+ APP_DIR = Path.cwd()
22
+ DATA_DIR = APP_DIR / "data"
23
+ DATA_FILE = DATA_DIR / "victims.json"
24
+ NEXT_STEP_FILE = DATA_DIR / "next_step.json"
25
+ STREAMLIT_ROOT_CONFIG = Path("/root/.streamlit/config.toml")
26
+
27
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
28
+
29
+ # -----------------------
30
+ # Simple first-aid content
31
+ # -----------------------
32
+ SNAKEBITE_STEPS = [
33
+ "Keep the victim calm and still — limit movement.",
34
+ "Call emergency services immediately.",
35
+ "Position the affected limb at or slightly below heart level.",
36
+ "Remove tight clothing or jewelry near the bite area.",
37
+ "Clean the wound gently with soap and water; do NOT cut or suck the bite.",
38
+ "Cover with a clean, loose dressing. Monitor breathing and consciousness.",
39
+ ]
40
+
41
+ NORMAL_WOUND_STEPS = [
42
+ "Wash your hands if possible.",
43
+ "Apply gentle pressure with a clean cloth to stop bleeding.",
44
+ "Rinse the wound with clean water (do not use strong antiseptics directly).",
45
+ "Apply an antiseptic wipe or ointment if available.",
46
+ "Cover with a sterile dressing or bandage.",
47
+ "If bleeding continues or wound is deep, seek medical help.",
48
+ ]
49
+
50
+
51
+ def get_steps(wound_type: str):
52
+ return SNAKEBITE_STEPS if wound_type == "poisoned" else NORMAL_WOUND_STEPS
53
+ # -----------------------
54
+ # Firebase (Pyrebase4) configuration
55
+ # -----------------------
56
+ import pyrebase
57
+
58
+ # Lấy config từ secrets.toml
59
+ if "firebase" in st.secrets:
60
+ fb_cfg = st.secrets["firebase"]
61
+ firebase_config = {
62
+ "apiKey": fb_cfg["apiKey"],
63
+ "authDomain": fb_cfg["authDomain"],
64
+ "projectId": fb_cfg["projectId"],
65
+ "storageBucket": fb_cfg["storageBucket"],
66
+ "messagingSenderId": fb_cfg["messagingSenderId"],
67
+ "appId": fb_cfg["appId"],
68
+ "measurementId": fb_cfg["measurementId"],
69
+ "databaseURL": fb_cfg["databaseURL"]
70
+ }
71
+ firebase = pyrebase.initialize_app(firebase_config)
72
+ db = firebase.database()
73
+ else:
74
+ st.error("Firebase configuration not found in secrets.toml")
75
+ db = None
76
+
77
+
78
+ # -----------------------
79
+ # Read latest wound data
80
+ # -----------------------
81
+ def get_latest_wound_data():
82
+ if not db:
83
+ return None
84
+ try:
85
+ data = db.child("data").get().val()
86
+ if not data:
87
+ return None
88
+ # ✅ Nếu data là dict và chứa key "Wound" trực tiếp
89
+ if "Wound" in data:
90
+ return data
91
+ # ✅ Nếu có nhiều bản ghi (record_001, record_002, ...)
92
+ elif isinstance(data, dict):
93
+ latest_key = list(data.keys())[-1]
94
+ return data[latest_key]
95
+ return data
96
+ except Exception as e:
97
+ st.error(f"Error reading Firebase data: {e}")
98
+ return None
99
+
100
+
101
+ # -----------------------
102
+ # Auto-display section
103
+ # -----------------------
104
+ st.set_page_config(page_title="First Aid Assistant (Firebase)", layout="wide")
105
+ st.title("🩺 First Aid Assistant — Firebase Connected")
106
+
107
+ data = get_latest_wound_data()
108
+
109
+ if isinstance(data, str):
110
+ try:
111
+ # cố gắng parse nếu nó là JSON string
112
+ data = json.loads(data)
113
+ except Exception:
114
+ st.error("⚠️ Firebase returned unexpected string data.")
115
+ data = None
116
+
117
+ if data and isinstance(data, dict):
118
+ st.subheader("📡 Latest record from Firebase")
119
+ st.json(data)
120
+
121
+ wound_type = str(data.get("Wound", "")).strip().lower()
122
+ st.markdown("---")
123
+
124
+ if wound_type == "snakebite":
125
+ st.subheader("🐍 Detected case: Snakebite")
126
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(SNAKEBITE_STEPS)]))
127
+ st.session_state.wound_type = "snakebite"
128
+ st.session_state.basic_done = True
129
+
130
+ elif wound_type in ["normal", "wound"]:
131
+ st.subheader("🩹 Detected case: Normal wound")
132
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(NORMAL_WOUND_STEPS)]))
133
+ st.session_state.wound_type = "normal"
134
+ st.session_state.basic_done = True
135
+
136
+ else:
137
+ st.warning(f"⚠️ Unknown wound type: {wound_type}")
138
+ else:
139
+ st.warning("No valid data found in Firebase.")
140
+
141
+ # -----------------------
142
+ # Refresh Firebase data button
143
+ # -----------------------
144
+ st.markdown("---")
145
+ if st.button("🔄 Refresh Firebase data"):
146
+ st.toast("Refreshing data from Firebase...", icon="🔁")
147
+ data = get_latest_wound_data()
148
+ if data:
149
+ st.success("✅ Data refreshed successfully!")
150
+
151
+ # Cập nhật session state & hướng dẫn theo wound class
152
+ wound_type = str(data.get("Wound", "")).strip().lower()
153
+ st.session_state.wound_type = wound_type
154
+ st.session_state.basic_done = True
155
+
156
+ st.subheader("📡 Latest record (refreshed)")
157
+ st.json(data)
158
+ st.markdown("---")
159
+
160
+ if wound_type == "snakebite":
161
+ st.subheader("🐍 Detected case: Snakebite")
162
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(SNAKEBITE_STEPS)]))
163
+
164
+ elif wound_type in ["normal", "wound"]:
165
+ st.subheader("🩹 Detected case: Normal wound")
166
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(NORMAL_WOUND_STEPS)]))
167
+
168
+ else:
169
+ st.warning(f"⚠️ Unknown wound type: {wound_type}")
170
+ else:
171
+ st.warning("No data found in Firebase.")
172
+
173
+ # -----------------------
174
+ # Data helpers
175
+ # -----------------------
176
+
177
+ def ensure_datafile():
178
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
179
+ if not DATA_FILE.exists():
180
+ DATA_FILE.write_text("[]", encoding="utf-8")
181
+
182
+
183
+ def save_victim_info(victim: dict):
184
+ ensure_datafile()
185
+ try:
186
+ with DATA_FILE.open("r+", encoding="utf-8") as f:
187
+ try:
188
+ data = json.load(f)
189
+ if not isinstance(data, list):
190
+ data = []
191
+ except Exception:
192
+ data = []
193
+ victim_record = {
194
+ "timestamp": datetime.utcnow().isoformat() + "Z",
195
+ **victim,
196
+ }
197
+ data.append(victim_record)
198
+ f.seek(0)
199
+ json.dump(data, f, indent=2)
200
+ f.truncate()
201
+ return victim_record
202
+ except Exception as e:
203
+ # bubble up a friendly error
204
+ raise RuntimeError(f"Failed to save victim info: {e}")
205
+
206
+
207
+ def write_next_step(wound_type: str):
208
+ try:
209
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
210
+ NEXT_STEP_FILE.write_text(json.dumps({"wound_type": wound_type, "created_at": datetime.utcnow().isoformat() + "Z"}), encoding="utf-8")
211
+ except Exception:
212
+ pass
213
+
214
+ # -----------------------
215
+ # OpenAI integration
216
+ # -----------------------
217
+ DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
218
+
219
+ # Initialize session state for temporary UI key
220
+ if "ui_api_key" not in st.session_state:
221
+ st.session_state.ui_api_key = "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"
222
+
223
+ def get_openai_config_simple():
224
+ """
225
+ Returns a small config dict:
226
+ {
227
+ "OPENAI_apiKey": <string or None>,
228
+ "OPENAI_model": <string or None>
229
+ }
230
+ Priority: env var -> st.secrets['openai'] -> session UI key
231
+ """
232
+ cfg = {
233
+ "OPENAI_apiKey": os.environ.get("OPENAI_API_KEY"),
234
+ "OPENAI_model": os.environ.get("OPENAI_MODEL"),
235
+ }
236
+
237
+ # Try reading .streamlit/secrets.toml [openai] table if present
238
+ try:
239
+ if hasattr(st, "secrets"):
240
+ openai_sec = st.secrets.get("openai")
241
+ if isinstance(openai_sec, dict):
242
+ cfg["OPENAI_apiKey"] = openai_sec.get("OPENAI_API_KEY") or openai_sec.get("api_key") or cfg["OPENAI_apiKey"]
243
+ cfg["OPENAI_model"] = openai_sec.get("OPENAI_MODEL") or openai_sec.get("model") or cfg["OPENAI_model"]
244
+ except Exception:
245
+ pass
246
+
247
+ # fallback to the temporary UI key if nothing else
248
+ if not cfg["OPENAI_apiKey"]:
249
+ cfg["OPENAI_apiKey"] = st.session_state.get("ui_api_key")
250
+
251
+ return cfg
252
+
253
+ def get_effective_model():
254
+ # Prefer env var
255
+ env_model = os.environ.get("OPENAI_MODEL")
256
+ if env_model:
257
+ return env_model
258
+
259
+ # Try Streamlit secrets (handle nested or flat)
260
+ secret_model = None
261
+ if hasattr(st, "secrets"):
262
+ try:
263
+ # Nested under [openai]
264
+ secret_model = st.secrets["openai"]["OPENAI_MODEL"]
265
+ except KeyError:
266
+ # Flat key
267
+ secret_model = st.secrets.get("OPENAI_MODEL")
268
+
269
+ # Fallback to default
270
+ return secret_model or DEFAULT_MODEL
271
+
272
+ def make_openai_client(api_key):
273
+ if not api_key:
274
+ return None, "OpenAI API key not set. Provide OPENAI_API_KEY env var, set in secrets, or paste a temporary key in the UI (not recommended for production)."
275
+
276
+ try:
277
+ # Prefer modern OpenAI client
278
+ from openai import OpenAI
279
+ client = OpenAI(api_key=api_key)
280
+ return client, None
281
+ except ImportError:
282
+ try:
283
+ # Fallback to legacy openai
284
+ import openai
285
+ openai.api_key = api_key
286
+ # Return the module as 'client' (downstream code must adapt calls)
287
+ return openai, None
288
+ except ImportError:
289
+ return None, "openai package not installed. Add 'openai' to requirements.txt and rebuild/deploy."
290
+ except Exception as e:
291
+ return None, f"Failed to create OpenAI client: {str(e)}"
292
+
293
+
294
+ def extract_text_from_response(resp):
295
+ """Robust extraction across SDK shapes. Returns a plain string (possibly empty)."""
296
+ try:
297
+ # 1) If response already a string
298
+ if isinstance(resp, str):
299
+ return resp.strip()
300
+
301
+ # 2) If response has direct 'output_text' (Responses API convenience)
302
+ if hasattr(resp, "output_text"):
303
+ try:
304
+ return str(resp.output_text).strip()
305
+ except Exception:
306
+ pass
307
+ if isinstance(resp, dict) and "output_text" in resp and isinstance(resp["output_text"], str):
308
+ return resp["output_text"].strip()
309
+
310
+ # 3) If response has 'output' list (Responses API): iterate content blocks
311
+ out = None
312
+ if hasattr(resp, "output"):
313
+ out = getattr(resp, "output")
314
+ elif isinstance(resp, dict) and "output" in resp:
315
+ out = resp["output"]
316
+
317
+ if out:
318
+ # out is typically a list
319
+ for item in out:
320
+ # dict-like
321
+ if isinstance(item, dict):
322
+ # common content shapes: item['content'] -> list with dicts containing 'text'
323
+ c = item.get("content") or item.get("text") or item.get("items")
324
+ if isinstance(c, str):
325
+ return c.strip()
326
+ if isinstance(c, list) and c:
327
+ # first content block may be dict or string
328
+ first = c[0]
329
+ if isinstance(first, dict):
330
+ # common key names
331
+ for k in ("text", "content", "payload", "data"):
332
+ if k in first and isinstance(first[k], str):
333
+ return first[k].strip()
334
+ # try to stringify the dict
335
+ return str(first).strip()
336
+ elif isinstance(first, str):
337
+ return first.strip()
338
+ else:
339
+ # item may be an object with attributes
340
+ # try common attributes
341
+ for attr in ("text", "content", "value"):
342
+ if hasattr(item, attr):
343
+ val = getattr(item, attr)
344
+ if isinstance(val, str):
345
+ return val.strip()
346
+ try:
347
+ return str(val).strip()
348
+ except Exception:
349
+ pass
350
+
351
+ # 4) ChatCompletion style: choices -> first -> message -> content
352
+ if hasattr(resp, "choices") and getattr(resp, "choices", None):
353
+ first = resp.choices[0]
354
+ # object-like
355
+ if hasattr(first, "message") and getattr(first.message, "content", None):
356
+ return str(first.message.content).strip()
357
+ if hasattr(first, "text") and getattr(first, "text", None):
358
+ return str(first.text).strip()
359
+ if isinstance(resp, dict) and "choices" in resp and isinstance(resp["choices"], list) and resp["choices"]:
360
+ first = resp["choices"][0]
361
+ if isinstance(first, dict):
362
+ msg = first.get("message") or first
363
+ if isinstance(msg, dict):
364
+ c = msg.get("content") or msg.get("text")
365
+ if isinstance(c, str):
366
+ return c.strip()
367
+ # if content is nested structure, stringify
368
+ return str(c).strip()
369
+ # fallback
370
+ if "text" in first and isinstance(first["text"], str):
371
+ return first["text"].strip()
372
+
373
+ # 5) If resp looks like a Pydantic model or other object: try to convert to dict then locate text
374
+ try:
375
+ # modern pydantic has model_dump()
376
+ if hasattr(resp, "model_dump"):
377
+ dd = resp.model_dump()
378
+ # recursively try to extract
379
+ return extract_text_from_response(dd)
380
+ # older pydantic may have dict()
381
+ if hasattr(resp, "dict"):
382
+ dd = resp.dict()
383
+ return extract_text_from_response(dd)
384
+ except Exception:
385
+ pass
386
+
387
+ # 6) fallback: try common attributes or dict keys, or string conversion
388
+ for attr in ("text", "content", "message", "output", "response"):
389
+ if hasattr(resp, attr):
390
+ try:
391
+ return str(getattr(resp, attr)).strip()
392
+ except Exception:
393
+ pass
394
+ if isinstance(resp, dict):
395
+ return str(resp).strip()
396
+
397
+ return str(resp).strip()
398
+ except Exception:
399
+ # absolute fallback
400
+ try:
401
+ return str(resp)
402
+ except Exception:
403
+ return ""
404
+
405
+
406
+ def normalize_assistant_text(x):
407
+ """
408
+ Coerce various SDK response shapes into a plain string safely.
409
+ - If x is already a str, return it (stripped).
410
+ - If it has common attributes (text, content), use those.
411
+ - Otherwise fallback to JSON/string conversion.
412
+ """
413
+ try:
414
+ if x is None:
415
+ return ""
416
+ if isinstance(x, str):
417
+ return x.strip()
418
+ # common attribute names
419
+ for attr in ("text", "content", "output_text"):
420
+ if hasattr(x, attr):
421
+ val = getattr(x, attr)
422
+ if isinstance(val, str):
423
+ return val.strip()
424
+ # some attrs can be objects themselves
425
+ try:
426
+ return str(val).strip()
427
+ except Exception:
428
+ pass
429
+ # dict-like
430
+ try:
431
+ if isinstance(x, dict):
432
+ for k in ("text", "content", "output_text"):
433
+ if k in x and isinstance(x[k], str):
434
+ return x[k].strip()
435
+ return json.dumps(x, default=str)[:10000].strip()
436
+ except Exception:
437
+ pass
438
+ # fallback: try string conversion
439
+ return str(x).strip()
440
+ except Exception:
441
+ # last-resort fallback
442
+ return ""
443
+
444
+ # --- Robust query function (uses get_openai_config_simple) ---
445
+ def query_openai(system_prompt: str, user_message: str):
446
+ openai_cfg = get_openai_config_simple()
447
+ api_key = openai_cfg.get("OPENAI_apiKey")
448
+ model = openai_cfg.get("OPENAI_model") or DEFAULT_MODEL
449
+
450
+ client, err = make_openai_client(api_key)
451
+ if err:
452
+ return {"error": err}
453
+
454
+ try:
455
+ # 1) Modern OpenAI client: client.chat.create(...)
456
+ if hasattr(client, "chat") and hasattr(client.chat, "create"):
457
+ response = client.chat.create(
458
+ model=model,
459
+ messages=[
460
+ {"role": "system", "content": system_prompt},
461
+ {"role": "user", "content": user_message},
462
+ ],
463
+ temperature=0.2,
464
+ max_tokens=800,
465
+ )
466
+ text = extract_text_from_response(response)
467
+ return {"text": text}
468
+
469
+ # 2) Classic openai package ChatCompletion
470
+ if hasattr(client, "ChatCompletion"):
471
+ response = client.ChatCompletion.create(
472
+ model=model,
473
+ messages=[
474
+ {"role": "system", "content": system_prompt},
475
+ {"role": "user", "content": user_message},
476
+ ],
477
+ temperature=0.2,
478
+ max_tokens=800,
479
+ )
480
+ text = extract_text_from_response(response)
481
+ return {"text": text}
482
+
483
+ # 3) Responses API style: client.responses.create(...)
484
+ if hasattr(client, "responses") and hasattr(client.responses, "create"):
485
+ prompt = system_prompt + "\n\n" + user_message
486
+ response = None
487
+ # Try a few different kwarg sets because different SDK versions accept different names
488
+ candidate_kw_sets = [
489
+ {"model": model, "input": prompt, "max_tokens": 800, "temperature": 0.2},
490
+ {"model": model, "input": prompt, "max_output_tokens": 800, "temperature": 0.2},
491
+ {"model": model, "input": prompt, "temperature": 0.2},
492
+ {"model": model, "input": prompt},
493
+ ]
494
+ last_exc = None
495
+ for kw in candidate_kw_sets:
496
+ try:
497
+ response = client.responses.create(**kw)
498
+ break
499
+ except TypeError as te:
500
+ # method doesn't accept some kwarg name(s) — try the next set
501
+ last_exc = te
502
+ continue
503
+ except Exception as e:
504
+ # other runtime error (quota, auth, etc.) -> propagate as assistant error
505
+ return {"error": f"OpenAI responses request failed: {e}"}
506
+ if response is None:
507
+ # none of the kwarg sets worked
508
+ return {"error": f"OpenAI responses client didn't accept tried args. Last error: {last_exc}"}
509
+ text = extract_text_from_response(response)
510
+ return {"text": text}
511
+
512
+ # 4) Very old shape: Completion.create
513
+ if hasattr(client, "Completion") and hasattr(client.Completion, "create"):
514
+ prompt = system_prompt + "\n\n" + user_message
515
+ response = client.Completion.create(model=model, prompt=prompt, max_tokens=800, temperature=0.2)
516
+ text = extract_text_from_response(response)
517
+ return {"text": text}
518
+
519
+ return {"error": "OpenAI client present but no compatible chat method found. Consider upgrading the openai package (pip install --upgrade openai)."}
520
+ except Exception as e:
521
+ return {"error": f"OpenAI request failed: {e}"}
522
+
523
+
524
+
525
+ def make_system_prompt(wound_type: str, victim_info: dict) -> str:
526
+ return (
527
+ f"You are an expert first aid assistant. The victim has wound_type={wound_type}.\n"
528
+ "- Give clear, actionable, short steps that a non-professional can follow.\n"
529
+ "- Mention when to call emergency services or go to hospital.\n"
530
+ f"- Use the victim info below to adjust guidance:\n{json.dumps(victim_info, indent=2)}\n"
531
+ "Keep language concise. Use numbered short steps when appropriate."
532
+ )
533
+
534
+ # -----------------------
535
+ # Config check / helper
536
+ # -----------------------
537
+ DEFAULT_CONFIG_TEXT = """[server]
538
+ headless = true
539
+ port = 8501
540
+ enableCORS = false
541
+ address = "0.0.0.0"
542
+ # disable XSRF to avoid the override warning
543
+ enableXsrfProtection = false
544
+
545
+ [browser]
546
+ gatherUsageStats = false
547
+ """
548
+
549
+
550
+ def show_and_optionally_reset_config():
551
+ st.subheader("Streamlit config check")
552
+ if STREAMLIT_ROOT_CONFIG.exists():
553
+ st.info(f"Found config at `{STREAMLIT_ROOT_CONFIG}`")
554
+ try:
555
+ import toml
556
+
557
+ raw = STREAMLIT_ROOT_CONFIG.read_text(encoding="utf-8")
558
+ toml.loads(raw) # validate
559
+ st.success("Config TOML parsed successfully.")
560
+ st.code(raw, language="toml")
561
+ except Exception as e:
562
+ st.error(f"Config parse error: {e}")
563
+ st.write("Here is a safe replacement you can apply. Click the button to attempt reset.")
564
+ st.code(DEFAULT_CONFIG_TEXT, language="toml")
565
+ if st.button("Attempt to reset /root/.streamlit/config.toml (may require root)"):
566
+ try:
567
+ STREAMLIT_ROOT_CONFIG.write_text(DEFAULT_CONFIG_TEXT, encoding="utf-8")
568
+ st.success("Wrote default config. You may need to restart Streamlit or the container.")
569
+ except Exception as write_exc:
570
+ st.error(f"Failed to write config: {write_exc}")
571
+ st.info("If writing failed due to permissions, run this command on the host:")
572
+ cmd = (
573
+ "sudo mkdir -p /root/.streamlit && "
574
+ "sudo tee /root/.streamlit/config.toml > /dev/null <<'EOF'\n"
575
+ + DEFAULT_CONFIG_TEXT
576
+ + "EOF\n"
577
+ )
578
+ st.code(cmd, language="bash")
579
+ else:
580
+ st.info(f"No config found at `{STREAMLIT_ROOT_CONFIG}`")
581
+ if st.button("Create default config at /root/.streamlit/config.toml (may require root)"):
582
+ try:
583
+ STREAMLIT_ROOT_CONFIG.parent.mkdir(parents=True, exist_ok=True)
584
+ STREAMLIT_ROOT_CONFIG.write_text(DEFAULT_CONFIG_TEXT, encoding="utf-8")
585
+ st.success("Wrote default config. Restart Streamlit if needed.")
586
+ except Exception as e:
587
+ st.error(f"Failed to write config: {e}")
588
+ st.info("If writing failed due to permissions, run this on the host:")
589
+ cmd = (
590
+ "sudo mkdir -p /root/.streamlit && "
591
+ "sudo tee /root/.streamlit/config.toml > /dev/null <<'EOF'\n"
592
+ + DEFAULT_CONFIG_TEXT
593
+ + "EOF\n"
594
+ )
595
+ st.code(cmd, language="bash")
596
+
597
+ # -----------------------
598
+ # Streamlit UI
599
+ # -----------------------
600
+
601
+ # Function: Save Victim Info to Firebase
602
+ def save_victim_info(victim: dict):
603
+ """
604
+ Save a victim record to Firebase Realtime Database using Pyrebase4.
605
+ Automatically generates a unique record ID.
606
+ Returns the saved record (dict).
607
+ """
608
+ if not db:
609
+ raise ValueError("Firebase not connected.")
610
+
611
+ import time
612
+
613
+ # Unique ID for each victim record
614
+ record_id = f"victim_{int(time.time())}"
615
+
616
+ # Ensure wound_type & timestamp fields exist
617
+ victim_data = victim.copy()
618
+ victim_data["timestamp"] = time.strftime("%d/%m/%Y %H:%M:%S")
619
+
620
+ try:
621
+ # Save data under "victims/<record_id>"
622
+ db.child("victims").child(record_id).set(victim_data)
623
+ return victim_data
624
+ except Exception as e:
625
+ raise RuntimeError(f"Failed to save victim info: {e}")
626
+
627
+ # Advanced section
628
+ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
629
+ st.header("Advanced assistance")
630
+ st.markdown(f"**Wound type:** `{st.session_state.wound_type}`")
631
+
632
+ with st.form("victim_form", clear_on_submit=False):
633
+ st.subheader("Victim information (helps tailor guidance)")
634
+ name = st.text_input("Victim name (optional)")
635
+ age = st.text_input("Age (years)", help="Enter an integer or leave blank")
636
+ weight = st.text_input("Weight (kg)", help="Enter a number or leave blank")
637
+ height = st.text_input("Height (cm)", help="Enter a number or leave blank")
638
+ notes = st.text_area("Short notes (allergies, meds, conscious status...)", max_chars=500, height=80)
639
+
640
+ col_a, col_b = st.columns([1, 3])
641
+ with col_a:
642
+ submit = st.form_submit_button("Save info & get advanced guidance")
643
+ with col_b:
644
+ # Use form_submit_button here (not st.button) because we are inside a form
645
+ emergency_submit = st.form_submit_button("Emergency — Call local services")
646
+
647
+ # Both submit buttons return True when they were clicked.
648
+ if emergency_submit:
649
+ # Validate numeric fields before saving
650
+ try:
651
+ age_v = int(age) if (age and age.strip() != "") else None
652
+ except Exception:
653
+ st.error("Age must be an integer (or leave blank).")
654
+ st.stop()
655
+ try:
656
+ weight_v = float(weight) if (weight and weight.strip() != "") else None
657
+ except Exception:
658
+ st.error("Weight must be a number (kg) or leave blank.")
659
+ st.stop()
660
+ try:
661
+ height_v = float(height) if (height and height.strip() != "") else None
662
+ except Exception:
663
+ st.error("Height must be a number (cm) or leave blank.")
664
+ st.stop()
665
+
666
+ victim = {
667
+ "name": name or "unknown",
668
+ "age": age_v,
669
+ "weight_kg": weight_v,
670
+ "height_cm": height_v,
671
+ "notes": notes or "",
672
+ "wound_type": st.session_state.wound_type,
673
+ "saved_at": datetime.utcnow().isoformat() + "Z",
674
+ "emergency": True,
675
+ }
676
+
677
+ try:
678
+ record = save_victim_info(victim)
679
+ st.session_state.last_saved_record = record
680
+ # Emergency: highlight and instruct user to call local services immediately
681
+ st.error("EMERGENCY recorded. Call local emergency services now.")
682
+ st.warning("If the victim is unconscious, having trouble breathing, or bleeding heavily, call emergency services immediately.")
683
+ except Exception as e:
684
+ st.error(str(e))
685
+ st.write(traceback.format_exc())
686
+ st.stop()
687
+
688
+ if submit:
689
+ # Validate numeric fields
690
+ try:
691
+ age_v = int(age) if (age and age.strip() != "") else None
692
+ except Exception:
693
+ st.error("Age must be an integer (or leave blank).")
694
+ st.stop()
695
+ try:
696
+ weight_v = float(weight) if (weight and weight.strip() != "") else None
697
+ except Exception:
698
+ st.error("Weight must be a number (kg) or leave blank.")
699
+ st.stop()
700
+ try:
701
+ height_v = float(height) if (height and height.strip() != "") else None
702
+ except Exception:
703
+ st.error("Height must be a number (cm) or leave blank.")
704
+ st.stop()
705
+
706
+ victim = {
707
+ "name": name or "unknown",
708
+ "age": age_v,
709
+ "weight_kg": weight_v,
710
+ "height_cm": height_v,
711
+ "notes": notes or "",
712
+ "wound_type": st.session_state.wound_type,
713
+ "saved_at": datetime.utcnow().isoformat() + "Z",
714
+ "emergency": False,
715
+ }
716
+
717
+ try:
718
+ record = save_victim_info(victim)
719
+ st.session_state.last_saved_record = record
720
+ st.success("Victim info saved.")
721
+ except Exception as e:
722
+ st.error(str(e))
723
+ st.write(traceback.format_exc())
724
+ st.stop()
725
+
726
+ # Prepare and call OpenAI for the normal (non-emergency) flow
727
+ system_prompt = make_system_prompt(st.session_state.wound_type, victim)
728
+ initial_user_msg = (
729
+ "The patient has completed the basic steps. Provide the next, in-depth first aid guidance and any monitoring steps. "
730
+ "Keep guidance concise and list immediate priorities first."
731
+ )
732
+ with st.spinner("Contacting assistant..."):
733
+ resp = query_openai(system_prompt, initial_user_msg)
734
+ if "error" in resp:
735
+ st.error(f"Assistant error: {resp['error']}")
736
+ else:
737
+ assistant_text = normalize_assistant_text(resp.get("text"))
738
+ st.session_state.assistant_text = assistant_text
739
+ st.session_state.chat_history = [
740
+ {"role": "assistant", "text": assistant_text}
741
+ ]
742
+ st.markdown("**Assistant guidance:**")
743
+ st.text_area("Assistant", value=assistant_text, height=250, key="assistant_output")
744
+
745
+
746
+ # Chat follow-ups
747
+ st.subheader("Ask follow-up questions")
748
+ st.markdown("Ask the assistant further questions related to the victim or care. Keep questions concise.")
749
+ col_in, col_btn = st.columns([4, 1])
750
+ with col_in:
751
+ user_q = st.text_input("Your question", key="chat_input")
752
+ with col_btn:
753
+ if st.button("Ask"):
754
+ if not user_q or user_q.strip() == "":
755
+ st.warning("Type a question first.")
756
+ else:
757
+ victim_info = st.session_state.get("last_saved_record") or {}
758
+ system_prompt = make_system_prompt(st.session_state.wound_type, victim_info)
759
+ with st.spinner("Assistant is replying..."):
760
+ resp = query_openai(system_prompt, user_q)
761
+ if "error" in resp:
762
+ st.error(f"Assistant error: {resp['error']}")
763
+ else:
764
+ assistant_reply = normalize_assistant_text(resp.get("text"))
765
+ history = st.session_state.get("chat_history", [])
766
+ history.append({"role": "user", "text": user_q})
767
+ history.append({"role": "assistant", "text": assistant_reply})
768
+ st.session_state.chat_history = history
769
+ st.success("Assistant responded. See conversation below.")
770
+
771
+ # Conversation display
772
+ if st.session_state.get("chat_history"):
773
+ st.subheader("Conversation log")
774
+ for turn in st.session_state.chat_history:
775
+ role = turn.get("role", "assistant")
776
+ text = turn.get("text", "")
777
+ if role == "assistant":
778
+ st.markdown(f"**Assistant:** {text}")
779
+ else:
780
+ st.markdown(f"**User:** {text}")
781
+
782
+ else:
783
+ st.info("Start by reviewing the quick steps for Snakebite or Normal wound and confirm completion using the checkbox, then click Next → Advanced.")
784
+
785
+ st.markdown("---")
786
+ st.caption(
787
+ "🔒 Security: Prefer setting OPENAI_API_KEY as an environment variable or platform secret. "
788
+ "If you paste a key into the UI it is stored only in session memory and not written to disk."
789
+ )
790
+
791
+ # END