185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
# 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
|
||
```
|