Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,80 +11,93 @@ from PIL import Image
|
|
| 11 |
from groq import Groq
|
| 12 |
from fpdf import FPDF
|
| 13 |
from datetime import datetime
|
|
|
|
| 14 |
|
| 15 |
-
# --- 1.
|
| 16 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 17 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
| 18 |
|
| 19 |
if not GROQ_API_KEY or not GOOGLE_API_KEY:
|
| 20 |
-
st.error("β οΈ API Keys Missing!
|
| 21 |
st.stop()
|
| 22 |
|
| 23 |
genai.configure(api_key=GOOGLE_API_KEY)
|
| 24 |
vision_model = genai.GenerativeModel('gemini-2.0-flash')
|
| 25 |
|
| 26 |
-
st.set_page_config(page_title="
|
| 27 |
|
| 28 |
-
# --- 2. PERSISTENT
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
def
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
"Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 39 |
-
"Doctor": doctor,
|
| 40 |
-
"RoomID": room,
|
| 41 |
-
"Summary": chat_summary
|
| 42 |
-
}])
|
| 43 |
-
pd.concat([df, new_entry]).to_csv(RECORDS_FILE, index=False)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
@st.cache_resource
|
| 46 |
-
def
|
| 47 |
client = chromadb.PersistentClient(path="./med_vector_db")
|
| 48 |
emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
if
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
st.stop()
|
| 76 |
|
| 77 |
-
# --- 5. NAVIGATION ---
|
| 78 |
with st.sidebar:
|
| 79 |
-
st.
|
| 80 |
-
|
|
|
|
|
|
|
| 81 |
st.divider()
|
| 82 |
if st.session_state.role == "Patient":
|
| 83 |
-
nav = st.radio("
|
| 84 |
else:
|
| 85 |
-
nav = st.radio("
|
| 86 |
st.divider()
|
| 87 |
-
st.info("System
|
| 88 |
|
| 89 |
# --- 6. PATIENT PORTAL MODULE ---
|
| 90 |
if st.session_state.role == "Patient":
|
|
@@ -92,72 +105,76 @@ if st.session_state.role == "Patient":
|
|
| 92 |
st.header("π¬ Clinical AI Assistant")
|
| 93 |
for m in st.session_state.msgs: st.chat_message(m["role"]).write(m["content"])
|
| 94 |
|
| 95 |
-
q = st.chat_input("
|
| 96 |
c1, c2 = st.columns(2)
|
| 97 |
-
with c1:
|
| 98 |
-
with c2:
|
| 99 |
|
| 100 |
-
|
| 101 |
-
if
|
| 102 |
-
with st.spinner("
|
| 103 |
-
if
|
| 104 |
-
with pdfplumber.open(
|
| 105 |
-
else:
|
| 106 |
-
elif
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
|
| 117 |
st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
|
| 118 |
st.rerun()
|
| 119 |
|
| 120 |
-
elif nav == "π§ͺ
|
| 121 |
-
st.header("π§ͺ Interactive
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
bpm = st.slider("Current Heart Rate (BPM)", 40, 200, 72); st.metric("Pulse", bpm)
|
| 125 |
-
with col2:
|
| 126 |
-
sugar = st.slider("Blood Glucose (mg/dL)", 50, 400, 95); st.metric("Glucose", sugar)
|
| 127 |
-
st.image("https://www.verywellfit.com/thmb/p2O_L3uS9m7O3r4E1uG2F3G_Y3I=/1500x0/filters:no_upscale():max_bytes(150000):strip_icc()/HeartRateChart-5b8d5a1bc9e77c00507a2a1a.jpg", use_container_width=True)
|
| 128 |
|
| 129 |
elif nav == "π Contact Doctor":
|
| 130 |
-
st.header("π
|
| 131 |
-
|
| 132 |
-
if st.button("
|
| 133 |
-
room = f"
|
| 134 |
-
signal_col.upsert(documents=[f"CALL|{
|
| 135 |
-
st.session_state.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
elif nav == "π My Records":
|
| 141 |
-
st.header("π My
|
| 142 |
-
|
| 143 |
-
|
| 144 |
|
| 145 |
# --- 7. DOCTOR PORTAL MODULE ---
|
| 146 |
elif st.session_state.role == "Doctor":
|
| 147 |
-
if nav == "π₯οΈ
|
| 148 |
-
st.header("π₯οΈ
|
| 149 |
res = signal_col.get(ids=["latest"])
|
| 150 |
if res['documents']:
|
| 151 |
parts = res['documents'][0].split("|")
|
| 152 |
-
st.warning(f"π
|
| 153 |
-
if st.button("
|
| 154 |
-
st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px"
|
| 155 |
-
if st.button("β
Archive Session
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
| 11 |
from groq import Groq
|
| 12 |
from fpdf import FPDF
|
| 13 |
from datetime import datetime
|
| 14 |
+
import hashlib
|
| 15 |
|
| 16 |
+
# --- 1. SECURE CONFIGURATION ---
|
| 17 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 18 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
| 19 |
|
| 20 |
if not GROQ_API_KEY or not GOOGLE_API_KEY:
|
| 21 |
+
st.error("β οΈ API Keys Missing! Add them to Space Secrets.")
|
| 22 |
st.stop()
|
| 23 |
|
| 24 |
genai.configure(api_key=GOOGLE_API_KEY)
|
| 25 |
vision_model = genai.GenerativeModel('gemini-2.0-flash')
|
| 26 |
|
| 27 |
+
st.set_page_config(page_title="HealthConnect | Hassan Naseer", layout="wide", page_icon="π₯")
|
| 28 |
|
| 29 |
+
# --- 2. PERSISTENT STORAGE (USER & CLINICAL DB) ---
|
| 30 |
+
USER_DB = "users_secure.csv"
|
| 31 |
+
HISTORY_DB = "permanent_clinical_records.csv"
|
| 32 |
|
| 33 |
+
def hash_pass(password):
|
| 34 |
+
return hashlib.sha256(str.encode(password)).hexdigest()
|
| 35 |
+
|
| 36 |
+
def load_db(file, cols):
|
| 37 |
+
if os.path.exists(file): return pd.read_csv(file)
|
| 38 |
+
return pd.DataFrame(columns=cols)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
def save_user(username, password, role):
|
| 41 |
+
df = load_db(USER_DB, ["username", "password", "role"])
|
| 42 |
+
if username in df['username'].values: return False
|
| 43 |
+
new_user = pd.DataFrame([{"username": username, "password": hash_pass(password), "role": role}])
|
| 44 |
+
pd.concat([df, new_user]).to_csv(USER_DB, index=False)
|
| 45 |
+
return True
|
| 46 |
+
|
| 47 |
+
# --- 3. VECTOR DATABASE ENGINE ---
|
| 48 |
@st.cache_resource
|
| 49 |
+
def get_db():
|
| 50 |
client = chromadb.PersistentClient(path="./med_vector_db")
|
| 51 |
emb_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
| 52 |
+
main = client.get_or_create_collection(name="nutech_final_rag", embedding_function=emb_fn)
|
| 53 |
+
signal = client.get_or_create_collection(name="global_call_signals", embedding_function=emb_fn)
|
| 54 |
+
return main, signal
|
| 55 |
+
|
| 56 |
+
main_col, signal_col = get_db()
|
| 57 |
+
|
| 58 |
+
# --- 4. AUTHENTICATION GATEWAY ---
|
| 59 |
+
if "logged_in" not in st.session_state: st.session_state.logged_in = False
|
| 60 |
+
|
| 61 |
+
if not st.session_state.logged_in:
|
| 62 |
+
st.markdown("<h1 style='text-align: center;'>π₯ HealthConnect Enterprise</h1>", unsafe_allow_html=True)
|
| 63 |
+
tab1, tab2 = st.tabs(["π Login", "π Create Account"])
|
| 64 |
+
|
| 65 |
+
with tab2:
|
| 66 |
+
new_u = st.text_input("Choose Username")
|
| 67 |
+
new_p = st.text_input("Choose Password", type="password")
|
| 68 |
+
new_role = st.selectbox("I am a:", ["Patient", "Doctor"])
|
| 69 |
+
if st.button("Register Account"):
|
| 70 |
+
if save_user(new_u, new_p, new_role): st.success("Account Created! Please Login.")
|
| 71 |
+
else: st.error("Username already taken.")
|
| 72 |
+
|
| 73 |
+
with tab1:
|
| 74 |
+
u = st.text_input("Username")
|
| 75 |
+
p = st.text_input("Password", type="password")
|
| 76 |
+
if st.button("Login"):
|
| 77 |
+
users = load_db(USER_DB, ["username", "password", "role"])
|
| 78 |
+
match = users[(users['username'] == u) & (users['password'] == hash_pass(p))]
|
| 79 |
+
if not match.empty:
|
| 80 |
+
st.session_state.logged_in = True
|
| 81 |
+
st.session_state.username = u
|
| 82 |
+
st.session_state.role = match.iloc[0]['role']
|
| 83 |
+
st.session_state.msgs = [] # Fresh session chat
|
| 84 |
+
st.rerun()
|
| 85 |
+
else: st.error("Invalid credentials.")
|
| 86 |
st.stop()
|
| 87 |
|
| 88 |
+
# --- 5. PORTAL NAVIGATION ---
|
| 89 |
with st.sidebar:
|
| 90 |
+
st.markdown(f"### Welcome, **{st.session_state.username}**")
|
| 91 |
+
st.caption(f"Role: {st.session_state.role}")
|
| 92 |
+
if st.button("Sign Out"):
|
| 93 |
+
st.session_state.logged_in = False; st.rerun()
|
| 94 |
st.divider()
|
| 95 |
if st.session_state.role == "Patient":
|
| 96 |
+
nav = st.radio("Menu", ["π¬ AI Chat & Docs", "π§ͺ Health Lab", "π Contact Doctor", "πΈ Vision Scanner", "π My Records"])
|
| 97 |
else:
|
| 98 |
+
nav = st.radio("Menu", ["π₯οΈ Tele-Consult Desk", "π Patient Archives"])
|
| 99 |
st.divider()
|
| 100 |
+
st.info("System: Production Ready")
|
| 101 |
|
| 102 |
# --- 6. PATIENT PORTAL MODULE ---
|
| 103 |
if st.session_state.role == "Patient":
|
|
|
|
| 105 |
st.header("π¬ Clinical AI Assistant")
|
| 106 |
for m in st.session_state.msgs: st.chat_message(m["role"]).write(m["content"])
|
| 107 |
|
| 108 |
+
q = st.chat_input("Enter symptoms...")
|
| 109 |
c1, c2 = st.columns(2)
|
| 110 |
+
with c1: v = st.audio_input("Voice Input", key=f"v_{len(st.session_state.msgs)}")
|
| 111 |
+
with c2: up = st.file_uploader("Upload Medical PDF/IMG", type=['pdf', 'png', 'jpg'])
|
| 112 |
|
| 113 |
+
fq = q if q else None
|
| 114 |
+
if up:
|
| 115 |
+
with st.spinner("Processing..."):
|
| 116 |
+
if up.type == "application/pdf":
|
| 117 |
+
with pdfplumber.open(up) as f: fq = "Review PDF: " + " ".join([p.extract_text() for p in f.pages if p.extract_text()])
|
| 118 |
+
else: fq = "Review Scan: " + " ".join(easyocr.Reader(['en']).readtext(np.array(Image.open(up)), detail=0))
|
| 119 |
+
elif v:
|
| 120 |
+
fq = Groq(api_key=GROQ_API_KEY).audio.transcriptions.create(file=("a.wav", v.getvalue()), model="whisper-large-v3", response_format="text")
|
| 121 |
+
|
| 122 |
+
if fq:
|
| 123 |
+
st.session_state.msgs.append({"role": "user", "content": fq})
|
| 124 |
+
# --- INDEX ERROR SAFETY FIX ---
|
| 125 |
+
res = main_col.query(query_texts=[fq], n_results=1)
|
| 126 |
+
if res.get('documents') and len(res['documents']) > 0 and len(res['documents'][0]) > 0:
|
| 127 |
+
ctx = res['documents'][0][0]
|
| 128 |
+
else:
|
| 129 |
+
ctx = "No specific match in local database."
|
| 130 |
+
# ------------------------------
|
| 131 |
ans = Groq(api_key=GROQ_API_KEY).chat.completions.create(model="llama-3.3-70b-versatile", messages=[{"role": "system", "content": f"Context: {ctx}"}] + st.session_state.msgs)
|
| 132 |
st.session_state.msgs.append({"role": "assistant", "content": ans.choices[0].message.content})
|
| 133 |
st.rerun()
|
| 134 |
|
| 135 |
+
elif nav == "π§ͺ Health Lab":
|
| 136 |
+
st.header("π§ͺ Interactive Diagnostics")
|
| 137 |
+
bpm = st.slider("Heart Rate (BPM)", 40, 200, 72); st.metric("Pulse", bpm)
|
| 138 |
+
sugar = st.slider("Blood Glucose (mg/dL)", 50, 400, 95); st.metric("Level", sugar)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
elif nav == "π Contact Doctor":
|
| 141 |
+
st.header("π Request Live Consultation")
|
| 142 |
+
doc = st.selectbox("Choose Doctor", ["Dr_Ahmed", "Dr_Sara"])
|
| 143 |
+
if st.button("Start Connection"):
|
| 144 |
+
room = f"Room-{st.session_state.username}-{np.random.randint(100,999)}"
|
| 145 |
+
signal_col.upsert(documents=[f"CALL|{doc}|{room}|{st.session_state.username}"], ids=["latest"])
|
| 146 |
+
st.session_state.room = room
|
| 147 |
+
if "room" in st.session_state:
|
| 148 |
+
st.components.v1.html(f'<iframe src="https://meet.jit.si/{st.session_state.room}" width="100%" height="550px"></iframe>', height=600)
|
| 149 |
+
|
| 150 |
+
elif nav == "πΈ Vision Scanner":
|
| 151 |
+
cam = st.camera_input("Scanner")
|
| 152 |
+
if cam:
|
| 153 |
+
rep = vision_model.generate_content(["ACT AS PHARMACIST: Salt, Dosage.", Image.open(cam)])
|
| 154 |
+
st.write(rep.text)
|
| 155 |
|
| 156 |
elif nav == "π My Records":
|
| 157 |
+
st.header("π My Visit History")
|
| 158 |
+
df = load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"])
|
| 159 |
+
st.table(df[df['Patient'] == st.session_state.username])
|
| 160 |
|
| 161 |
# --- 7. DOCTOR PORTAL MODULE ---
|
| 162 |
elif st.session_state.role == "Doctor":
|
| 163 |
+
if nav == "π₯οΈ Tele-Consult Desk":
|
| 164 |
+
st.header("π₯οΈ Active Incoming Desk")
|
| 165 |
res = signal_col.get(ids=["latest"])
|
| 166 |
if res['documents']:
|
| 167 |
parts = res['documents'][0].split("|")
|
| 168 |
+
st.warning(f"π CALL: Incoming from {parts[3]}")
|
| 169 |
+
if st.button("Join Room"):
|
| 170 |
+
st.components.v1.html(f'<iframe src="https://meet.jit.si/{parts[2]}" width="100%" height="600px"></iframe>', height=650)
|
| 171 |
+
if st.button("β
Archive Session"):
|
| 172 |
+
df = load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"])
|
| 173 |
+
new_row = pd.DataFrame([{"Time": datetime.now().strftime("%Y-%m-%d %H:%M"), "Patient": parts[3], "Doctor": st.session_state.username, "Status": "Completed"}])
|
| 174 |
+
pd.concat([df, new_row]).to_csv(HISTORY_DB, index=False)
|
| 175 |
+
signal_col.delete(ids=["latest"]); st.success("Session Saved!"); st.rerun()
|
| 176 |
+
else: st.info("Monitoring for patient requests...")
|
| 177 |
+
|
| 178 |
+
elif nav == "π Patient Archives":
|
| 179 |
+
st.header("π Global Clinical Records")
|
| 180 |
+
st.dataframe(load_db(HISTORY_DB, ["Time", "Patient", "Doctor", "Status"]), use_container_width=True)
|