File size: 13,483 Bytes
ac21e89
 
 
 
 
 
 
 
 
 
 
 
 
952e2b3
ac21e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d66dd9
 
 
 
ac21e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d66dd9
ac21e89
 
 
3d66dd9
 
 
 
 
 
 
 
ac21e89
3d66dd9
 
ac21e89
3d66dd9
ac21e89
 
 
3d66dd9
ac21e89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d66dd9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
import os
import base64
import tempfile
import gradio as gr
from openai import OpenAI
import fitz  # PyMuPDF

# ── NVIDIA Build client (key stored in HF Secret) ─────────────────────────────
client = OpenAI(
    base_url="https://integrate.api.nvidia.com/v1",
    api_key=os.environ.get("NVIDIA_API_KEY"),
)

MODEL = "openai/gpt-oss-20b"  # GPT via NVIDIA Build — change this to match your NVIDIA model name aka salman's lair

# ── System prompts ────────────────────────────────────────────────────────────
SYSTEM_PROMPTS = {
    "🩺 General Diagnosis": """You are Sehat Guard, an expert AI medical assistant for Pakistan.
Help patients understand symptoms and lab results.

RULES:
- Respond in the SAME LANGUAGE as the user (English or Urdu script).
- When a lab report is shared, analyze ALL values, flag abnormal ones (HIGH/LOW/NORMAL), explain in simple language, give overall interpretation.
- Structure: 📋 Lab Analysis | ⚠️ Abnormal Values | 💡 What This Means | 🩺 Possible Conditions | ✅ Recommendations | 🏥 When to See a Doctor.
- Consider Pakistani diseases: dengue, typhoid, TB, malaria, hepatitis B/C.
- Emergency numbers: 1122 (Rescue) | 115 (Edhi).
- End with: AI guidance only, not a substitute for professional medical care.""",

    "👨‍⚕️ Doctor Mode": """You are Sehat Guard DOCTOR MODE — clinical decision support for licensed physicians in Pakistan.
When lab reports are uploaded:
- Interpret all values with clinical precision
- Flag critical values requiring immediate action
- Differential diagnosis based on the panel
- Suggest additional investigations
- Evidence-based treatment (WHO/DRAP guidelines)
- Drug dosages, interactions, contraindications
- ICD-10 codes where relevant
Respond in the same language as the user.""",

    "🚨 Emergency Triage": """You are Sehat Guard TRIAGE MODULE.
When lab results or symptoms are shared:
- Flag any CRITICAL values immediately (K+ >6.5, Hb <5, Glucose >600, elevated Troponin)
- Assign: 🔴 IMMEDIATE / 🟡 URGENT / 🟢 NON-URGENT
- State exact reason for triage level
- Immediate action steps
- Pakistan emergency: 1122 | 115
Be direct and action-oriented. Respond in user's language.""",

    "💊 Medications": """You are Sehat Guard MEDICATION MODULE.
When lab results or symptoms are shared:
- Identify conditions and recommend medications available in Pakistan
- Generic + brand names, dosage, frequency, duration
- Contraindications based on abnormal values
- Affordable alternatives for Pakistani patients
Prescriptions require a licensed doctor.
Respond in user's language.""",
}

DISCLAIMER = (
    "⚠️ **Medical Disclaimer:** Sehat Guard is for informational guidance only. "
    "Always consult a licensed doctor. "
    "**Emergencies:** 1122 (Rescue) | 115 (Edhi)"
)

# ── Helpers ───────────────────────────────────────────────────────────────────
def encode_image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

def pdf_to_base64_images(pdf_path: str) -> list:
    doc = fitz.open(pdf_path)
    images = []
    for page in doc:
        pix = page.get_pixmap(dpi=200)
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
            pix.save(tmp.name)
            images.append(encode_image_to_base64(tmp.name))
    doc.close()
    return images

def build_messages(system, history, user_text, image_b64_list=None):
    messages = [{"role": "system", "content": system}]
    for human, assistant in history:
        if human is not None:
            messages.append({"role": "user",      "content": str(human)})
        if assistant is not None:
            messages.append({"role": "assistant", "content": str(assistant)})

    if image_b64_list:
        content = []
        for b64 in image_b64_list:
            content.append({
                "type": "image_url",
                "image_url": {"url": f"data:image/png;base64,{b64}", "detail": "high"},
            })
        content.append({"type": "text", "text": user_text})
        messages.append({"role": "user", "content": content})
    else:
        messages.append({"role": "user", "content": user_text})

    return messages

