Nolist commited on
Commit
bb3e5e4
·
verified ·
1 Parent(s): 11b9516

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +131 -61
app.py CHANGED
@@ -1,10 +1,10 @@
1
- # streamlit_app.py
2
  """
3
- Single-file Streamlit app (no Dockerfile required).
4
- - Saves data to ./data/victims.json (auto-created)
5
- - Uses OPENAI_API_KEY env var or temporary UI key input
6
- - Checks and optionally resets /root/.streamlit/config.toml if malformed
7
- - Run locally with: streamlit run streamlit_app.py
8
  """
9
 
10
  import os
@@ -12,18 +12,17 @@ import json
12
  import traceback
13
  from datetime import datetime
14
  from pathlib import Path
15
- from fastapi import FastAPI
16
  import streamlit as st
17
 
18
  # -----------------------
19
- # Configuration / Paths
20
  # -----------------------
21
  APP_DIR = Path.cwd()
22
  DATA_DIR = APP_DIR / "data"
23
  DATA_FILE = DATA_DIR / "victims.json"
24
- STREAMLIT_ROOT_CONFIG = Path("/root/.streamlit/config.toml") # common in containers
 
25
 
26
- # Ensure data dir exists
27
  DATA_DIR.mkdir(parents=True, exist_ok=True)
28
 
29
  # -----------------------
@@ -47,17 +46,20 @@ NORMAL_WOUND_STEPS = [
47
  "If bleeding continues or wound is deep, seek medical help.",
48
  ]
49
 
 
50
  def get_steps(wound_type: str):
51
  return SNAKEBITE_STEPS if wound_type == "poisoned" else NORMAL_WOUND_STEPS
52
 
53
  # -----------------------
54
  # Data helpers
55
  # -----------------------
 
56
  def ensure_datafile():
57
  DATA_DIR.mkdir(parents=True, exist_ok=True)
58
  if not DATA_FILE.exists():
59
  DATA_FILE.write_text("[]", encoding="utf-8")
60
 
 
61
  def save_victim_info(victim: dict):
62
  ensure_datafile()
63
  try:
@@ -70,7 +72,7 @@ def save_victim_info(victim: dict):
70
  data = []
71
  victim_record = {
72
  "timestamp": datetime.utcnow().isoformat() + "Z",
73
- **victim
74
  }
75
  data.append(victim_record)
76
  f.seek(0)
@@ -78,41 +80,74 @@ def save_victim_info(victim: dict):
78
  f.truncate()
79
  return victim_record
80
  except Exception as e:
81
- raise
 
 
 
 
 
 
 
 
 
82
 
83
  # -----------------------
84
  # OpenAI integration
85
  # -----------------------
86
  DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
87
 
88
- # Option 1 (recommended): set OPENAI_API_KEY environment variable.
89
- _env_api_key = "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"
 
 
90
 
91
- # We'll allow a temporary in-UI key (not persisted) via session_state if user pastes it.
92
  if "ui_api_key" not in st.session_state:
93
  st.session_state.ui_api_key = None
94
 
95
- def get_effective_api_key():
96
- """Prefer env var, fallback to temporary UI key."""
97
- return _env_api_key or st.session_state.ui_api_key
98
 
99
- # Lazy import OpenAI only when needed
100
- def make_openai_client(api_key):
 
101
  try:
102
- from openai import OpenAI
103
  except Exception:
104
- return None, "openai package is not installed. Add 'openai' to requirements.txt and rebuild."
 
 
 
 
 
105
  if not api_key:
106
  return None, "OpenAI API key not set. Provide OPENAI_API_KEY env var or paste a temporary key in the UI (not recommended for production)."
107
  try:
 
 
 
108
  client = OpenAI(api_key=api_key)
109
  return client, None
110
- except Exception as e:
111
- return None, f"Failed to create OpenAI client: {e}"
 
 
 
 
 
 
 
112
 
113
  def extract_text_from_response(resp):
