File size: 12,091 Bytes
98da1ab
 
 
 
 
a1c9731
 
 
 
 
 
 
 
 
 
98da1ab
a1c9731
98da1ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c9731
 
 
 
 
 
 
 
98da1ab
 
a1c9731
98da1ab
 
 
 
a1c9731
98da1ab
 
a1c9731
98da1ab
 
 
 
 
 
 
 
a1c9731
 
 
 
 
 
 
 
 
98da1ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c9731
98da1ab
 
 
a1c9731
98da1ab
 
a1c9731
98da1ab
 
 
 
 
 
 
a1c9731
98da1ab
 
a1c9731
 
98da1ab
 
a1c9731
98da1ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c9731
98da1ab
 
a1c9731
98da1ab
 
 
 
 
a1c9731
 
98da1ab
 
 
 
a1c9731
98da1ab
 
a1c9731
98da1ab
a1c9731
98da1ab
a1c9731
98da1ab
a1c9731
98da1ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1c9731
 
 
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
"""Araras-Gemma4 — HF Space (Gradio + ZeroGPU)
Clinical decision support copilot for Brazilian rare diseases.

For licensed healthcare professionals.
Submission: Gemma 4 Good Hackathon (Kaggle, May 2026).
"""
import os, re, unicodedata, json
import gradio as gr
import spaces
from llama_cpp import Llama
from huggingface_hub import hf_hub_download

MODEL_REPO = "Raras-AI/araras-gemma4-e4b-v4-gguf"
MODEL_FILE = "araras-gemma4-e4b-v4-Q4_K_M.gguf"

# ─── Canonical ORPHA resolver (subset, 26 CEAF diseases) ───────────────
CANONICAL = [
    (["esclerose multipla","multiple sclerosis","neuromielite optica","mogad","desmielinizante"], "ORPHA:802", "esclerose-multipla", "Esclerose Múltipla", ["betainterferona-1a","natalizumabe","fingolimode","ocrelizumabe"]),
    (["doenca falciforme","anemia falciforme","hbss","sickle cell","drepanocitose","falciforme"], "ORPHA:232", "doenca-falciforme", "Doença Falciforme", ["hidroxiureia","deferasirox","L-glutamina"]),
    (["ame tipo 1","sma tipo 1","spinal muscular atrophy type 1","werdnig","ame tipo i"], "ORPHA:70", "atrofia-muscular-espinhal-5q", "AME Tipo 1 (Werdnig-Hoffmann)", ["nusinersena","onasemnogeno-abeparvoveque"]),
    (["atrofia muscular espinhal","ame 5q","sma 5q","spinal muscular atrophy"], "ORPHA:83330", "atrofia-muscular-espinhal-5q", "Atrofia Muscular Espinhal 5q", ["nusinersena","risdiplam","onasemnogeno-abeparvoveque"]),
    (["hipertensao arterial pulmonar","hipertensao pulmonar","pulmonary hypertension"], "ORPHA:182090", "hipertensao-pulmonar", "Hipertensão Arterial Pulmonar", ["sildenafila","bosentana","ambrisentana","macitentana"]),
    (["fibrose cistica","cystic fibrosis","mucoviscidose","cftr"], "ORPHA:586", "fibrose-cistica", "Fibrose Cística", ["dornase-alfa","tobramicina inalatória","ivacaftor","lumacaftor"]),
    (["fenilcetonuria","pku","phenylketonuria","fenilalaninemia"], "ORPHA:716", "fenilcetonuria", "Fenilcetonúria", ["fórmula metabólica isenta de fenilalanina","sapropterina"]),
    (["doenca de wilson","wilson disease","kayser"], "ORPHA:905", "doenca-de-wilson", "Doença de Wilson", ["d-penicilamina","trientina","zinco-acetato"]),
    (["mucopolissacaridose tipo i","mps i","mps 1","hurler","iduronidase"], "ORPHA:579", "mucopolissacaridose-tipo-i", "MPS I (Hurler)", ["laronidase"]),
    (["mucopolissacaridose tipo ii","mps ii","mps 2","hunter","iduronato"], "ORPHA:580", "mucopolissacaridose-tipo-ii", "MPS II (Hunter)", ["idursulfase-alfa"]),
    (["mucopolissacaridose tipo vi","mps vi","mps 6","maroteaux","arilsulfatase b"], "ORPHA:583", "mucopolissacaridose-tipo-vi", "MPS VI (Maroteaux-Lamy)", ["galsulfase"]),
    (["scid","imunodeficiencia combinada","severe combined immunodeficiency"], "ORPHA:183660", None, "SCID", ["TCTH","IVIg"]),
    (["doenca de gaucher","gaucher disease","glucocerebrosidase"], "ORPHA:355", "doenca-de-gaucher", "Doença de Gaucher", ["imiglucerase","alfa-velaglicerase","alfa-taliglicerase"]),
    (["doenca de pompe","pompe disease","alfa-glicosidase"], "ORPHA:365", "doenca-de-pompe", "Doença de Pompe", ["alfa-alglicosidase"]),
    (["doenca de fabry","fabry disease","alfa-galactosidase"], "ORPHA:324", "doenca-de-fabry", "Doença de Fabry", ["alfa-agalsidase","beta-agalsidase","migalastate"]),
    (["hemoglobinuria paroxistica noturna","hpn","paroxysmal nocturnal"], "ORPHA:447", "hemoglobinuria-paroxistica-noturna", "Hemoglobinúria Paroxística Noturna", ["eculizumabe","ravulizumabe"]),
    (["esclerose lateral amiotrofica","ela","amyotrophic lateral sclerosis"], "ORPHA:803", "esclerose-lateral-amiotrofica", "Esclerose Lateral Amiotrófica", ["riluzol","edaravone"]),
]

