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

185 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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).
- **< 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
```