10 KiB
OrdinaceAgentEmail — agent na žádosti o recept
Hledá ve schránce ordinace@buzalkova.cz e-maily, kde pacient žádá o předepsání léku (recept), vytěžuje pacienta + požadované léky a pacienta ověřuje v kartotéce Medicusu.
Stav: nasazeno na toweru (produkce), DELTA režim
recepty_agent.py zpracuje všechny nové maily od posledního zpracovaného
(vodoznak), klasifikuje Claude modelem, identifikuje pacienta a u vysoké jistoty
založí požadavek v Medeviu, jinak dá dotaz do fronty (Telegram). Označuje maily
kategoriemi. Běží na toweru — viz „Nasazení na tower" níže.
Tok
- Graph API — DELTA
nove_inbox_messages(mailbox, since_iso): všechny maily sreceivedDateTime gt vodoznak, řazeno vzestupně, stránkováno (maxMAX_PER_RUN=200/běh). Vodoznak =_last_processed.txt(2 řádky: čas + ID posl. mailu). POZOR: Graph má sub-sekundovou přesnost, alereceivedDateTimese zobrazuje oříznutě na sekundy →gtvrací i hraniční už zpracovaný mail; ten se odfiltruje podle uloženého ID. První běh (vodoznak chybí) jen nastaví vodoznak na nejnovější mail a od příště jede dopředně (historie se nedohání). - AI klasifikace + vytěžení (Claude
claude-haiku-4-5) — pro každý mail JSON:je_zadost_o_recept,pacient(může se lišit od odesílatele — příbuzní píší za pacienta),rodne_cislo(přesně jak je v textu),datum_narozeni,leky[](nazev + poznamka),poznamka,duvod. - Ověření v Medicusu (
MedicusLookup) — celá kartotéka se načte do paměti (KAR ~6300 pacientů + kontakty z KARKONTAKT: ~70 e-mailů, ~4150 telefonů; TYP: 1=pevná, 2=mobil, 3=e-mail). Párování v pořadí spolehlivosti:- RČ z textu mailu (Medicus ukládá RČ bez lomítka) — jednoznačné,
- e-mail odesílatele proti KARKONTAKT,
- telefon z textu mailu proti KARKONTAKT (jen číslice, bez +420 — telefonů je v kartotéce hodně, často rozhodne i duplicitní jména),
- jméno — bez diakritiky, bez ohledu na pořadí slov (Jaroslav Klíma
= Klíma Jaroslav); při více kandidátech zúžení datem narození
(z
datum_narozeninebo odvozeným z nesedícího RČ). Výstup:[SHODA RČ/E-MAIL/JMÉNO/JMÉNO+DATUM]s detaily pacienta (RČ, datum narození, pojišťovna, idpac, příznak vyřazení), nebo[NENALEZEN].
- Nejednoznačnost (více pacientů stejného jména, např. otec a syn) —
resolve_by_prescriptions(): načte nestornované recepty kandidátů z tabulkyRECEPT(STORNO <> 'T', posledníchRECEPT_MONTHS= 24 měsíců) a rozhodne podle shody požadovaných léků s historií:- Deterministicky —
_drug_matches(): substring oběma směry („tadalafil" ~ „TADALAFIL ACCORD") + prefix prvních slov od 5 znaků („Concord" ~ „CONCOR"). Jediný kandidát s nejvyšším nenulovým skóre vyhrává →[SHODA JMÉNO+LÉKY V HISTORII]. - Claude fallback — když deterministika nerozhodne (nikdo/více se
shodou), model dostane požadované léky + seznamy předepsaných léků
kandidátů a rozhodne i přes generika/účinné látky →
[SHODA JMÉNO+LÉKY+AI]. Když ani AI nerozhodne →[NEROZHODNUTO]- výpis kandidátů k ruční kontrole.
- Deterministicky —
- Report + cena AI za běh (~0,04 Kč/mail).
Sdílená infrastruktura
EmailAgent/graph_mail.py— import přessys.path(stejná app registrace, Mail.Read Application). Credentials natvrdo tam.Knihovny/medicus_db.py— Firebird připojení k Medicusu (DSN podle názvu počítače, na Z230 →reporter:c:\medicus\medicus.fdb).ANTHROPIC_API_KEYzMedevio/.env.
Vytvoření požadavku v Medeviu — mcp_medevio.zaloz_pozadavek_recept
Jakmile agent správně identifikuje pacienta + léky, založí mu v Medeviu požadavek „Recept na léky" přesně jako by ho podal pacient v aplikaci — vyplní oba fieldy dotazníku a přidá štítek CLAUDE. Vše v jednom volání:
import mcp_medevio
mcp_medevio.zaloz_pozadavek_recept(patient_uuid, leky="Euthyrox 100", poznamka="docházejí mi léky")
Mapování (ověřeno naživo na Vladkovi 2026-06-13):
leky→ dotazník pole „Název léků" (přes ECRF fieldnazev-leku)poznamka→ dotazník pole „Poznámka" (jde přesuserNote— funguje i z klinické strany!)stitek=True(default) → přiřadí štítek CLAUDE (assignTagToPatientRequest)
Postup uvnitř: fillECRFForm (oba fieldy, byDoctor:False) → createPatientRequestWithoutReservation
(createdByDoctor:False) → assignTagToPatientRequest. Auth: Bearer token z Medevio/token.txt
(auto-refresh při 401). Konstanty/mutace viz Medevio/medevio_api_notes.md.
Agent (recepty_agent.py) volá tuto funkci automaticky po jednoznačné identifikaci
pacienta; leky_str z _format_leky, pozn_str z _format_poznamka (hlavička + zkomprimované tělo mailu).
UUID pacienta hledá _medevio_find_patient v MySQL medevio_pacient (RČ → patient_id).
POZN.: požadavky v Medeviu nejdou smazat, jen zavřít („Vyřídit") — proto testovat na
testovacím pacientovi Vladko (0210db7b-…).
Skóre jistoty identifikace pacienta — skore_jistoty
Než agent založí požadavek, spočítá skóre 0–100, jak jistě nalezený pacient odpovídá pacientovi z mailu. Kombinuje víc nezávislých signálů; rozpor srazí dolů (tím se ošetří díra, kdy shoda na RČ s překlepem trefí jiného pacienta).
| Signál (shoda) | + | Rozpor | − |
|---|---|---|---|
| RČ sedí | 55 | jméno úplně jiné | 45 |
| jméno přesně / příjmení / částečně | 30 / 15 / 8 | datum narození nesedí | 35 |
| datum narození sedí | 20 | RČ nesedí na pacienta | 35 |
| e-mail odesílatele v kartotéce | 30 | ||
| telefon z mailu v kartotéce | 20 | ||
| lék v historii receptů | 10 |
Rozhodnutí (jediný práh SCORE_AUTO=85):
- ≥ 85 → založí požadavek automaticky (štítek CLAUDE).
- < 85 → NIC nezaloží a místo toho se zeptá člověka přes Telegram (viz níže). Důvod: vytvoření požadavku je nevratné a hned viditelné pacientovi — pacienta v něm nejde přepsat ani požadavek smazat. „Založit a pak ověřit" proto nedává smysl; ověřujeme PŘED založením.
Skóre i důvody jdou do logu. Funkce je čistá (testovatelná stubem), bez zápisů.
Human-in-the-loop přes Telegram (nejistá identifikace)
Když je jistota < 85, agent jen zapíše dotaz do fronty a jde dál. Vyřízení dělá samostatný proces. Moduly:
| Modul | Role |
|---|---|
recept_pending.py |
fronta dotazů (_pending_recepty.json, atomický zápis), stavy ceka→zalozeno/preskoceno |
recept_dialog.py |
čistě: format_otazka (text do Telegramu) + parse_odpoved (RČ / číslo kandidáta / „ne") |
recept_telegram.py |
přenos přes user účet agenta (Telethon, Knihovny/telegram_user.py), vlastní session recepty, píše Vladovi (6639316354) |
recept_resolver.py |
proces s vlastní session: pošle otázky, krátce polluje odpovědi (přes precti_zpravy, since_id), podle odpovědi založí (správné RČ je definitivní) a označí mail |
Tok: e-mailový agent (vysoká jistota → založí; jinak → recept_pending.pridej).
Resolver: otázka z účtu agenta → odpověď jako reply (párování přes
reply_to_msg_id) → mcp_medevio.zaloz_pozadavek_recept správnému pacientovi
→ mail dostane ClaudeZpracovalRecept. Bez odpovědi záznam zůstává ceka
(čeká se libovolně dlouho, znovu se neptá).
Telegram infrastruktura je popsaná v Trilium „2026-06-14 Telegram — bot, user účet agenta a MCP server". User účet (na rozdíl od bota) unese víc souběžných sessions, každá vidí všechny zprávy → odpovědi se rozlišují přes reply.
Jednorázový krok (uživatel, v terminálu — čeká na SMS kód):
python -m Knihovny.telegram_user login --jako recepty
Pak spuštění resolveru: python recept_resolver.py
Pozn.: nová session nezná Vladovu „entitu" → posílání by spadlo na Could not
find the input entity. Resolver to řeší sám: na startu volá recept_telegram.priprav()
(get_dialogs → entita se uloží do session). Login lze řídit i na dálku
dvoukrokově: login_posli_kod('recepty') → PHONE_CODE_HASH → login_dokonci(kod, hash, 'recepty').
Ověřeno naživo 2026-06-14: celý round-trip — agent zapsal nejistý dotaz → resolver poslal otázku do Telegramu → Vlado odpověděl RČ jako reply → resolver založil „Recept na léky" správnému pacientovi (dotazník Název léků + Poznámka, štítek CLAUDE) a poslal potvrzení zpět.
Známé limity / TODO
- E-mailových kontaktů je v kartotéce málo (~70 z 6300 pacientů) — párování e-mailem zabere zřídka; telefonů je ~4150, proto se vytěžuje i telefon z textu mailu. Do budoucna by šlo e-mail odesílatele po ručním potvrzení do KARKONTAKT doplňovat.
- Párování jménem vyžaduje přesnou shodu množiny slov — překlepy ve jméně nenajde (kandidát: fuzzy matching / nabídka podobných jmen).
- Bez summary e-mailu a bez odpovědi pacientovi — kandidáti na další krok
(vzor:
EmailAgent/faktury_agent.py). - Idempotence: po úspěšném založení požadavku se mail označí kategorií
ClaudeZpracovalRecept(graph_mail.ensure_category/add_category, vyžaduje Mail.ReadWrite — ověřeno, app ho má). Při dalším běhu se takto označené maily přeskočí (ještě před AI klasifikací). Maily čekající na odpověď přes Telegram se přeskočí podlerecept_pending.je_mail_pending(znovu se neptá). - DELTA režim: zpracuje vše po vodoznaku (ne jen N nejnovějších). Strop
MAX_PER_RUN=200/běh (kdyby byl vodoznak hodně zpět — zbytek dobere další běh). Vodoznak lze ručně posunout úpravou_last_processed.txt(např. backfill).
Spuštění
Vývoj (notebook/Z230):
python U:\ordinaceprojekt\OrdinaceAgentEmail\recepty_agent.py
Produkce (tower, python-runner) — viz „Nasazení na tower":
docker exec -e PYTHONIOENCODING=utf-8 -e MEDICUS_FDB_DSN=192.168.1.76:/firebird/data/medicus.fdb \
python-runner python /scripts/OrdinaceReceptAgent/OrdinaceAgentEmail/recepty_agent.py