114
- # Best-effort extraction for different SDK shapes
115
  try:
 
 
 
 
 
 
 
 
 
 
116
  if isinstance(resp, dict):
117
  choices = resp.get("choices")
118
  if choices:
@@ -122,46 +157,64 @@ def extract_text_from_response(resp):
122
  if isinstance(msg, dict):
123
  return msg.get("content") or msg.get("text") or str(resp)
124
  return first.get("text") or str(resp)
125
- # fallback to str
126
  return str(resp)
127
  except Exception:
128
  return str(resp)
129
 
 
130
  def query_openai(system_prompt: str, user_message: str):
131
  api_key = get_effective_api_key()
132
  client, err = make_openai_client(api_key)
133
  if err:
134
  return {"error": err}
135
  try:
136
- response = client.chat.create(
137
- model=DEFAULT_MODEL,
138
- messages=[
139
- {"role": "system", "content": system_prompt},
140
- {"role": "user", "content": user_message},
141
- ],
142
- temperature=0.2,
143
- max_tokens=800,
144
- )
145
- text = extract_text_from_response(response)
146
- return {"text": text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  except Exception as e:
148
  return {"error": f"OpenAI request failed: {e}"}
149
 
 
150
  def make_system_prompt(wound_type: str, victim_info: dict) -> str:
151
  return (
152
  f"You are an expert first aid assistant. The victim has wound_type={wound_type}.\n"
153
  "- Give clear, actionable, short steps that a non-professional can follow.\n"
154
  "- Mention when to call emergency services or go to hospital.\n"
155
  f"- Use the victim info below to adjust guidance:\n{json.dumps(victim_info, indent=2)}\n"
156
- "Keep language concise."
157
  )
158
 
159
  # -----------------------
160
- # Config check and repair helper
161
  # -----------------------
162
  DEFAULT_CONFIG_TEXT = """[server]
163
  headless = true
164
- port = 7860
165
  enableCORS = false
166
  address = "0.0.0.0"
167
  # disable XSRF to avoid the override warning
@@ -171,16 +224,14 @@ enableXsrfProtection = false
171
  gatherUsageStats = false
172
  """
173
 
 
174
  def show_and_optionally_reset_config():
175
- """
176
- Shows status about /root/.streamlit/config.toml and provides a safe reset button.
177
- If permission prevents writing, shows the exact shell command to run as root.
178
- """
179
  st.subheader("Streamlit config check")
180
  if STREAMLIT_ROOT_CONFIG.exists():
181
  st.info(f"Found config at `{STREAMLIT_ROOT_CONFIG}`")
182
  try:
183
  import toml
 
184
  raw = STREAMLIT_ROOT_CONFIG.read_text(encoding="utf-8")
185
  toml.loads(raw) # validate
186
  st.success("Config TOML parsed successfully.")
@@ -232,11 +283,17 @@ st.markdown(
232
  "Start with the quick steps on the left/right, confirm completion, then proceed to the advanced assistant."
233
  )
234
 
 
 
 
 
 
 
235
  col1, col2 = st.columns(2)
236
 
237
  with col1:
238
  st.subheader("Snakebite (poisoned wound)")
239
- st.markdown("\n".join([f"{i+1}. {s}" for i,s in enumerate(SNAKEBITE_STEPS)]))
240
  snake_done = st.checkbox("I have completed these basic steps (snakebite)", key="snake_done")
241
  if st.button("Next → Advanced (snakebite)"):
242
  if not snake_done:
@@ -244,11 +301,13 @@ with col1:
244
  else:
245
  st.session_state.wound_type = "poisoned"
246
  st.session_state.basic_done = True
247
- st.success("Proceeding to advanced assistance for snakebite. Scroll down.")
 
 
248
 
249
  with col2:
250
  st.subheader("Normal wound")
251
- st.markdown("\n".join([f"{i+1}. {s}" for i,s in enumerate(NORMAL_WOUND_STEPS)]))
252
  wound_done = st.checkbox("I have completed these basic steps (normal wound)", key="wound_done")
253
  if st.button("Next → Advanced (normal wound)"):
254
  if not wound_done:
@@ -256,20 +315,23 @@ with col2:
256
  else:
257
  st.session_state.wound_type = "normal"
258
  st.session_state.basic_done = True
259
- st.success("Proceeding to advanced assistance for normal wound. Scroll down.")
 
 
260
 
261
  st.markdown("---")
262
 
263
- # System status area
264
- with st.expander("System status & Run instructions", expanded=True):
265
  st.write(f"Data file: `{DATA_FILE}`")
 
266
  st.write(f"Model (OPENAI_MODEL): `{DEFAULT_MODEL}`")
267
- st.write(f"OPENAI_API_KEY env var present: `{bool(_env_api_key)}`")
268
- st.write("To run locally: `streamlit run streamlit_app.py`")
269
- st.write("If you removed Dockerfile, this app is ready to run directly with Streamlit or deploy to a platform.")
270
  show_and_optionally_reset_config()
271
 
272
- # Temporary UI API key input (not persisted)
273
  with st.expander("(Optional) Temporary OpenAI API key (paste here for quick test)", expanded=False):
274
  st.warning("Do NOT paste production keys here in a public environment. This value is stored only in session memory and not saved to disk.")
275
  new_key = st.text_input("Temporary API key (session only)", value=st.session_state.ui_api_key or "")
@@ -293,7 +355,8 @@ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
293
  with col_a:
294
  submit = st.form_submit_button("Save info & get advanced guidance")
295
  with col_b:
296
- st.button("Emergency — Call local services", help="Use this to indicate urgent help is needed")
 
297
 
298
  if submit:
299
  # Validate numeric fields
@@ -328,7 +391,7 @@ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
328
  st.session_state.last_saved_record = record
329
  st.success("Victim info saved.")
330
  except Exception as e:
331
- st.error(f"Failed to save victim info: {e}")
332
  st.write(traceback.format_exc())
333
  st.stop()
334
 
@@ -354,7 +417,7 @@ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
354
  # Chat follow-ups
355
  st.subheader("Ask follow-up questions")
356
  st.markdown("Ask the assistant further questions related to the victim or care. Keep questions concise.")
357
- col_in, col_btn = st.columns([4,1])
358
  with col_in:
359
  user_q = st.text_input("Your question", key="chat_input")
360
  with col_btn:
@@ -370,8 +433,10 @@ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
370
  st.error(f"Assistant error: {resp['error']}")
371
  else:
372
  assistant_reply = resp.get("text", "").strip()
373
- st.session_state.chat_history.append({"role": "user", "text": user_q})
374
- st.session_state.chat_history.append({"role": "assistant", "text": assistant_reply})
 
 
375
  st.success("Assistant responded. See conversation below.")
376
 
377
  # Conversation display
@@ -384,9 +449,14 @@ if st.session_state.get("basic_done") and st.session_state.get("wound_type"):
384
  st.markdown(f"**Assistant:** {text}")
385
  else:
386
  st.markdown(f"**User:** {text}")
 
387
  else:
388
  st.info("Start by reviewing the quick steps for Snakebite or Normal wound and confirm completion using the checkbox, then click Next → Advanced.")
389
 
390
  st.markdown("---")
391
- st.caption("🔒 Security: Prefer setting OPENAI_API_KEY as an environment variable (or use platform secrets). "
392
- "If you paste a key into the UI it is stored only in session memory and not written to disk.")
 
 
 
 
 
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
 
12
  import traceback
13
  from datetime import datetime
14
  from pathlib import Path
 
15
  import streamlit as st
16
 
17
  # -----------------------
18
+ # Paths / Configuration
19
  # -----------------------
20
  APP_DIR = Path.cwd()
21
  DATA_DIR = APP_DIR / "data"
22
  DATA_FILE = DATA_DIR / "victims.json"
23
+ NEXT_STEP_FILE = DATA_DIR / "next_step.json"
24
+ STREAMLIT_ROOT_CONFIG = Path("/root/.streamlit/config.toml")
25
 
 
26
  DATA_DIR.mkdir(parents=True, exist_ok=True)
27
 
28
  # -----------------------
 
46
  "If bleeding continues or wound is deep, seek medical help.",
47
  ]
