triazai-api / app.py
simikkk's picture
Upload app.py
13a7b73 verified
Raw
History Blame Contribute Delete
13.1 kB
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()