Spaces:
Running
Running
| 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 | <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() |