# ── Streaming chat function ───────────────────────────────────────────────────
def chat(message, history, mode, file_upload, age, gender, weight, conditions, allergies):
    if not message.strip() and file_upload is None:
        yield "", history
        return

    system = SYSTEM_PROMPTS.get(mode, SYSTEM_PROMPTS["🩺 General Diagnosis"])

    # Patient context
    parts = []
    if age:        parts.append(f"Age: {int(age)}y")
    if gender:     parts.append(f"Gender: {gender}")
    if weight:     parts.append(f"Weight: {int(weight)}kg")
    if conditions: parts.append(f"Conditions: {conditions}")
    if allergies:  parts.append(f"Allergies: {allergies}")
    patient_ctx = ("\n\n[Patient Info — " + " | ".join(parts) + "]") if parts else ""

    user_text    = (message.strip() or "Please analyze this uploaded medical document and provide a detailed interpretation.") + patient_ctx
    display_msg  = message.strip() or "📄 File uploaded — please analyze"

    try:
        # NVIDIA Build does not support vision — text only
        if file_upload is not None:
            ext = os.path.splitext(file_upload)[1].lower()
            if ext == ".pdf":
                # Extract text from PDF instead
                doc = fitz.open(file_upload)
                pdf_text = ""
                for page in doc:
                    pdf_text += page.get_text()
                doc.close()
                user_text = f"The patient has uploaded a lab report. Here is the extracted text:\n\n{pdf_text[:4000]}\n\n{user_text}"
                display_msg = "📄 PDF uploaded" + (f" — {message.strip()}" if message.strip() else "")
            elif ext in [".jpg", ".jpeg", ".png", ".webp", ".bmp"]:
                user_text = "The patient has uploaded an image of a medical document (lab report/prescription/X-ray). Unfortunately I cannot read images directly — please type out the key values from the report and I will analyze them for you.\n\n" + user_text
                display_msg = "🖼️ Image uploaded" + (f" — {message.strip()}" if message.strip() else "")
            else:
                history.append((message or "File upload", "❌ Unsupported file type. Please upload PDF, JPG, or PNG."))
                yield "", history
                return

        messages = build_messages(system, history, user_text)

        # ── Streaming response ──
        stream = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            max_tokens=1500,
            temperature=0.3,
            stream=True,
        )

        partial_reply = ""
        history = history + [(display_msg, "")]

        for chunk in stream:
            if not getattr(chunk, "choices", None):
                continue
            delta = chunk.choices[0].delta
            token = getattr(delta, "content", None)
            if token:
                partial_reply += token
                history[-1] = (display_msg, partial_reply)
                yield "", history

    except Exception as e:
        err = f"❌ Error: {str(e)}\n\nCheck your `NVIDIA_API_KEY` secret in Hugging Face Space Settings."
        history.append((display_msg, err))
        yield "", history

# ── Gradio UI ─────────────────────────────────────────────────────────────────
CSS = """
body { background: #f0f7f3; }
.gradio-container { max-width: 1150px !important; margin: 0 auto; }
#header {
    background: linear-gradient(135deg, #0f4230 0%, #1a6b4a 60%, #27a96b 100%);
    padding: 24px 32px; border-radius: 16px; margin-bottom: 16px; text-align: center;
}
#header h1 { color: #fff; font-size: 2rem; margin: 0; }
#header p  { color: #a8dfc2; font-size: 0.9rem; margin: 6px 0 0; }
#disclaimer {
    background: #fff8e1; border: 1.5px solid #f9c84a;
    border-radius: 10px; padding: 10px 16px;
    font-size: 0.82rem; color: #5a4000; margin-bottom: 14px;
}
.message.user > div {
    background: #1a6b4a !important; color: #fff !important;
    border-radius: 18px 18px 4px 18px !important;
}
.message.bot > div {
    background: #e8f5ee !important; color: #1c2c24 !important;
    border-radius: 18px 18px 18px 4px !important;
    border: 1px solid #d0e4d8 !important;
}
"""