48
 
49
+
50
  def get_steps(wound_type: str):
51
  return SNAKEBITE_STEPS if wound_type == "poisoned" else NORMAL_WOUND_STEPS
52
 
53
  # -----------------------
54
  # Data helpers
55
  # -----------------------
56
+
57
  def ensure_datafile():
58
  DATA_DIR.mkdir(parents=True, exist_ok=True)
59
  if not DATA_FILE.exists():
60
  DATA_FILE.write_text("[]", encoding="utf-8")
61
 
62
+
63
  def save_victim_info(victim: dict):
64
  ensure_datafile()
65
  try:
 
72
  data = []
73
  victim_record = {
74
  "timestamp": datetime.utcnow().isoformat() + "Z",
75
+ **victim,
76
  }
77
  data.append(victim_record)
78
  f.seek(0)
 
80
  f.truncate()
81
  return victim_record
82
  except Exception as e:
83
+ # bubble up a friendly error
84
+ raise RuntimeError(f"Failed to save victim info: {e}")
85
+
86
+
87
+ def write_next_step(wound_type: str):
88
+ try:
89
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
90
+ NEXT_STEP_FILE.write_text(json.dumps({"wound_type": wound_type, "created_at": datetime.utcnow().isoformat() + "Z"}), encoding="utf-8")
91
+ except Exception:
92
+ pass
93
 
