Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
#
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
-
-
|
| 5 |
-
-
|
| 6 |
-
-
|
| 7 |
-
-
|
| 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 |
-
#
|
| 20 |
# -----------------------
|
| 21 |
APP_DIR = Path.cwd()
|
| 22 |
DATA_DIR = APP_DIR / "data"
|
| 23 |
DATA_FILE = DATA_DIR / "victims.json"
|
| 24 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# -----------------------
|
| 84 |
# OpenAI integration
|
| 85 |
# -----------------------
|
| 86 |
DEFAULT_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 100 |
-
|
|
|
|
| 101 |
try:
|
| 102 |
-
|
| 103 |
except Exception:
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
def extract_text_from_response(resp):
|
| 114 |
-
#
|
| 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 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 161 |
# -----------------------
|
| 162 |
DEFAULT_CONFIG_TEXT = """[server]
|
| 163 |
headless = true
|
| 164 |
-
port =
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 260 |
|
| 261 |
st.markdown("---")
|
| 262 |
|
| 263 |
-
# System status area
|
| 264 |
-
with st.expander("System status & Run instructions", expanded=
|
| 265 |
st.write(f"Data file: `{DATA_FILE}`")
|
|
|
|
| 266 |
st.write(f"Model (OPENAI_MODEL): `{DEFAULT_MODEL}`")
|
| 267 |
-
|
| 268 |
-
st.write("
|
| 269 |
-
st.write("
|
| 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"
|
|
|
|
| 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(
|
| 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.
|
| 374 |
-
|
|
|
|
|
|
|
| 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(
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|