# 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. **RČ** 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í: ```python 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 `userNote` — **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čí 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): ```powershell 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 ```