94
  # -----------------------
95
  # OpenAI integration
96
  # -----------------------
97
  DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
98
 
99
+ # Prefer secure sources for API key:
100
+ # 1) Environment variable OPENAI_API_KEY
101
+ # 2) Streamlit secrets (if deployed to Streamlit sharing / Hugging Face etc)
102
+ # 3) Temporary UI key stored in session_state (not persisted to disk)
103
 
 
104
  if "ui_api_key" not in st.session_state:
105
  st.session_state.ui_api_key = None
106
 
 
 
 
107
 
108
+ def get_effective_api_key():
109
+ env_key = os.environ.get("OPENAI_API_KEY")
110
+ secret_key = None
111
  try:
112
+ secret_key = st.secrets.get("OPENAI_API_KEY") if hasattr(st, "secrets") else None
113
  except Exception:
114
+ secret_key = None
115
+ return env_key or secret_key or st.session_state.ui_api_key
116
+
117
+
118
+ def make_openai_client(api_key):
119
+ # attempt to import modern OpenAI client, fall back to classic openai
120
  if not api_key:
121
  return None, "OpenAI API key not set. Provide OPENAI_API_KEY env var or paste a temporary key in the UI (not recommended for production)."
122
  try:
123
+ # modern package
124
+ from openai import OpenAI
125
+
126
  client = OpenAI(api_key=api_key)
127
  return client, None
128
+ except Exception:
129
+ try:
130
+ import openai
131
+
132
+ openai.api_key = api_key
133
+ return openai, None
134
+ except Exception:
135
+ return None, "openai package not installed or failed to import. Add 'openai' to requirements.txt and rebuild/deploy."
136
+
137
 
138
  def extract_text_from_response(resp):
139
+ # handle multiple SDK shapes
140
  try:
141
+ # modern OpenAI SDK (client.chat.create) returns an object with .choices[0].message.content
142
+ if hasattr(resp, "choices") and resp.choices:
143
+ first = resp.choices[0]
144
+ # modern object
145
+ if hasattr(first, "message") and hasattr(first.message, "content"):
146
+ return first.message.content
147
+ # older SDK shape
148
+ if hasattr(first, "text"):
149
+ return first.text
150
+ # if dict-like
151
  if isinstance(resp, dict):
152
  choices = resp.get("choices")
153
  if choices:
 