def norm(s):
    s = (s or "").lower()
    s = unicodedata.normalize("NFD", s)
    return "".join(c for c in s if unicodedata.category(c) != "Mn")

def resolve(text):
    """Find disease mentions in model output, return list of dicts."""
    text_n = norm(text)
    found, seen = [], set()
    # Per-line parse first
    for line in text.split("\n"):
        line_n = norm(line)
        for kws, orpha, slug, name, drugs in CANONICAL:
            if orpha in seen: continue
            if any(k in line_n for k in kws):
                found.append({"orpha": orpha, "name_pt": name, "pcdt_slug": slug, "ceaf_drugs": drugs})
                seen.add(orpha); break
    # Fallback whole-text
    if not found:
        for kws, orpha, slug, name, drugs in CANONICAL:
            if orpha in seen: continue
            if any(k in text_n for k in kws):
                found.append({"orpha": orpha, "name_pt": name, "pcdt_slug": slug, "ceaf_drugs": drugs})
                seen.add(orpha)
    return found[:3]

_llm = None
def get_llm():
    global _llm
    if _llm is None:
        path = hf_hub_download(MODEL_REPO, MODEL_FILE)
        _llm = Llama(model_path=path, n_ctx=8192, n_gpu_layers=-1, verbose=False)
    return _llm

SYS = """Você é ARARAS, copiloto de apoio à decisão clínica em doenças raras para profissionais de saúde no Brasil.

NÃO substitui diagnóstico — apoia o(a) médico(a) na consulta.

FORMATO OBRIGATÓRIO:
- TEXTO SIMPLES. PROIBIDO LaTeX/$\\mathbf$/fórmulas matemáticas.
- Respostas CURTAS — até 8 linhas por turno.
- Lista numerada com até 3 hipóteses (nome PT-BR + 1 linha justificativa).
- NUNCA invente código ORPHA ou Portaria — apenas escreva o nome canônico em PT-BR.
- Máximo 2 emojis por resposta.
- Para perguntas de exames/encaminhamento/CEAF: responda direto.

DOENÇAS RARAS COMUNS NO BRASIL (priorizar):
Doença de Gaucher, Pompe, Fabry, Mucopolissacaridoses I/II/VI, Doença Falciforme,
AME 5q/tipo 1, Esclerose Múltipla, Hipertensão Pulmonar, Fibrose Cística, PKU,
Wilson, SCID, HPN, ELA, ESPECIALMENTE com sinais clássicos:
- Hepatoesplenomegalia + citopenias + Erlenmeyer = Gaucher
- Fácies grosseira + hepatosplenomegalia = MPS
- Hipotonia profunda bebê + arreflexia = AME tipo 1
- Surtos remitentes + Lhermitte = EM
- Crises álgicas + HbSS = Falciforme"""

@spaces.GPU(duration=90)
def araras_infer(case_text, history):
    """Main inference — case_text is the latest user turn; history is conversation history."""
    if not case_text.strip():
        return "", history, "", ""

    llm = get_llm()
    # Build messages from history + new
    messages = [{"role": "system", "content": SYS}]
    for h in history:
        if isinstance(h, dict):
            messages.append({"role": h["role"], "content": h["content"]})
    messages.append({"role": "user", "content": case_text})

    resp = llm.create_chat_completion(
        messages=messages,
        temperature=0.5, top_p=0.95, top_k=64, repeat_penalty=1.15, max_tokens=700,
    )
    raw = resp["choices"][0]["message"]["content"]

    # Resolve ORPHAs
    orphas = resolve(raw)
    orpha_md = ""
    sus_md = ""
    if orphas:
        orpha_md = "**🧬 Diagnósticos reconhecidos** (top-3):\n\n" + "\n".join(
            f"- **{o['name_pt']}** (`{o['orpha']}`)" for o in orphas
        )
        top = orphas[0]
        if top.get("pcdt_slug"):
            pcdt_url = f"https://www.gov.br/conitec/pt-br/midias/protocolos/{top['pcdt_slug']}.pdf"
            sus_md = (f"### 🇧🇷 Conduta SUS sugerida ({top['name_pt']})\n\n"
                       f"**📜 PCDT do MS**: [`{top['pcdt_slug']}`]({pcdt_url})\n\n"
                       f"**💊 CEAF dispensa**: {', '.join(top['ceaf_drugs'])}\n\n"
                       f"**🏥 Próximos passos**: solicitar exames confirmatórios; encaminhar ao centro de referência regional.")

    # Append to history (Gradio chat format)
    new_history = history + [
        {"role": "user", "content": case_text},
        {"role": "assistant", "content": raw},
    ]
    return "", new_history, orpha_md, sus_md


