notebookvb
This commit is contained in:
@@ -36,3 +36,7 @@ __pycache__/
|
|||||||
# Cookies (session tokeny)
|
# Cookies (session tokeny)
|
||||||
**/vozp_cookies.json
|
**/vozp_cookies.json
|
||||||
**/vzp_cookies.json
|
**/vzp_cookies.json
|
||||||
|
|
||||||
|
# Telegram user session (Telethon) — drží přihlášení, nikdy do gitu!
|
||||||
|
**/*.session
|
||||||
|
**/*.session-journal
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ Import vždy přes `sys.path` na kořen projektu nebo přímou cestou.
|
|||||||
| `mysql_db.py` | — | Připojení a operace s MySQL databází |
|
| `mysql_db.py` | — | Připojení a operace s MySQL databází |
|
||||||
| `medicus_db.py` | — | Připojení k databázi Medicus (Firebird) |
|
| `medicus_db.py` | — | Připojení k databázi Medicus (Firebird) |
|
||||||
| `vzpb2b_client.py` | — | Klient pro VZP B2B API (stav pojištění) |
|
| `vzpb2b_client.py` | — | Klient pro VZP B2B API (stav pojištění) |
|
||||||
|
| `telegram_notify.py` | `posli_telegram()`, `zeptej_se_telegram()` | Notifikace a obousměrná komunikace přes Telegram **bota** (@Vlado_Claude_Bot) |
|
||||||
|
| `telegram_user.py` | `posli_jako_ja()`, `zeptej_se_jako()` | Komunikace přes plnohodnotný **user účet** agenta (Telethon, víc agentů = víc sessions) |
|
||||||
|
|
||||||
## Přehled skriptů
|
## Přehled skriptů
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ def get_medicus_connection():
|
|||||||
"LEKAR": r"localhost:M:\medicus\data\medicus.fdb",
|
"LEKAR": r"localhost:M:\medicus\data\medicus.fdb",
|
||||||
"SESTRA": r"192.168.1.10:m:\medicus\data\medicus.fdb",
|
"SESTRA": r"192.168.1.10:m:\medicus\data\medicus.fdb",
|
||||||
"LENOVO": r"192.168.1.10:m:\medicus\data\medicus.fdb",
|
"LENOVO": r"192.168.1.10:m:\medicus\data\medicus.fdb",
|
||||||
"NTBVBHP470G10": r"reporter:c:\medicus\medicus.fdb",
|
"NTBVBHP470G10": r"192.168.1.76:/firebird/data/medicus.fdb", # přepnuto z reporteru na tower 2026-06-14
|
||||||
"Z230": r"reporter:c:\medicus\medicus.fdb",
|
"Z230": r"192.168.1.76:/firebird/data/medicus.fdb", # přepnuto z reporteru na tower 2026-06-14
|
||||||
|
"TOWER": r"192.168.1.76:/firebird/data/medicus.fdb", # Firebird 2.5 docker kontejner na toweru
|
||||||
}
|
}
|
||||||
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
|
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
+11
-1
@@ -1,4 +1,14 @@
|
|||||||
ANTHROPIC_API_KEY=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
|
ANTHROPIC_API_KEY=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
|
||||||
|
|
||||||
CENTRAL_LOG_TOKEN=b1e95b3ca9b64769d14bb80370a07882958cac95a0eb9d7758933f151a053c08
|
CENTRAL_LOG_TOKEN=b1e95b3ca9b64769d14bb80370a07882958cac95a0eb9d7758933f151a053c08
|
||||||
CENTRAL_LOG_GATEWAY=http://192.168.1.76:8770
|
CENTRAL_LOG_GATEWAY=http://192.168.1.76:8770
|
||||||
|
|
||||||
|
# Telegram bot (ClaudeBot @Vlado_Claude_Bot) — notifikace o průběhu
|
||||||
|
TELEGRAM_BOT_TOKEN=8821687113:AAF9U9S989ZJ0OG2St3o8CyHUSKg7RqyYVM
|
||||||
|
TELEGRAM_CHAT_ID=6639316354
|
||||||
|
|
||||||
|
# Telegram USER účet (Telethon) — plnohodnotný účet agenta
|
||||||
|
# api_id/api_hash z https://my.telegram.org (přihlas se číslem nového účtu)
|
||||||
|
TELEGRAM_API_ID=39599696
|
||||||
|
TELEGRAM_API_HASH=f93ed362cdbfb4f5df85072a0350a8fc
|
||||||
|
TELEGRAM_PHONE=+420705920457
|
||||||
@@ -241,34 +241,46 @@ request {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Request Creation (Vytvoření požadavku lékařem) — ODCHYCENO 2026-06-13
|
### Request Creation (Vytvoření požadavku "Recept na léky") — ODCHYCENO/OVĚŘENO 2026-06-13
|
||||||
|
|
||||||
Lékařský účet (klinický token) **NEumí vyplnit pacientský dotazník** smysluplně — formulář
|
Přes API **lze založit požadavek s plně vyplněným pacientským dotazníkem** (oba fieldy),
|
||||||
„Recept na léky" má z lékařské strany (`sid: ERECEPT_SIMPLEST_BEZ_DAVKOVANI`) jen jedno
|
takže vypadá jako reálné podání pacientem. Funkce: `mcp_medevio.zaloz_pozadavek_recept`.
|
||||||
pole `nazev-leku`, kdežto pacient v appce vyplní dvě pole („Název léků" + „Poznámka").
|
(Pozn.: lékařské UI „Nový požadavek" pole dotazníku NEzobrazí — ale API je přijme.)
|
||||||
**Proto: obsah z e-mailu zapisujeme do INTERNÍ POZNÁMKY, ne do dotazníku.**
|
|
||||||
|
|
||||||
Vytvoření prázdného požadavku „Recept na léky" je **dvoukrok**:
|
**Dvoukrok (+ volitelně štítek):**
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
# 1) vytvoř (prázdný) ECRF fill → vrátí ecrfFill.id
|
# 1) vyplň ECRF formulář → vrátí ecrfFill.id
|
||||||
mutation ClinicRequestCreateModal_FillECRFForm($input: FillECRFFormInput!) {
|
mutation Step_FillECRFForm($input: FillECRFFormInput!) {
|
||||||
ecrfFill: fillECRFForm(input: $input) { id }
|
patientEcrfFill: fillECRFForm(input: $input) { id }
|
||||||
}
|
}
|
||||||
# input: { byDoctor: true, fields: [], patientId, sid: "ERECEPT_SIMPLEST_BEZ_DAVKOVANI", stepId: "erecept-gp-request" }
|
# input: {
|
||||||
|
# patientId, sid: "ERECEPT_SIMPLEST_BEZ_DAVKOVANI", stepId: "erecept-gp-request",
|
||||||
|
# byDoctor: false,
|
||||||
|
# fields: [{ fieldName: "nazev-leku", value: "<léky>", checkedEnumerations: [] }]
|
||||||
|
# } → pole "Název léků" v dotazníku
|
||||||
|
|
||||||
# 2) vytvoř požadavek s odkazem na ecrfFill
|
# 2) vytvoř požadavek
|
||||||
mutation ClinicRequestCreateModal_CreateRequest($clinicSlug: String!, $input: CreatePatientRequestWithoutReservationInput!) {
|
mutation ...CreatePatientRequestWithoutReservation($clinicSlug: String!, $input: ...) {
|
||||||
patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id }
|
patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id }
|
||||||
}
|
}
|
||||||
# input: { patientId, userECRFId, ecrfFillIds: [<id z kroku 1>], createdByDoctor: true, shouldInvitePatient: false }
|
# input: {
|
||||||
|
# patientId, userECRFId, ecrfFillIds: [<id z kroku 1>], medicalRecordIds: [], challengeId: null,
|
||||||
|
# userNote: "<poznámka>", ← zobrazí se jako pole "Poznámka" v dotazníku
|
||||||
|
# createdByDoctor: false
|
||||||
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
POZOR: `createPatientRequest` (bez „WithoutReservation") požadavek vytvoří, ale
|
||||||
|
NEZOBRAZÍ se v žádné frontě — používat `createPatientRequestWithoutReservation`.
|
||||||
|
|
||||||
| Klíč | Hodnota |
|
| Klíč | Hodnota |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| ECRF „Recept na léky" `userECRFId` | `79488e86-e9e5-47e3-8b19-7e5229427f23` |
|
| ECRF „Recept na léky" `userECRFId` | `79488e86-e9e5-47e3-8b19-7e5229427f23` |
|
||||||
| ECRF `sid` | `ERECEPT_SIMPLEST_BEZ_DAVKOVANI` |
|
| ECRF `sid` | `ERECEPT_SIMPLEST_BEZ_DAVKOVANI` |
|
||||||
| ECRF `stepId` | `erecept-gp-request` |
|
| ECRF `stepId` | `erecept-gp-request` |
|
||||||
|
| pole 1 `fieldName` | `nazev-leku` (→ „Název léků") |
|
||||||
|
| pole 2 | `userNote` v create inputu (→ „Poznámka") |
|
||||||
|
|
||||||
Seznam typů požadavků: `UserEcrfAutocomplete_ListUserECRFsByClinic`.
|
Seznam typů požadavků: `UserEcrfAutocomplete_ListUserECRFsByClinic`.
|
||||||
|
|
||||||
@@ -279,11 +291,17 @@ query TagRequestEditModal_ListTags($clinicSlug: String!, $requestId: UUID!) { .
|
|||||||
mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) {
|
mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) {
|
||||||
tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id }
|
tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id }
|
||||||
}
|
}
|
||||||
|
# Vytvoření nového štítku:
|
||||||
|
mutation TagEditModal_CreateTag($clinicSlug: String!, $input: CreateTagInput!) {
|
||||||
|
tag: createTag(clinicSlug: $clinicSlug, input: $input) { id name color icon important isOrganizationWide }
|
||||||
|
}
|
||||||
|
# input: { name, color (např. "SKY"/"ORCHID"), icon: null, important: false, type: "patient_request", isOrganizationWide: false }
|
||||||
```
|
```
|
||||||
|
|
||||||
| Štítek | tagId | barva |
|
| Štítek | tagId | barva |
|
||||||
|--------|-------|-------|
|
|--------|-------|-------|
|
||||||
| `CLAUDE` | `c136aeca-0625-4c43-b81f-fc3949ec6ba6` | ORCHID |
|
| `CLAUDE` | `c136aeca-0625-4c43-b81f-fc3949ec6ba6` | ORCHID |
|
||||||
|
| `OVĚŘIT PACIENTA` | `9d3271b3-309d-4d20-93ee-285f3e56ba42` | SKY |
|
||||||
| `NEZAPOMENOUT` | `5bced917-83d2-46db-896c-c8e615de1a69` | GREY |
|
| `NEZAPOMENOUT` | `5bced917-83d2-46db-896c-c8e615de1a69` | GREY |
|
||||||
|
|
||||||
### Request Detail
|
### Request Detail
|
||||||
|
|||||||
+91
-22
@@ -57,32 +57,94 @@ přesuny, odpovědi), žádný `state.json`.
|
|||||||
počítače, na Z230 → `reporter:c:\medicus\medicus.fdb`).
|
počítače, na Z230 → `reporter:c:\medicus\medicus.fdb`).
|
||||||
- `ANTHROPIC_API_KEY` z `Medevio/.env`.
|
- `ANTHROPIC_API_KEY` z `Medevio/.env`.
|
||||||
|
|
||||||
## Vytvoření požadavku v Medeviu — `medevio_recept.py`
|
## Vytvoření požadavku v Medeviu — `mcp_medevio.zaloz_pozadavek_recept`
|
||||||
|
|
||||||
Funkce pro agenta: jakmile je pacient + léky správně identifikován, založí mu
|
Jakmile agent správně identifikuje pacienta + léky, založí mu v Medeviu požadavek
|
||||||
v Medeviu požadavek **„Recept na léky"**, aby ho lékař viděl (Medevio kontrolujeme
|
**„Recept na léky"** přesně jako by ho podal pacient v aplikaci — vyplní **oba fieldy
|
||||||
průběžně, e-mail zřídka).
|
dotazníku** a přidá **štítek CLAUDE**. Vše v jednom volání:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from medevio_recept import vytvor_recept
|
import mcp_medevio
|
||||||
rid = vytvor_recept(rodne_cislo="730920/8104",
|
mcp_medevio.zaloz_pozadavek_recept(patient_uuid, leky="Euthyrox 100", poznamka="docházejí mi léky")
|
||||||
nazev_leku="Euthyrox 100 µg",
|
|
||||||
poznamka="docházejí mi léky")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Co se stane (vše odchyceno z webu Medevia 2026-06-13, ověřeno na testovacím pacientovi):
|
Mapování (ověřeno naživo na Vladkovi 2026-06-13):
|
||||||
1. **RČ → patient UUID** přes MySQL `medevio.medevio_pacient` (`identification_number` → `patient_id`).
|
- `leky` → dotazník pole **„Název léků"** (přes ECRF field `nazev-leku`)
|
||||||
2. `fillECRFForm` (prázdný) → `createPatientRequestWithoutReservation` → založí „Recept na léky".
|
- `poznamka` → dotazník pole **„Poznámka"** (jde přes `userNote` — **funguje** i z klinické strany!)
|
||||||
3. `createClinicPatientRequestNote` → obě pole do **interní poznámky** (formátováno „Název léků / Poznámka").
|
- `stitek=True` (default) → přiřadí **štítek CLAUDE** (`assignTagToPatientRequest`)
|
||||||
4. `assignTagToPatientRequest` → štítek **CLAUDE** (`pridat_stitek=False` vypne).
|
|
||||||
|
|
||||||
**Proč interní poznámka, ne dotazník:** lékařský přístup neumí vyplnit pacientský
|
Postup uvnitř: `fillECRFForm` (oba fieldy, `byDoctor:False`) → `createPatientRequestWithoutReservation`
|
||||||
ECRF dotazník smysluplně (z lékařské strany má jen 1 pole `nazev-leku`), proto obsah
|
(`createdByDoctor:False`) → `assignTagToPatientRequest`. Auth: Bearer token z `Medevio/token.txt`
|
||||||
jde do interní poznámky (viditelná jen pro ordinaci).
|
(auto-refresh při 401). Konstanty/mutace viz `Medevio/medevio_api_notes.md`.
|
||||||
|
|
||||||
Auth: Bearer token z `Medevio/token.txt` (dlouhodobý). Test: `python medevio_recept.py`
|
Agent (`recepty_agent.py`) volá tuto funkci automaticky po jednoznačné identifikaci
|
||||||
(založí testovací Recept na Vladkovi `0210db7b-…`). Pro test bez DB lookup je parametr
|
pacienta; `leky_str` z `_format_leky`, `pozn_str` z `_format_poznamka` (hlavička + zkomprimované tělo mailu).
|
||||||
`patient_uuid=`. Mutace + konstanty jsou v `Medevio/medevio_api_notes.md`.
|
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
|
## Známé limity / TODO
|
||||||
|
|
||||||
@@ -92,9 +154,16 @@ Auth: Bearer token z `Medevio/token.txt` (dlouhodobý). Test: `python medevio_re
|
|||||||
do KARKONTAKT doplňovat.
|
do KARKONTAKT doplňovat.
|
||||||
- Párování jménem vyžaduje přesnou shodu množiny slov — překlepy ve jméně
|
- 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).
|
nenajde (kandidát: fuzzy matching / nabídka podobných jmen).
|
||||||
- Zatím bez označování mailů, bez summary e-mailu, bez odpovědi pacientovi —
|
- Bez summary e-mailu a bez odpovědi pacientovi — kandidáti na další krok
|
||||||
kandidáti na další krok (vzor: `EmailAgent/faktury_agent.py`).
|
(vzor: `EmailAgent/faktury_agent.py`).
|
||||||
- Bez idempotence (žádný state) — testovací běhy čtou vždy posledních N mailů.
|
- **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á).
|
||||||
|
- Pozor: agent čte jen `NEWEST_N` (5) nejnovějších mailů — hloub do inboxu
|
||||||
|
nejde. Když je všech 5 nejnovějších označených, neudělá nic.
|
||||||
|
|
||||||
## Spuštění
|
## Spuštění
|
||||||
|
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
medevio_recept.py — vytvoření požadavku "Recept na léky" v Medeviu pro pacienta.
|
|
||||||
|
|
||||||
Určeno k volání z e-mailového agenta (recepty_agent.py): jakmile agent správně
|
|
||||||
identifikuje pacienta a co chce, založí mu v Medeviu požadavek, aby ho lékař viděl
|
|
||||||
(e-mail kontrolujeme zřídka, Medevio pořád).
|
|
||||||
|
|
||||||
Hlavní funkce:
|
|
||||||
vytvor_recept(rodne_cislo, nazev_leku, poznamka) -> request_id
|
|
||||||
|
|
||||||
Co dělá (vše ověřeno/odchyceno 2026-06-13 na testovacím pacientovi Vladko):
|
|
||||||
1. RČ -> patient UUID (MySQL medevio.medevio_pacient)
|
|
||||||
2. fillECRFForm -> ecrfFill.id (prázdný formulář)
|
|
||||||
3. createPatientRequestWithoutReservation -> založí "Recept na léky"
|
|
||||||
4. createClinicPatientRequestNote -> obě pole do INTERNÍ POZNÁMKY
|
|
||||||
5. assignTagToPatientRequest -> štítek CLAUDE
|
|
||||||
|
|
||||||
POZOR: lékařský (klinický) přístup neumí vyplnit pacientský dotazník smysluplně,
|
|
||||||
proto obsah ("Název léků" + "Poznámka") jde do interní poznámky, ne do dotazníku.
|
|
||||||
|
|
||||||
Auth: Bearer token z Medevio/token.txt (dlouhodobý API token).
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import pymysql
|
|
||||||
from pymysql.cursors import DictCursor
|
|
||||||
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Konstanty odchycené z Medevia (klinika mudr-buzalkova)
|
|
||||||
# ============================================================
|
|
||||||
CLINIC_SLUG = "mudr-buzalkova"
|
|
||||||
GRAPHQL_URL = "https://api.medevio.cz/graphql"
|
|
||||||
|
|
||||||
RECEPT_USER_ECRF_ID = "79488e86-e9e5-47e3-8b19-7e5229427f23" # typ "Recept na léky"
|
|
||||||
RECEPT_SID = "ERECEPT_SIMPLEST_BEZ_DAVKOVANI"
|
|
||||||
RECEPT_STEP_ID = "erecept-gp-request"
|
|
||||||
CLAUDE_TAG_ID = "c136aeca-0625-4c43-b81f-fc3949ec6ba6" # štítek CLAUDE
|
|
||||||
|
|
||||||
TOKEN_PATH = Path(__file__).resolve().parent.parent / "Medevio" / "token.txt"
|
|
||||||
|
|
||||||
# MySQL je na různých strojích na různém portu — zkusíme kandidáty po řadě.
|
|
||||||
_MYSQL_BASE = dict(user="root", password="Vlado9674+", database="medevio")
|
|
||||||
_MYSQL_CANDIDATES = [
|
|
||||||
dict(host="192.168.1.76", port=3306),
|
|
||||||
dict(host="192.168.1.76", port=3307),
|
|
||||||
dict(host="127.0.0.1", port=3307),
|
|
||||||
dict(host="127.0.0.1", port=3306),
|
|
||||||
]
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# GraphQL operace (přesně jak je posílá web Medevia)
|
|
||||||
# ============================================================
|
|
||||||
M_FILL_ECRF = r"""
|
|
||||||
mutation ClinicRequestCreateModal_FillECRFForm($input: FillECRFFormInput!) {
|
|
||||||
ecrfFill: fillECRFForm(input: $input) { id }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
M_CREATE_REQUEST = r"""
|
|
||||||
mutation ClinicRequestCreateModal_CreateRequest($clinicSlug: String!, $input: CreatePatientRequestWithoutReservationInput!) {
|
|
||||||
patientRequest: createPatientRequestWithoutReservation(clinicSlug: $clinicSlug, input: $input) { id }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
M_CREATE_NOTE = r"""
|
|
||||||
mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) {
|
|
||||||
createClinicPatientRequestNote(noteInput: $noteInput) { id }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
M_ASSIGN_TAG = r"""
|
|
||||||
mutation TagRequestEditModal_AssignTagToRequest($clinicSlug: String!, $requestId: UUID!, $tagId: UUID!) {
|
|
||||||
tagRequest: assignTagToPatientRequest(clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId) { id }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Pomocné funkce
|
|
||||||
# ============================================================
|
|
||||||
def _read_token() -> str:
|
|
||||||
t = TOKEN_PATH.read_text(encoding="utf-8").strip()
|
|
||||||
return t[7:].strip() if t.lower().startswith("bearer ") else t
|
|
||||||
|
|
||||||
|
|
||||||
def _gql(query: str, variables: dict, token: str) -> dict:
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
r = requests.post(GRAPHQL_URL, json={"query": query, "variables": variables},
|
|
||||||
headers=headers, timeout=30)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
if data.get("errors"):
|
|
||||||
raise RuntimeError(f"GraphQL chyba: {data['errors']}")
|
|
||||||
return data["data"]
|
|
||||||
|
|
||||||
|
|
||||||
def _mysql():
|
|
||||||
last = None
|
|
||||||
for cand in _MYSQL_CANDIDATES:
|
|
||||||
try:
|
|
||||||
return pymysql.connect(**cand, **_MYSQL_BASE,
|
|
||||||
cursorclass=DictCursor, connect_timeout=4)
|
|
||||||
except Exception as e:
|
|
||||||
last = e
|
|
||||||
raise RuntimeError(f"MySQL (medevio) nedostupné na žádném kandidátovi: {last}")
|
|
||||||
|
|
||||||
|
|
||||||
def najdi_uuid_dle_rc(rodne_cislo: str) -> dict:
|
|
||||||
"""RČ (s lomítkem i bez) -> řádek pacienta z medevio_pacient.
|
|
||||||
Vyhazuje LookupError když pacient není nalezen nebo je nejednoznačný."""
|
|
||||||
rc = re.sub(r"\D", "", rodne_cislo or "")
|
|
||||||
if not rc:
|
|
||||||
raise ValueError("Prázdné / neplatné rodné číslo.")
|
|
||||||
conn = _mysql()
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT patient_id, name, surname, status, user_id "
|
|
||||||
"FROM medevio_pacient "
|
|
||||||
"WHERE REPLACE(identification_number, '/', '') = %s",
|
|
||||||
(rc,),
|
|
||||||
)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
if not rows:
|
|
||||||
raise LookupError(f"Pacient s RČ {rc} není v Medevio databázi (medevio_pacient).")
|
|
||||||
if len(rows) > 1:
|
|
||||||
raise LookupError(f"RČ {rc} odpovídá více pacientům — nejednoznačné, řeš ručně.")
|
|
||||||
return rows[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _format_note(nazev_leku: str, poznamka: str) -> str:
|
|
||||||
return (
|
|
||||||
"Žádost o recept (zpracováno e-mailovým agentem).\n\n"
|
|
||||||
f"Název léků:\n{(nazev_leku or '').strip() or '—'}\n\n"
|
|
||||||
f"Poznámka:\n{(poznamka or '').strip() or '—'}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Hlavní funkce
|
|
||||||
# ============================================================
|
|
||||||
def vytvor_recept(rodne_cislo: str = None, nazev_leku: str = "", poznamka: str = "",
|
|
||||||
*, patient_uuid: str = None, pridat_stitek: bool = True,
|
|
||||||
token: str = None, verbose: bool = True) -> str:
|
|
||||||
"""
|
|
||||||
Založí pacientovi požadavek "Recept na léky" + interní poznámku + štítek CLAUDE.
|
|
||||||
|
|
||||||
Parametry:
|
|
||||||
rodne_cislo – RČ pacienta (s lomítkem i bez); přeloží se na UUID přes MySQL.
|
|
||||||
nazev_leku – text 1. pole ("Název léků").
|
|
||||||
poznamka – text 2. pole ("Poznámka").
|
|
||||||
patient_uuid – volitelně rovnou UUID pacienta (obejde lookup dle RČ; pro testy).
|
|
||||||
pridat_stitek – True = přiřadí štítek CLAUDE.
|
|
||||||
token – volitelně Bearer token; jinak se načte z token.txt.
|
|
||||||
|
|
||||||
Vrací: request_id založeného požadavku (str).
|
|
||||||
"""
|
|
||||||
token = token or _read_token()
|
|
||||||
|
|
||||||
if patient_uuid:
|
|
||||||
uuid = patient_uuid
|
|
||||||
kdo = f"UUID {uuid}"
|
|
||||||
else:
|
|
||||||
pac = najdi_uuid_dle_rc(rodne_cislo)
|
|
||||||
uuid = pac["patient_id"]
|
|
||||||
kdo = f"{pac['surname']} {pac['name']} (RČ {rodne_cislo}, status {pac['status']})"
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
print(f"→ Pacient: {kdo}")
|
|
||||||
|
|
||||||
# 1) prázdný ECRF fill
|
|
||||||
fill = _gql(M_FILL_ECRF, {"input": {
|
|
||||||
"byDoctor": True,
|
|
||||||
"fields": [],
|
|
||||||
"patientId": uuid,
|
|
||||||
"sid": RECEPT_SID,
|
|
||||||
"stepId": RECEPT_STEP_ID,
|
|
||||||
}}, token)
|
|
||||||
ecrf_fill_id = fill["ecrfFill"]["id"]
|
|
||||||
|
|
||||||
# 2) vytvoř požadavek "Recept na léky"
|
|
||||||
created = _gql(M_CREATE_REQUEST, {"clinicSlug": CLINIC_SLUG, "input": {
|
|
||||||
"patientId": uuid,
|
|
||||||
"userECRFId": RECEPT_USER_ECRF_ID,
|
|
||||||
"ecrfFillIds": [ecrf_fill_id],
|
|
||||||
"createdByDoctor": True,
|
|
||||||
"shouldInvitePatient": False,
|
|
||||||
}}, token)
|
|
||||||
request_id = created["patientRequest"]["id"]
|
|
||||||
if verbose:
|
|
||||||
print(f"✓ Požadavek vytvořen: {request_id}")
|
|
||||||
|
|
||||||
# 3) interní poznámka s oběma poli
|
|
||||||
_gql(M_CREATE_NOTE, {"noteInput": {
|
|
||||||
"requestId": request_id,
|
|
||||||
"content": _format_note(nazev_leku, poznamka),
|
|
||||||
}}, token)
|
|
||||||
if verbose:
|
|
||||||
print("✓ Interní poznámka zapsána")
|
|
||||||
|
|
||||||
# 4) štítek CLAUDE
|
|
||||||
if pridat_stitek:
|
|
||||||
_gql(M_ASSIGN_TAG, {
|
|
||||||
"clinicSlug": CLINIC_SLUG,
|
|
||||||
"requestId": request_id,
|
|
||||||
"tagId": CLAUDE_TAG_ID,
|
|
||||||
}, token)
|
|
||||||
if verbose:
|
|
||||||
print("✓ Štítek CLAUDE přiřazen")
|
|
||||||
|
|
||||||
return request_id
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Test (na testovacím pacientovi Vladko)
|
|
||||||
# ============================================================
|
|
||||||
if __name__ == "__main__":
|
|
||||||
VLADKO_UUID = "0210db7b-8fb0-4b47-b1d8-ec7a10849a63" # Vladko - testovací aplikace
|
|
||||||
rid = vytvor_recept(
|
|
||||||
patient_uuid=VLADKO_UUID,
|
|
||||||
nazev_leku="Euthyrox 100 µg (TEST z medevio_recept.py)",
|
|
||||||
poznamka="Testovací požadavek z funkce vytvor_recept — možno zavřít.",
|
|
||||||
)
|
|
||||||
print(f"\nHOTOVO. request_id = {rid}")
|
|
||||||
print(f"https://my.medevio.cz/mudr-buzalkova/klinika/pacienti?pozadavek={rid}")
|
|
||||||
@@ -44,6 +44,8 @@ import graph_mail # noqa: E402
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
from Knihovny.medicus_db import get_medicus_db # noqa: E402
|
from Knihovny.medicus_db import get_medicus_db # noqa: E402
|
||||||
import mcp_medevio as _medevio # noqa: E402 GraphQL API + zaloz_pozadavek_recept
|
import mcp_medevio as _medevio # noqa: E402 GraphQL API + zaloz_pozadavek_recept
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import recept_pending as _pending # noqa: E402 fronta dotazů (nejistá identifikace)
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# NASTAVENÍ
|
# NASTAVENÍ
|
||||||
@@ -53,6 +55,19 @@ MAILBOX = "ordinace@buzalkova.cz"
|
|||||||
# Kolik nejnovějších mailů z Inboxu zpracovat (testovací režim).
|
# Kolik nejnovějších mailů z Inboxu zpracovat (testovací režim).
|
||||||
NEWEST_N = 5
|
NEWEST_N = 5
|
||||||
|
|
||||||
|
# Kategorie (štítek na mailu), kterou agent označí mail po úspěšném založení
|
||||||
|
# požadavku v Medeviu. Při dalším běhu se takto označené maily přeskočí
|
||||||
|
# → idempotence, nezakládá duplicitní požadavky.
|
||||||
|
PROCESSED_CATEGORY = "ClaudeZpracovalRecept"
|
||||||
|
# Kategorie pro maily, které nešlo vyřídit automaticky (k ruční kontrole).
|
||||||
|
MANUAL_CATEGORY = "ReceptRucne"
|
||||||
|
|
||||||
|
# Práh jistoty pro PLNĚ automatické založení požadavku. Vytvoření požadavku je
|
||||||
|
# nevratné a hned viditelné pacientovi → pod tímto prahem agent NIC nezaloží
|
||||||
|
# a místo toho se zeptá člověka přes Telegram (přes pending frontu, viz
|
||||||
|
# recept_pending.py / recept_resolver.py).
|
||||||
|
SCORE_AUTO = 85
|
||||||
|
|
||||||
# Claude model pro klasifikaci + vytěžení.
|
# Claude model pro klasifikaci + vytěžení.
|
||||||
ANTHROPIC_MODEL = "claude-haiku-4-5"
|
ANTHROPIC_MODEL = "claude-haiku-4-5"
|
||||||
|
|
||||||
@@ -102,7 +117,7 @@ def newest_inbox_messages(mailbox: str, n: int) -> list[dict]:
|
|||||||
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
|
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
|
||||||
params = {
|
params = {
|
||||||
"$orderby": "receivedDateTime desc",
|
"$orderby": "receivedDateTime desc",
|
||||||
"$select": "id,subject,from,receivedDateTime,bodyPreview,body",
|
"$select": "id,subject,from,receivedDateTime,bodyPreview,body,categories",
|
||||||
"$top": n,
|
"$top": n,
|
||||||
}
|
}
|
||||||
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
|
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
|
||||||
@@ -433,6 +448,80 @@ class MedicusLookup:
|
|||||||
return None, "", detail
|
return None, "", detail
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# SKÓRE JISTOTY IDENTIFIKACE PACIENTA
|
||||||
|
# =========================
|
||||||
|
def skore_jistoty(verdict: dict, patient: dict, sender_email: str,
|
||||||
|
lookup: "MedicusLookup") -> tuple[int, list[str]]:
|
||||||
|
"""Kvantifikuje jistotu (0–100), že `patient` z kartotéky je opravdu pacient
|
||||||
|
z e-mailu. Vrací (skóre, důvody). Více nezávislých shod = vyšší jistota;
|
||||||
|
rozpor (jiné jméno / datum / RČ) skóre tvrdě srazí a označí ⚠.
|
||||||
|
Tím se ošetří díra, kdy shoda na RČ (např. překlep) trefí jiného pacienta —
|
||||||
|
bez souhlasu jména spadne z 'jisté' do pásma 'nutná kontrola'."""
|
||||||
|
body = 0
|
||||||
|
duvody: list[str] = []
|
||||||
|
|
||||||
|
e_rc = _norm_rc(verdict.get("rodne_cislo") or "")
|
||||||
|
p_rc = _norm_rc(patient.get("rodcis") or "")
|
||||||
|
e_name = frozenset(_norm_text(verdict.get("pacient") or "").split())
|
||||||
|
p_name = frozenset(
|
||||||
|
_norm_text(f"{patient.get('jmeno') or ''} {patient.get('prijmeni') or ''}").split()
|
||||||
|
)
|
||||||
|
p_surname = _norm_text(patient.get("prijmeni") or "")
|
||||||
|
e_dob = verdict.get("datum_narozeni") or (_rc_to_birthdate(e_rc) if e_rc else None)
|
||||||
|
p_dob = (str(patient.get("datnar") or "")[:10]) or None
|
||||||
|
idpac = patient.get("idpac")
|
||||||
|
|
||||||
|
# Rodné číslo
|
||||||
|
if e_rc and p_rc:
|
||||||
|
if e_rc == p_rc:
|
||||||
|
body += 55; duvody.append("RČ sedí (+55)")
|
||||||
|
else:
|
||||||
|
body -= 35; duvody.append("⚠ RČ z mailu NESEDÍ na pacienta (−35)")
|
||||||
|
|
||||||
|
# Jméno
|
||||||
|
if e_name and p_name:
|
||||||
|
if e_name == p_name:
|
||||||
|
body += 30; duvody.append("jméno přesně (+30)")
|
||||||
|
elif p_surname and p_surname in e_name:
|
||||||
|
body += 15; duvody.append("příjmení sedí (+15)")
|
||||||
|
elif e_name & p_name:
|
||||||
|
body += 8; duvody.append("částečná shoda jména (+8)")
|
||||||
|
else:
|
||||||
|
body -= 45; duvody.append("⚠ jméno NESOUHLASÍ (−45)")
|
||||||
|
|
||||||
|
# Datum narození (z pole nebo odvozené z RČ)
|
||||||
|
if e_dob and p_dob:
|
||||||
|
if e_dob == p_dob:
|
||||||
|
body += 20; duvody.append("datum narození sedí (+20)")
|
||||||
|
else:
|
||||||
|
body -= 35; duvody.append("⚠ datum narození NESEDÍ (−35)")
|
||||||
|
|
||||||
|
# E-mail odesílatele v kartotéce pacienta
|
||||||
|
em = (sender_email or "").strip().lower()
|
||||||
|
if em and any(p.get("idpac") == idpac for p in lookup.by_email.get(em, [])):
|
||||||
|
body += 30; duvody.append("e-mail odesílatele v kartotéce (+30)")
|
||||||
|
|
||||||
|
# Telefon z textu mailu v kartotéce pacienta
|
||||||
|
ph = _norm_phone(verdict.get("telefon") or "")
|
||||||
|
if len(ph) >= 9 and any(p.get("idpac") == idpac for p in lookup.by_phone.get(ph, [])):
|
||||||
|
body += 20; duvody.append("telefon v kartotéce (+20)")
|
||||||
|
|
||||||
|
# Požadovaný lék v historii receptů pacienta
|
||||||
|
try:
|
||||||
|
requested = [(l.get("nazev") or "").strip() for l in (verdict.get("leky") or [])]
|
||||||
|
requested = [r for r in requested if r]
|
||||||
|
if requested and idpac is not None:
|
||||||
|
drugs = {(h.get("lek") or "").strip()
|
||||||
|
for h in lookup.prescriptions(idpac) if h.get("lek")}
|
||||||
|
if any(_drug_matches(req, d) for req in requested for d in drugs):
|
||||||
|
body += 10; duvody.append("lék v historii receptů (+10)")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return max(0, min(100, body)), duvody
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# MEDEVIO — ZÁPIS POŽADAVKU
|
# MEDEVIO — ZÁPIS POŽADAVKU
|
||||||
# =========================
|
# =========================
|
||||||
@@ -486,6 +575,18 @@ def _compress_body(body: str) -> str:
|
|||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _kand_info(p: dict) -> dict:
|
||||||
|
"""Z Medicus pacienta udělá lehký dict kandidáta pro Telegram dotaz."""
|
||||||
|
return {
|
||||||
|
"idpac": p.get("idpac"),
|
||||||
|
"rc": _norm_rc(p.get("rodcis") or ""),
|
||||||
|
"jmeno": p.get("jmeno") or "",
|
||||||
|
"prijmeni": p.get("prijmeni") or "",
|
||||||
|
"datnar": str(p.get("datnar") or "")[:10],
|
||||||
|
"poj": p.get("poj") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _format_leky(leky: list) -> str:
|
def _format_leky(leky: list) -> str:
|
||||||
"""Formátuje seznam léků pro pole 'Název léků' — čárkami oddělený výčet."""
|
"""Formátuje seznam léků pro pole 'Název léků' — čárkami oddělený výčet."""
|
||||||
parts = []
|
parts = []
|
||||||
@@ -543,8 +644,16 @@ def _medevio_find_patient(rc_normalized: str) -> str | None:
|
|||||||
# =========================
|
# =========================
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
log("\n" + "=" * 70)
|
log("\n" + "=" * 70)
|
||||||
log(f"START — schránka={MAILBOX}, test na {NEWEST_N} nejnovějších mailech")
|
log(f"START — schránka={MAILBOX}, {NEWEST_N} nejnovějších mailů")
|
||||||
log("REŽIM: read-only (ve schránce se nic nemění)")
|
log(f"REŽIM: zakládá požadavky v Medeviu; zpracované maily značí štítkem "
|
||||||
|
f"'{PROCESSED_CATEGORY}' (a příště přeskakuje)")
|
||||||
|
|
||||||
|
# Zajisti kategorii v master-listu schránky (s barvou). Best-effort.
|
||||||
|
try:
|
||||||
|
graph_mail.ensure_category(MAILBOX, PROCESSED_CATEGORY)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[POZOR] kategorii '{PROCESSED_CATEGORY}' nelze zajistit "
|
||||||
|
f"({type(e).__name__}: {e}) — chybí asi Mail.ReadWrite oprávnění")
|
||||||
|
|
||||||
msgs = newest_inbox_messages(MAILBOX, NEWEST_N)
|
msgs = newest_inbox_messages(MAILBOX, NEWEST_N)
|
||||||
log(f"Načteno {len(msgs)} mailů.")
|
log(f"Načteno {len(msgs)} mailů.")
|
||||||
@@ -561,6 +670,15 @@ def main() -> None:
|
|||||||
log(f" Od: {sender.get('name', '')} <{sender.get('address', '')}>")
|
log(f" Od: {sender.get('name', '')} <{sender.get('address', '')}>")
|
||||||
log(f" Předmět: {subj}")
|
log(f" Předmět: {subj}")
|
||||||
|
|
||||||
|
# Idempotence: mail už agent jednou zpracoval → přeskoč (žádný duplicitní požadavek).
|
||||||
|
if PROCESSED_CATEGORY in (msg.get("categories") or []):
|
||||||
|
log(f" => PŘESKOČENO — již zpracováno (štítek {PROCESSED_CATEGORY})\n")
|
||||||
|
continue
|
||||||
|
# Už čeká na potvrzení člověka přes Telegram → znovu se neptej.
|
||||||
|
if _pending.je_mail_pending(msg["id"]):
|
||||||
|
log(" => PŘESKOČENO — čeká na odpověď přes Telegram\n")
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v = classify(msg)
|
v = classify(msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -612,21 +730,61 @@ def main() -> None:
|
|||||||
for p in candidates:
|
for p in candidates:
|
||||||
log(f" - {lookup.describe(p)}")
|
log(f" - {lookup.describe(p)}")
|
||||||
|
|
||||||
# Pokud je pacient jednoznačně identifikován, založ požadavek v Medeviu.
|
# Skóre jistoty identifikace → rozhodnutí: založit / zeptat se člověka.
|
||||||
if identified_patient:
|
if identified_patient:
|
||||||
|
skore, duvody = skore_jistoty(
|
||||||
|
v, identified_patient, sender.get("address", ""), lookup
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
skore, duvody = 0, ["pacient nedohledán v kartotéce"]
|
||||||
|
log(f" Jistota: {skore}/100 — {'; '.join(duvody) or 'bez signálů'}")
|
||||||
|
|
||||||
|
leky_str = _format_leky(v.get("leky") or [])
|
||||||
|
pozn_str = _format_poznamka(msg)
|
||||||
|
|
||||||
|
if identified_patient and skore >= SCORE_AUTO:
|
||||||
|
# Vysoká jistota → založ rovnou.
|
||||||
rc = _norm_rc(identified_patient.get("rodcis") or "")
|
rc = _norm_rc(identified_patient.get("rodcis") or "")
|
||||||
leky_str = _format_leky(v.get("leky") or [])
|
|
||||||
pozn_str = _format_poznamka(msg)
|
|
||||||
patient_uuid = _medevio_find_patient(rc)
|
patient_uuid = _medevio_find_patient(rc)
|
||||||
if not patient_uuid:
|
if not patient_uuid:
|
||||||
log(f" Medevio: [NENALEZEN] RČ {rc} v Medeviu nenalezeno — požadavek nezaložen")
|
log(f" Medevio: [NENÍ V MEDEVIU] RČ {rc} — k ruční kontrole")
|
||||||
|
try:
|
||||||
|
graph_mail.add_category(MAILBOX, msg["id"], MANUAL_CATEGORY)
|
||||||
|
log(f" Mail: [OZNAČEN] {MANUAL_CATEGORY}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" Mail: [POZOR] štítek nenastaven "
|
||||||
|
f"({type(e).__name__}: {e})")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
result = _medevio.zaloz_pozadavek_recept(patient_uuid, leky_str, pozn_str)
|
result = _medevio.zaloz_pozadavek_recept(
|
||||||
log(f" Medevio: [ZALOZENO] požadavek {result['request_id']}"
|
patient_uuid, leky_str, pozn_str
|
||||||
f" | léky: {leky_str}")
|
)
|
||||||
|
log(f" Medevio: [ZALOZENO] požadavek "
|
||||||
|
f"{result['request_id']} [{skore}/100] | léky: {leky_str}")
|
||||||
|
# Označ mail jako zpracovaný → příště se přeskočí (idempotence).
|
||||||
|
try:
|
||||||
|
graph_mail.add_category(MAILBOX, msg["id"], PROCESSED_CATEGORY)
|
||||||
|
log(f" Mail: [OZNAČEN] štítek {PROCESSED_CATEGORY}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f" Mail: [POZOR] štítek nenastaven "
|
||||||
|
f"({type(e).__name__}: {e}) — riziko duplicity při dalším běhu!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f" Medevio: [CHYBA] {type(e).__name__}: {e}")
|
log(f" Medevio: [CHYBA] {type(e).__name__}: {e}")
|
||||||
|
else:
|
||||||
|
# Nejistá identifikace → NEZAKLÁDAT, zeptat se člověka přes Telegram.
|
||||||
|
kandidati = [_kand_info(p) for p in candidates]
|
||||||
|
_pending.pridej(
|
||||||
|
email_message_id=msg["id"],
|
||||||
|
email_subject=subj,
|
||||||
|
sender=f"{sender.get('name', '')} <{sender.get('address', '')}>",
|
||||||
|
leky_str=leky_str,
|
||||||
|
pozn_str=pozn_str,
|
||||||
|
skore=skore,
|
||||||
|
duvody=duvody,
|
||||||
|
kandidati=kandidati,
|
||||||
|
)
|
||||||
|
log(f" Rozhodnutí: [DOTAZ] jistota {skore} < {SCORE_AUTO} "
|
||||||
|
f"— čeká na potvrzení přes Telegram ({len(kandidati)} kandidátů)")
|
||||||
|
|
||||||
log("")
|
log("")
|
||||||
|
|
||||||
|
|||||||
+45
-6
@@ -736,6 +736,8 @@ def get_pacient(patient_id: str) -> dict:
|
|||||||
RECEPT_SID = "ERECEPT_SIMPLEST_BEZ_DAVKOVANI"
|
RECEPT_SID = "ERECEPT_SIMPLEST_BEZ_DAVKOVANI"
|
||||||
RECEPT_STEP_ID = "erecept-gp-request"
|
RECEPT_STEP_ID = "erecept-gp-request"
|
||||||
RECEPT_USER_ECRF_ID = "79488e86-e9e5-47e3-8b19-7e5229427f23" # šablona kliniky
|
RECEPT_USER_ECRF_ID = "79488e86-e9e5-47e3-8b19-7e5229427f23" # šablona kliniky
|
||||||
|
CLAUDE_TAG_ID = "c136aeca-0625-4c43-b81f-fc3949ec6ba6" # štítek "CLAUDE"
|
||||||
|
OVERIT_TAG_ID = "9d3271b3-309d-4d20-93ee-285f3e56ba42" # štítek "OVĚŘIT PACIENTA"
|
||||||
|
|
||||||
_FILL_MUTATION = """
|
_FILL_MUTATION = """
|
||||||
mutation Step_FillECRFForm($input: FillECRFFormInput!) {
|
mutation Step_FillECRFForm($input: FillECRFFormInput!) {
|
||||||
@@ -751,18 +753,41 @@ mutation PatientRequestSubmission_CreatePatientRequestWithoutReservation(
|
|||||||
) { id }
|
) { id }
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
_ASSIGN_TAG_MUTATION = """
|
||||||
|
mutation TagRequestEditModal_AssignTagToRequest(
|
||||||
|
$clinicSlug: String!, $requestId: UUID!, $tagId: UUID!
|
||||||
|
) {
|
||||||
|
tagRequest: assignTagToPatientRequest(
|
||||||
|
clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId
|
||||||
|
) { id }
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
|
def prirad_stitek(request_id: str, tag_id: str) -> None:
|
||||||
|
"""Přiřadí požadavku štítek (tag) podle jeho UUID."""
|
||||||
|
_gql("TagRequestEditModal_AssignTagToRequest", _ASSIGN_TAG_MUTATION, {
|
||||||
|
"clinicSlug": CLINIC_SLUG,
|
||||||
|
"requestId": request_id,
|
||||||
|
"tagId": tag_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def zaloz_pozadavek_recept(patient_id: str, leky: str, poznamka: str = "") -> dict:
|
def zaloz_pozadavek_recept(patient_id: str, leky: str, poznamka: str = "",
|
||||||
|
stitek: bool = True, extra_stitky: list = None) -> dict:
|
||||||
"""Založí v Medeviu požadavek "Recept na léky" za pacienta.
|
"""Založí v Medeviu požadavek "Recept na léky" za pacienta.
|
||||||
|
|
||||||
Požadavek se objeví v aktivní frontě ordinace stejně, jako by ho pacient
|
Požadavek se objeví v aktivní frontě ordinace stejně, jako by ho pacient
|
||||||
založil sám v aplikaci.
|
založil sám v aplikaci — vyplní oba fieldy dotazníku: "Název léků" (leky)
|
||||||
|
a "Poznámka" (poznamka). Volitelně přiřadí štítek CLAUDE pro odlišení
|
||||||
|
automaticky založených požadavků.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
patient_id: UUID pacienta (z hledej_pacienta / get_pacient).
|
patient_id: UUID pacienta (z hledej_pacienta / get_pacient).
|
||||||
leky: Volný text názvů léků (obsah pole "Název léků:").
|
leky: Volný text názvů léků (pole dotazníku "Název léků:").
|
||||||
poznamka: Volitelná uživatelská poznámka k požadavku (userNote).
|
poznamka: Text do pole dotazníku "Poznámka" (jde přes userNote).
|
||||||
|
stitek: True = přiřadí štítek CLAUDE (default).
|
||||||
|
extra_stitky: Volitelný seznam UUID dalších štítků (např. OVĚŘIT PACIENTA).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fill = _gql("Step_FillECRFForm", _FILL_MUTATION, {
|
fill = _gql("Step_FillECRFForm", _FILL_MUTATION, {
|
||||||
@@ -796,12 +821,26 @@ def zaloz_pozadavek_recept(patient_id: str, leky: str, poznamka: str = "") -> di
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
request_id = req["patientRequest"]["id"]
|
||||||
|
|
||||||
|
# Štítek CLAUDE — označení automaticky založených požadavků.
|
||||||
|
tag_ok = False
|
||||||
|
if stitek:
|
||||||
|
prirad_stitek(request_id, CLAUDE_TAG_ID)
|
||||||
|
tag_ok = True
|
||||||
|
|
||||||
|
# Další volitelné štítky (např. OVĚŘIT PACIENTA u nižší jistoty).
|
||||||
|
for tid in (extra_stitky or []):
|
||||||
|
prirad_stitek(request_id, tid)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"request_id": req["patientRequest"]["id"],
|
"request_id": request_id,
|
||||||
"fill_id": fill_id,
|
"fill_id": fill_id,
|
||||||
"patient_id": patient_id,
|
"patient_id": patient_id,
|
||||||
"leky": leky,
|
"leky": leky,
|
||||||
|
"stitek_claude": tag_ok,
|
||||||
|
"extra_stitky": list(extra_stitky or []),
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
log(f"zaloz_pozadavek_recept chyba: {traceback.format_exc()}")
|
log(f"zaloz_pozadavek_recept chyba: {traceback.format_exc()}")
|
||||||
|
|||||||
Reference in New Issue
Block a user