import gradio as gr import json import os import re import requests from datetime import datetime, date from typing import Optional # ── Synthetic patient database ───────────────────────────────────────────────── PATIENT_RECORDS = { "P001": { "name": "Jana Nováková", "age": 67, "gender": "F", "history": [ {"date": "2024-05-10", "event": "CABG bypass operace srdce"}, {"date": "2024-11-02", "event": "Kontrola kardiologie – stabilní stav"}, ], "medications": ["Warfarin 5mg", "Metoprolol 50mg", "Atorvastatin 20mg"], "allergies": ["Penicilin"], "blood_type": "A+", }, "P002": { "name": "Tomáš Kratochvíl", "age": 34, "gender": "M", "history": [ {"date": "2025-01-15", "event": "Zlomenina levého zápěstí – ošetřena"}, ], "medications": [], "allergies": [], "blood_type": "0-", }, "P003": { "name": "Marta Horáčková", "age": 52, "gender": "F", "history": [ {"date": "2025-04-20", "event": "Appendektomie"}, {"date": "2025-04-28", "event": "Kontrola po operaci – hojení v pořádku"}, ], "medications": ["Metformin 1000mg", "Ramipril 5mg"], "allergies": ["Ibuprofen"], "blood_type": "B+", }, "P004": { "name": "Karel Dvořák", "age": 71, "gender": "M", "history": [ {"date": "2025-12-01", "event": "Diabetes mellitus 2. typu – dispenzarizace"}, ], "medications": ["Inzulin Lantus", "Aspirin 100mg"], "allergies": [], "blood_type": "A-", }, } EPIDEMIOLOGY = { "current_date": str(date.today()), "alerts": [ "Zvýšený výskyt chřipky A (H1N1) – týden 24/2026, kraj Plzeňský: +34 % oproti průměru", "Lokální gastroenteritida – hlášeno 12 případů z Plzně-sever, pravděpodobně norovirus", ], "season": "léto", } # ── Patient queue (in-memory) ────────────────────────────────────────────────── patient_queue = [] # ── OpenRouter ───────────────────────────────────────────────────────────────── # Free models (no credit card required on openrouter.ai): # meta-llama/llama-3.3-70b-instruct:free ← doporučeno, nejlepší kvalita zdarma # google/gemma-3-27b-it:free # mistralai/mistral-7b-instruct:free ← záloha pokud 70B má frontu OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" FREE_MODEL = "meta-llama/llama-3.3-70b-instruct:free" TRIAGE_SYSTEM_PROMPT = """Jsi specializovaný triážní agent pro urgentní příjem v nemocnici. Pracuješ podle platného českého triážního standardu Manchester Triage System (MTS). PRIORITY (Manchester Triage System): - P1 RESUSCITACE (červená): okamžité ohrožení života, čas do ošetření: ihned - P2 URGENTNÍ (oranžová): potenciální ohrožení života, čas do ošetření: max 10 minut - P3 NALÉHAVÉ (žlutá): závažný stav, čas do ošetření: max 60 minut - P4 MÉNĚ NALÉHAVÉ (zelená): stabilní stav, čas do ošetření: max 120 minut - P5 NEURGENTNÍ (modrá): plánovaná péče, čas do ošetření: max 240 minut KLÍČOVÁ PRAVIDLA: 1. Vždy upozorni na "red flags" – příznaky skrytě nebezpečných stavů (atypický infarkt, disekce aorty, SAK, apod.) 2. U žen nad 40 let, diabetiků a starších pacientů VŽDY zvažuj atypické prezentace infarktu 3. Pokud jsi nejistý, raději eskaluj na vyšší prioritu (undertriage je nebezpečnější než overtriage) 4. Vysvětli PROČ jsi přiřadil danou prioritu – lékař musí rozumět tvému rozhodnutí 5. Nikdy nerozhoduješ sám – tvůj výstup je PODKLAD pro lékaře, ne konečné rozhodnutí DŮLEŽITÉ: Odpovídej POUZE čistým JSON objektem. Žádný text před ani za. Žádné ```json bloky. Přesný formát výstupu: { "priority": "P2", "priority_label": "URGENTNÍ", "priority_color": "orange", "confidence": "vysoká", "main_concern": "Jedna věta s hlavním důvodem priority", "red_flags": ["Red flag 1", "Red flag 2"], "differential_diagnoses": [ {"diagnosis": "Název diagnózy", "probability": "vysoká", "reasoning": "Stručné zdůvodnění"}, {"diagnosis": "Název diagnózy", "probability": "střední", "reasoning": "Stručné zdůvodnění"} ], "recommended_immediate_actions": ["Akce 1", "Akce 2", "Akce 3"], "escalate_to_doctor": true, "escalation_reason": "Důvod proč přivolat lékaře, nebo null", "data_sources_used": ["příznaky od recepce", "kartotéka pacienta", "epidemiologický kontext"] }""" def call_openrouter(user_message: str) -> dict: api_key = os.environ.get("OPENROUTER_API_KEY") if not api_key: raise ValueError( "OPENROUTER_API_KEY není nastaven. Jdi na Settings → Secrets a přidej ho." ) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://huggingface.co", "X-Title": "TriazAI", } payload = { "model": FREE_MODEL, "messages": [ {"role": "system", "content": TRIAGE_SYSTEM_PROMPT}, {"role": "user", "content": user_message}, ], "max_tokens": 1200, "temperature": 0.1, } resp = requests.post(OPENROUTER_URL, headers=headers, json=payload, timeout=60) resp.raise_for_status() raw = resp.json()["choices"][0]["message"]["content"].strip() # Strip markdown fences if model ignores instructions raw = re.sub(r"^```(?:json)?\s*", "", raw) raw = re.sub(r"\s*```$", "", raw) raw = raw.strip() # Find JSON object in response (fallback if model adds prose) match = re.search(r"\{[\s\S]*\}", raw) if match: raw = match.group(0) return json.loads(raw) def run_triage_agent( patient_data: dict, medical_record: Optional[dict], symptoms: str, vitals: dict, ) -> dict: record_text = ( json.dumps(medical_record, ensure_ascii=False, indent=2) if medical_record else "Kartotéka nedostupná – nový pacient bez záznamu" ) user_message = f"""NOVÝ PACIENT K TRIÁŽI: Základní údaje: - Jméno: {patient_data.get("name", "Neznámý")} - Věk: {patient_data.get("age", "Neznámý")} let - Pohlaví: {patient_data.get("gender", "Neznámé")} - Čas příchodu: {datetime.now().strftime("%H:%M")} Příznaky (popsala recepce): {symptoms} Vitální funkce: {json.dumps(vitals, ensure_ascii=False, indent=2)} Zdravotní kartotéka: {record_text} Epidemiologická situace: {json.dumps(EPIDEMIOLOGY, ensure_ascii=False, indent=2)} Proveď triáž. Vrať POUZE JSON objekt.""" return call_openrouter(user_message) # ── API functions ────────────────────────────────────────────────────────────── def lookup_patient(patient_id: str): if not patient_id or not patient_id.strip(): return "{}", "⚠️ Zadejte ID pacienta" record = PATIENT_RECORDS.get(patient_id.strip().upper()) if record: return json.dumps(record, ensure_ascii=False), f"✅ Nalezeno: {record['name']}" return "{}", "⚠️ Pacient nenalezen – nový pacient nebo chybné ID" def add_patient_to_queue( patient_id, name, age, gender, symptoms, bp_sys, bp_dia, hr, spo2, temp, gcs, ): if not symptoms or not symptoms.strip(): return "❌ Příznaky jsou povinné.", json.dumps(patient_queue, ensure_ascii=False) pid_clean = (patient_id or "").strip().upper() medical_record = PATIENT_RECORDS.get(pid_clean) if pid_clean else None patient_data = { "id": pid_clean if pid_clean else f"TEMP_{len(patient_queue)+1:03d}", "name": (name or "Neznámý").strip(), "age": int(age) if age else 0, "gender": gender or "X", "arrival_time": datetime.now().strftime("%H:%M"), } vitals = { "tlak": f"{int(bp_sys or 0)}/{int(bp_dia or 0)} mmHg", "puls": f"{int(hr or 0)} /min", "SpO2": f"{float(spo2 or 98)} %", "teplota": f"{float(temp or 36.8)} °C", "GCS": int(gcs or 15), } try: triage_result = run_triage_agent(patient_data, medical_record, symptoms, vitals) except Exception as e: return f"❌ Chyba triáže: {str(e)}", json.dumps(patient_queue, ensure_ascii=False) entry = { **patient_data, "symptoms": symptoms, "vitals": vitals, "medical_record": medical_record, "triage": triage_result, } patient_queue.append(entry) order = {"P1": 0, "P2": 1, "P3": 2, "P4": 3, "P5": 4} patient_queue.sort(key=lambda x: order.get(x["triage"].get("priority", "P5"), 4)) t = triage_result msg = f"✅ {patient_data['name']} přidán – {t.get('priority','?')} {t.get('priority_label','')}" return msg, json.dumps(patient_queue, ensure_ascii=False) def get_queue_json(): return json.dumps(patient_queue, ensure_ascii=False) def clear_patient(patient_id: str): global patient_queue pid = (patient_id or "").strip().upper() before = len(patient_queue) patient_queue = [p for p in patient_queue if p["id"] != pid] if len(patient_queue) < before: return f"✅ Pacient {pid} odebrán z fronty.", json.dumps(patient_queue, ensure_ascii=False) return f"⚠️ ID {pid} nenalezeno.", json.dumps(patient_queue, ensure_ascii=False) # ── Gradio UI ────────────────────────────────────────────────────────────────── with gr.Blocks(title="TriážAI API", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # TriážAI – Backend API Backend pro HTML frontend. Volá **OpenRouter** (free tier) – model: `meta-llama/llama-3.3-70b-instruct:free` > **Setup:** Přidej `OPENROUTER_API_KEY` do **Settings → Variables and Secrets** tohoto Space. > Klíč získáš zdarma na [openrouter.ai](https://openrouter.ai) po registraci. Přímé API endpointy pro HTML frontend: - `POST /call/lookup_patient` - `POST /call/add_patient_to_queue` - `POST /call/get_queue_json` - `POST /call/clear_patient` """) with gr.Tab("🔍 Vyhledat pacienta"): pid_in = gr.Textbox(label="ID pacienta", placeholder="P001, P002, P003 nebo P004") lookup_btn = gr.Button("Vyhledat", variant="secondary") with gr.Row(): stat_out = gr.Textbox(label="Status") rec_out = gr.Textbox(label="Záznam (JSON)", lines=12) lookup_btn.click(lookup_patient, inputs=[pid_in], outputs=[rec_out, stat_out]) with gr.Tab("🚨 Přidat pacienta (test)"): with gr.Row(): pid2 = gr.Textbox(label="ID pacienta", placeholder="P001 nebo nechej prázdné") pname = gr.Textbox(label="Jméno", placeholder="Jan Novák") page = gr.Number(label="Věk", value=50) pgender = gr.Radio(["M", "F", "X"], label="Pohlaví", value="M") psymptoms = gr.Textbox( label="Příznaky", lines=4, placeholder="Pacient udává bolest na hrudi vyzařující do levé paže, trvá 20 minut, nausea..." ) with gr.Row(): bps = gr.Number(label="TK systola", value=140) bpd = gr.Number(label="TK diastola", value=90) phr = gr.Number(label="Puls /min", value=95) pspo2 = gr.Number(label="SpO2 %", value=97) ptemp = gr.Number(label="Teplota °C", value=36.8) pgcs = gr.Number(label="GCS", value=15) add_btn = gr.Button("🚨 Odeslat k triáži", variant="primary") add_status = gr.Textbox(label="Výsledek") queue_out1 = gr.Textbox(label="Fronta po přidání (JSON)", lines=10) add_btn.click( add_patient_to_queue, inputs=[pid2, pname, page, pgender, psymptoms, bps, bpd, phr, pspo2, ptemp, pgcs], outputs=[add_status, queue_out1], ) with gr.Tab("👨‍⚕️ Fronta (doktor)"): refresh_btn = gr.Button("🔄 Obnovit frontu", variant="secondary") queue_out2 = gr.Textbox(label="Aktuální fronta (JSON)", lines=20) refresh_btn.click(get_queue_json, outputs=[queue_out2]) with gr.Row(): clr_id = gr.Textbox(label="ID pacienta k odebrání") clr_btn = gr.Button("✅ Označit jako ošetřeného", variant="secondary") clr_status = gr.Textbox(label="Status") clr_btn.click(clear_patient, inputs=[clr_id], outputs=[clr_status, queue_out2]) demo.launch()