Files
Vladimir Buzalka 2bdac59676 notebookvb
2026-06-14 12:07:35 +02:00

10 KiB
Raw Permalink Blame History

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

  1. Graph API — DELTA nove_inbox_messages(mailbox, since_iso): všechny maily s receivedDateTime gt vodoznak, řazeno vzestupně, stránkováno (max MAX_PER_RUN=200/běh). Vodoznak = _last_processed.txt (2 řádky: čas + ID posl. mailu). POZOR: Graph má sub-sekundovou přesnost, ale receivedDateTime se zobrazuje oříznutě na sekundy → gt vrací 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í).
  2. 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.
  3. 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:
    1. z textu mailu (Medicus ukládá RČ bez lomítka) — jednoznačné,
    2. e-mail odesílatele proti KARKONTAKT,
    3. telefon z textu mailu proti KARKONTAKT (jen číslice, bez +420 — telefonů je v kartotéce hodně, často rozhodne i duplicitní jména),
    4. 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_narozeni nebo 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].
  4. Nejednoznačnost (více pacientů stejného jména, např. otec a syn)resolve_by_prescriptions(): načte nestornované recepty kandidátů z tabulky RECEPT (STORNO <> 'T', posledních RECEPT_MONTHS = 24 měsíců) a rozhodne podle shody požadovaných léků s historií:
    1. 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].
    2. 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.
  5. Report + cena AI za běh (~0,04 Kč/mail).

Sdílená infrastruktura

  • EmailAgent/graph_mail.py — import přes sys.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_KEY z Medevio/.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 field nazev-leku)
  • poznamka → dotazník pole „Poznámka" (jde přes userNotefunguje 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 0100, 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).
  • < 85NIC 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 cekazalozeno/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čí podle recept_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