# ─── Pre-loaded clinical examples ──────────────────────────────────────
EXAMPLES = [
    "Lactente feminino, 4 meses, natural de Petrolina (PE), pais primos. Quadro de hipotonia profunda generalizada desde o nascimento, ausência de sustentação cefálica, choro fraco, dificuldade para mamar, e abolição dos reflexos osteotendíneos profundos. SpO2 95% em ar ambiente.",
    "Adolescente masculino, 15 anos, da zona rural de Conceição do Coité (BA), pardo. Internado há 4 horas com dor torácica intensa e dispneia. História: triagem neonatal positiva HbSS, crises álgicas vaso-oclusivas frequentes desde os 5 anos. Hemograma: Hb 6.8, retic 12%, leuco 18k. Saturação 89%.",
    "Mulher 28 anos, UBS Cruzeiro do Sul (AC). Fraqueza no braço esquerdo há 3 semanas + episódio prévio há 2 anos de diplopia que resolveu sozinha. Exame: paresia leve MSE, hiperreflexia, sinal de Lhermitte positivo. Nega febre.",
    "Recém-nascido masculino, 12 dias de vida, Manaus (AM). Triagem neonatal (teste do pezinho) com fenilalanina sérica 18 mg/dL (normal <2). Pais consanguíneos, primeira filha. Sem sintomas no momento.",
    "Menino de 8 anos, hepatoesplenomegalia progressiva, opacidade corneana bilateral, atraso cognitivo importante, fácies grosseira, irmã mais velha com mesmo quadro (falecida aos 12 anos).",
]

# ─── UI ────────────────────────────────────────────────────────────────
with gr.Blocks(title="Araras-Gemma4 — Copiloto Clínico de Doenças Raras", theme=gr.themes.Soft()) as demo:
    gr.Markdown("""# 🇧🇷 Araras-Gemma4
### Copiloto offline de apoio à decisão clínica em doenças raras — para profissionais de saúde no Brasil

**Para médicos, enfermeiros, agentes comunitários** atendendo nas UBSs e ambulatórios do SUS.
NÃO é diagnóstico — é apoio à decisão.

🤖 Gemma 4 E4B (4.5B params, quantizado Q4) · 🇧🇷 PT-BR · 🛜 roda offline no celular · Apache 2.0

🔗 [Model](https://huggingface.co/Raras-AI/araras-gemma4-e4b-v4-gguf) · [Code](https://github.com/rarasAI/araras-gemma4) · [RareBench-BR](https://github.com/rarasAI/rarebench-br) · [raras.org](https://raras.org)
""")

    with gr.Row():
        with gr.Column(scale=3):
            chatbot = gr.Chatbot(
                label="Conversa com o(a) profissional de saúde",
                type="messages",
                height=500,
                avatar_images=("👨‍⚕️", "🤖"),
            )
            msg = gr.Textbox(
                label="Descreva o caso ou pergunte sobre conduta, exames, CEAF…",
                placeholder="Ex: Lactente 4 meses com hipotonia profunda + arreflexia em UBS sem 4G…",
                lines=3,
            )
            with gr.Row():
                submit = gr.Button("Enviar", variant="primary", scale=2)
                clear = gr.Button("Nova consulta", scale=1)
        with gr.Column(scale=2):
            orpha_out = gr.Markdown(label="ORPHAs reconhecidos")
            sus_out = gr.Markdown(label="Conduta SUS")

    gr.Examples(
        examples=EXAMPLES,
        inputs=msg,
        label="📋 Casos de exemplo (clique pra testar)",
    )

    submit.click(araras_infer, inputs=[msg, chatbot], outputs=[msg, chatbot, orpha_out, sus_out])
    msg.submit(araras_infer, inputs=[msg, chatbot], outputs=[msg, chatbot, orpha_out, sus_out])
    clear.click(lambda: ("", [], "", ""), outputs=[msg, chatbot, orpha_out, sus_out])

    gr.Markdown("""---
### ⚠️ Aviso clínico
Araras é **software de apoio à decisão clínica** (SaMD per ANVISA Res. 657/2022 e 830/2023), destinado a **profissionais de saúde habilitados**. Não realiza diagnósticos nem prescreve. Toda recomendação é informativa e ancorada em fontes públicas verificáveis (gov.br/conitec, bvsms, PubMed). O profissional de saúde é sempre quem decide.

### 🎯 Submissão
Gemma 4 Good Hackathon · Google DeepMind / Kaggle · Maio 2026 · Tracks: Main + Health & Sciences + Digital Equity + Ollama + llama.cpp

— Dimas, fundador da [Raras.org](https://raras.org), paciente raro (distonia mioclônica, ORPHA:36899)
""")

demo.queue().launch()