157
  if isinstance(msg, dict):
158
  return msg.get("content") or msg.get("text") or str(resp)
159
  return first.get("text") or str(resp)
 
160
  return str(resp)
161
  except Exception:
162
  return str(resp)
163
 
164
+
165
  def query_openai(system_prompt: str, user_message: str):
166
  api_key = get_effective_api_key()
167
  client, err = make_openai_client(api_key)
168
  if err:
169
  return {"error": err}
170
  try:
171
+ # Modern OpenAI client
172
+ if hasattr(client, "chat") and hasattr(client.chat, "create"):
173
+ response = client.chat.create(
174
+ model=DEFAULT_MODEL,
175
+ messages=[
176
+ {"role": "system", "content": system_prompt},
177
+ {"role": "user", "content": user_message},
178
+ ],
179
+ temperature=0.2,
180
+ max_tokens=800,
181
+ )
182
+ text = extract_text_from_response(response)
183
+ return {"text": text}
184
+ # Classic openai package
185
+ if hasattr(client, "ChatCompletion"):
186
+ response = client.ChatCompletion.create(
187
+ model=DEFAULT_MODEL,
188
+ messages=[
189
+ {"role": "system", "content": system_prompt},
190
+ {"role": "user", "content": user_message},
191
+ ],
192
+ temperature=0.2,
193
+ max_tokens=800,
194
+ )
195
+ text = extract_text_from_response(response)
196
+ return {"text": text}
197
+ # last fallback
198
+ return {"error": "OpenAI client present but no compatible chat method found."}
199
  except Exception as e:
200
  return {"error": f"OpenAI request failed: {e}"}
201
 
202
+
203
  def make_system_prompt(wound_type: str, victim_info: dict) -> str:
204
  return (
205
  f"You are an expert first aid assistant. The victim has wound_type={wound_type}.\n"
206
  "- Give clear, actionable, short steps that a non-professional can follow.\n"
207
  "- Mention when to call emergency services or go to hospital.\n"
208
  f"- Use the victim info below to adjust guidance:\n{json.dumps(victim_info, indent=2)}\n"
209
+ "Keep language concise. Use numbered short steps when appropriate."
210
  )
211
 
212
  # -----------------------
213
+ # Config check / helper
214
  # -----------------------
215
  DEFAULT_CONFIG_TEXT = """[server]
216
  headless = true
217
+ port = 8501
218
  enableCORS = false
219
  address = "0.0.0.0"
220
  # disable XSRF to avoid the override warning
 
224
  gatherUsageStats = false
225
  """
226
 
227
+
228
  def show_and_optionally_reset_config():
 
 
 
 
229
  st.subheader("Streamlit config check")
230
  if STREAMLIT_ROOT_CONFIG.exists():
231
  st.info(f"Found config at `{STREAMLIT_ROOT_CONFIG}`")
232
  try:
233
  import toml
234
+
235
  raw = STREAMLIT_ROOT_CONFIG.read_text(encoding="utf-8")
236
  toml.loads(raw) # validate
237
  st.success("Config TOML parsed successfully.")
 
283
  "Start with the quick steps on the left/right, confirm completion, then proceed to the advanced assistant."
284
  )
285
 
286
+ # Reset session controls for convenience
287
+ if st.button("Reset session (clear) "):
288
+ for k in list(st.session_state.keys()):
289
+ del st.session_state[k]
290
+ st.experimental_rerun()
291
+
292
  col1, col2 = st.columns(2)
293
 
294
  with col1:
295
  st.subheader("Snakebite (poisoned wound)")
296
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(SNAKEBITE_STEPS)]))
297
  snake_done = st.checkbox("I have completed these basic steps (snakebite)", key="snake_done")
298
  if st.button("Next → Advanced (snakebite)"):
299
  if not snake_done:
 
301
  else:
302
  st.session_state.wound_type = "poisoned"
303
  st.session_state.basic_done = True
