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(""" """) gr.HTML(f'
{DISCLAIMER}
') 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()