WELCOME = (
    "👋 Hello! I'm **Sehat Guard** 🛡️ — your AI medical assistant for Pakistan.\n\n"
    "**I can help with:**\n"
    "• 📋 **Lab report analysis** — upload PDF or photo\n"
    "• 🩺 Symptom assessment & diagnosis\n"
    "• 🚨 Emergency triage (🔴🟡🟢)\n"
    "• 💊 Medication information\n"
    "• 👨‍⚕️ Clinical support for doctors\n\n"
    "Type in **English** or **Urdu** (اردو). Upload a lab report on the left."
)

with gr.Blocks(css=CSS, title="Sehat Guard — AI Medical Assistant") as demo:

    gr.HTML("""
    <div id="header">
      <h1>🛡️ Sehat Guard &nbsp;|&nbsp; <span style="font-family:serif;font-size:1.5rem">صحت گارڈ</span></h1>
      <p>AI Medical Assistant · Pakistan · Lab Reports · English & Urdu · Powered by NVIDIA Build</p>
    </div>
    """)
    gr.HTML(f'<div id="disclaimer">{DISCLAIMER}</div>')

    with gr.Row():

        # ── Sidebar ───────────────────────────────────────────────────────────
        with gr.Column(scale=1, min_width=270):

            mode = gr.Radio(
                choices=list(SYSTEM_PROMPTS.keys()),
                value="🩺 General Diagnosis",
                label="Consultation Mode",
            )

            gr.Markdown("---\n### 📁 Upload Lab Report / X-Ray / Prescription")
            file_upload = gr.File(
                label="Upload PDF or Image",
                file_types=[".pdf", ".jpg", ".jpeg", ".png", ".webp"],
                file_count="single",
            )
            gr.Markdown("_Supports: CBC, LFT, RFT, HbA1c, X-ray, ECG, ultrasound, prescriptions_")

            gr.Markdown("---\n### 👤 Patient Info *(optional)*")
            age        = gr.Number(label="Age (years)", precision=0, minimum=0, maximum=120)
            gender     = gr.Dropdown(["", "Male", "Female", "Other"], label="Gender", value="")
            weight     = gr.Number(label="Weight (kg)", precision=0, minimum=0)
            conditions = gr.Textbox(label="Known Conditions", placeholder="e.g. Diabetes, Hypertension")
            allergies  = gr.Textbox(label="Allergies",         placeholder="e.g. Penicillin")

            gr.Markdown("""
---
🇵🇰 **Emergency Numbers**
- **1122** — Rescue / Ambulance
- **115** — Edhi Foundation
- **115** — Chippa Welfare
""")

        # ── Chat ──────────────────────────────────────────────────────────────
        with gr.Column(scale=3):

            chatbot = gr.Chatbot(
                value=[[None, WELCOME]],
                label="Sehat Guard Chat",
                height=520,
            )

            with gr.Row():
                user_input = gr.Textbox(
                    placeholder="Describe symptoms or ask a question... / علامات بتائیں یا سوال پوچھیں...",
                    label="Your Message",
                    scale=5,
                    lines=2,
                )
                send_btn = gr.Button("Send ➤", scale=1, variant="primary")

            gr.Examples(
                examples=[
                    ["I have fever 38.5°C, body aches and headache for 2 days. Could it be dengue?"],
                    ["مجھے 2 دن سے بخار ہے اور جسم میں درد ہے، کیا یہ ڈینگی ہو سکتا ہے؟"],
                    ["Please analyze my uploaded CBC report and explain what's abnormal."],
                    ["My HbA1c is 9.2%, fasting glucose 210 mg/dL. What does this mean?"],
                    ["میرا بلڈ پریشر 160/100 ہے، کیا کرنا چاہیے؟"],
                    ["CBC: Hb 7.2, WBC 14,000, Platelets 45,000. What could this indicate?"],
                    ["Triage: 70yo male, sudden severe headache, vomiting, neck stiffness."],
                    ["TSH 12.5 mIU/L, T4 low. What medication is needed in Pakistan?"],
                ],
                inputs=user_input,
                label="💡 Example Questions",
            )

    inputs  = [user_input, chatbot, mode, file_upload, age, gender, weight, conditions, allergies]
    outputs = [user_input, chatbot]

    send_btn.click(fn=chat, inputs=inputs, outputs=outputs)
    user_input.submit(fn=chat, inputs=inputs, outputs=outputs)

demo.launch()