304
+ write_next_step("poisoned")
305
+ st.success("Proceeding to advanced assistance for snakebite.")
306
+ st.experimental_rerun()
307
 
308
  with col2:
309
  st.subheader("Normal wound")
310
+ st.markdown("\n".join([f"{i+1}. {s}" for i, s in enumerate(NORMAL_WOUND_STEPS)]))
311
  wound_done = st.checkbox("I have completed these basic steps (normal wound)", key="wound_done")
312
  if st.button("Next → Advanced (normal wound)"):
313
  if not wound_done:
 
315
  else:
316
  st.session_state.wound_type = "normal"
317
  st.session_state.basic_done = True
318
+ write_next_step("normal")
319
+ st.success("Proceeding to advanced assistance for normal wound.")
320
+ st.experimental_rerun()
321
 
322
  st.markdown("---")
323
 
324
+ # System status area (useful for debugging / deployment)
325
+ with st.expander("System status & Run instructions", expanded=False):
326
  st.write(f"Data file: `{DATA_FILE}`")
327
+ st.write(f"Next-step file: `{NEXT_STEP_FILE}`")
328
  st.write(f"Model (OPENAI_MODEL): `{DEFAULT_MODEL}`")
329
+ api_key_present = bool(os.environ.get("OPENAI_API_KEY") or (hasattr(st, "secrets") and st.secrets.get("OPENAI_API_KEY")))
330
+ st.write(f"OPENAI_API_KEY present in env/secrets: `{api_key_present}`")
331
+ st.write("To run locally: `streamlit run app.py`\nIf deploying to Hugging Face or another platform, add OPENAI_API_KEY to platform secrets.")
332
  show_and_optionally_reset_config()
333
 
334
+ # Temporary UI API key input (not persisted to disk)
335
  with st.expander("(Optional) Temporary OpenAI API key (paste here for quick test)", expanded=False):
336
  st.warning("Do NOT paste production keys here in a public environment. This value is stored only in session memory and not saved to disk.")
337
  new_key = st.text_input("Temporary API key (session only)", value=st.session_state.ui_api_key or "")
 
355
  with col_a:
356
  submit = st.form_submit_button("Save info & get advanced guidance")
357
  with col_b:
358
+ if st.button("Emergency — Call local services"):
359
+ st.warning("If this is an emergency, call your local emergency number now.")
360
 
361
  if submit:
362
  # Validate numeric fields
 
391
  st.session_state.last_saved_record = record
392
  st.success("Victim info saved.")
393
  except Exception as e:
394
+ st.error(str(e))
395
  st.write(traceback.format_exc())
396
  st.stop()
397
 
 
417
  # Chat follow-ups
418
  st.subheader("Ask follow-up questions")
419
  st.markdown("Ask the assistant further questions related to the victim or care. Keep questions concise.")
420
+ col_in, col_btn = st.columns([4, 1])
421
  with col_in:
422
  user_q = st.text_input("Your question", key="chat_input")
423
  with col_btn:
 
433
  st.error(f"Assistant error: {resp['error']}")
434
  else:
435
  assistant_reply = resp.get("text", "").strip()
436
+ history = st.session_state.get("chat_history", [])
437
+ history.append({"role": "user", "text": user_q})
438
+ history.append({"role": "assistant", "text": assistant_reply})
439
+ st.session_state.chat_history = history
440
  st.success("Assistant responded. See conversation below.")
441
 
442
  # Conversation display
 
449
  st.markdown(f"**Assistant:** {text}")
450
  else:
451
  st.markdown(f"**User:** {text}")
452
+
453
  else:
454
  st.info("Start by reviewing the quick steps for Snakebite or Normal wound and confirm completion using the checkbox, then click Next → Advanced.")
455
 
456
  st.markdown("---")
457
+ st.caption(
458
+ "🔒 Security: Prefer setting OPENAI_API_KEY as an environment variable or platform secret. "
459
+ "If you paste a key into the UI it is stored only in session memory and not written to disk."
460
+ )
461
+
462
+ # END