notebookvb

This commit is contained in:
Vladimir Buzalka
2026-06-14 12:07:35 +02:00
parent 9133fe9497
commit 2bdac59676
16 changed files with 1484 additions and 29 deletions
+22 -10
View File
@@ -4,18 +4,22 @@ 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: testovací režim (read-only)
## Stav: nasazeno na toweru (produkce), DELTA režim
`recepty_agent.py` zatím jen načte `NEWEST_N` (= 5) nejnovějších mailů
z Inboxu, klasifikuje je Claude modelem a vypíše report do konzole
a `_log_recepty.txt`. **Ve schránce nic nemění** (žádné kategorie,
přesuny, odpovědi), žádný `state.json`.
`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** `newest_inbox_messages()`: N nejnovějších mailů z Inboxu,
bez filtru na přílohy (`$orderby receivedDateTime desc` — bez filtru
funguje, na rozdíl od kombinace s `hasAttachments`, viz EmailAgent).
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),
@@ -162,11 +166,19 @@ založil „Recept na léky" správnému pacientovi (dotazník Název léků + P
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.
- 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
```
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
recept_dialog.py — formátování otázky do Telegramu a parsování odpovědi člověka.
Čisté funkce bez vedlejších efektů (snadno testovatelné).
"""
import re
import unicodedata
def _bez_diakritiky(s: str) -> str:
s = unicodedata.normalize("NFKD", s or "")
return "".join(c for c in s if not unicodedata.combining(c))
def format_otazka(rec: dict) -> str:
"""Sestaví text otázky do Telegramu z pending záznamu."""
sk = rec.get("skore")
lines = ["🟡 Žádost o recept — nejistá identifikace"
+ (f" (jistota {sk}/100)" if sk is not None else "")]
if rec.get("sender"):
lines.append(f"Od: {rec['sender']}")
if rec.get("email_subject"):
lines.append(f"Předmět: {rec['email_subject']}")
if rec.get("leky_str"):
lines.append(f"Léky: {rec['leky_str']}")
if rec.get("duvody"):
lines.append("Proč nejisté: " + "; ".join(rec["duvody"]))
kand = rec.get("kandidati") or []
if kand:
lines.append("")
lines.append("Kandidáti:")
for i, k in enumerate(kand, 1):
lines.append(
f" {i}) {k.get('prijmeni', '')} {k.get('jmeno', '')}"
f", RČ {k.get('rc', '?')}, nar. {k.get('datnar', '?')}"
f", poj. {k.get('poj', '?')}"
)
else:
lines.append("")
lines.append("(žádný kandidát v kartotéce)")
lines.append("")
lines.append("Odpověz jako reply na tuto zprávu:")
lines.append("• RČ správného pacienta (definitivní)")
if kand:
lines.append("• nebo číslo kandidáta (1, 2, …)")
lines.append("• nebo „ne“ = nezakládat")
return "\n".join(lines)
_SKIP = {
"ne", "nezakladat", "preskoc", "preskocit", "zahodit", "zahod",
"ignoruj", "nic", "stop", "nezaklada", "nezakladej",
}
def parse_odpoved(text: str) -> dict:
"""Rozparsuje odpověď člověka.
Vrací dict:
{"akce": "rc", "rc": "7309208104"} definitivní RČ pacienta
{"akce": "kandidat", "index": 1} výběr kandidáta podle pořadí
{"akce": "preskoc"} nezakládat
{"akce": "nejasne"} nerozpoznáno
"""
t = (text or "").strip()
low = _bez_diakritiky(t).lower().strip().rstrip(".!")
if low in _SKIP:
return {"akce": "preskoc"}
digits = re.sub(r"\D", "", t)
if len(digits) in (9, 10):
return {"akce": "rc", "rc": digits}
m = re.fullmatch(r"\s*(\d{1,2})\s*", t)
if m:
return {"akce": "kandidat", "index": int(m.group(1))}
return {"akce": "nejasne"}
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
recept_pending.py — fronta "čekajících" žádostí o recept, u kterých si agent
NENÍ jistý identifikací pacienta a potřebuje potvrzení člověka přes Telegram.
E-mailový agent sem zapíše záznam (stav 'ceka') a NIC dalšího nedělá.
Resolver (recept_resolver.py) záznamy bere, pošle otázku do Telegramu a podle
odpovědi člověka založí požadavek správnému pacientovi.
Úložiště: JSON soubor _pending_recepty.json vedle tohoto modulu (atomický zápis).
Stavy záznamu: 'ceka''zalozeno' | 'preskoceno'.
"""
import json
import os
import tempfile
import uuid
from datetime import datetime
from pathlib import Path
STORE = Path(__file__).resolve().parent / "_pending_recepty.json"
def _load() -> list:
if not STORE.exists():
return []
try:
return json.loads(STORE.read_text(encoding="utf-8"))
except Exception:
return []
def _save(items: list) -> None:
tmp = tempfile.NamedTemporaryFile(
"w", encoding="utf-8", dir=str(STORE.parent), delete=False, suffix=".tmp"
)
try:
json.dump(items, tmp, ensure_ascii=False, indent=2)
tmp.flush()
os.fsync(tmp.fileno())
tmp.close()
os.replace(tmp.name, STORE)
except Exception:
try:
os.unlink(tmp.name)
except Exception:
pass
raise
def pridej(*, email_message_id: str, email_subject: str = "", sender: str = "",
leky_str: str = "", pozn_str: str = "", skore=None,
duvody=None, kandidati=None) -> dict:
"""Přidá nový čekající dotaz. leky_str/pozn_str se použijí při pozdějším
založení požadavku, kandidáti se ukážou člověku v otázce."""
items = _load()
rec = {
"id": uuid.uuid4().hex,
"vytvoreno": datetime.now().isoformat(timespec="seconds"),
"email_message_id": email_message_id,
"email_subject": email_subject,
"sender": sender,
"leky_str": leky_str,
"pozn_str": pozn_str,
"skore": skore,
"duvody": duvody or [],
"kandidati": kandidati or [],
"otazka_message_id": None, # vyplní resolver po odeslání otázky
"stav": "ceka", # ceka | zalozeno | preskoceno
"vysledek": None,
}
items.append(rec)
_save(items)
return rec
def cekajici() -> list:
return [r for r in _load() if r.get("stav") == "ceka"]
def cekajici_bez_otazky() -> list:
"""Záznamy, na které se ještě neposlala otázka do Telegramu."""
return [r for r in _load()
if r.get("stav") == "ceka" and not r.get("otazka_message_id")]
def je_mail_pending(email_message_id: str) -> bool:
return any(r.get("email_message_id") == email_message_id
and r.get("stav") == "ceka" for r in _load())
def najdi_dle_otazky(otazka_message_id) -> dict | None:
if otazka_message_id is None:
return None
for r in _load():
if r.get("otazka_message_id") == otazka_message_id:
return r
return None
def aktualizuj(rec_id: str, **fields) -> dict | None:
items = _load()
out = None
for r in items:
if r.get("id") == rec_id:
r.update(fields)
out = r
break
if out is not None:
_save(items)
return out
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
recept_resolver.py — vyřizuje "čekající" žádosti o recept přes Telegram.
Jediný proces, který mluví s Telegramem (kvůli getUpdates). Ve smyčce:
1) pošle otázky pro nové pending záznamy (které ještě otázku nemají),
2) long-polluje odpovědi; odpověď (reply na otázku) → podle obsahu:
• RČ (910 číslic) → definitivní volba pacienta → založí požadavek
• číslo kandidáta → vybere kandidáta → založí požadavek
• „ne“ → přeskočí (nezakládá)
Po založení označí původní e-mail kategorií ClaudeZpracovalRecept.
Telegram přenos je v recept_telegram.py (token/chat dodá uživatel).
Bez odpovědi záznam zůstává 'ceka' (čeká se libovolně dlouho).
Spuštění: python recept_resolver.py
"""
import re
import sys
import time
from pathlib import Path
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent / "EmailAgent"))
sys.path.insert(0, str(HERE.parent))
import recept_pending as PEND # noqa: E402
import recept_dialog as DLG # noqa: E402
import recept_telegram as TG # noqa: E402
import graph_mail # noqa: E402 (značení mailu)
import mcp_medevio as MED # noqa: E402 (zaloz_pozadavek_recept)
from Knihovny.mysql_db import connect_mysql # noqa: E402
MAILBOX = "ordinace@buzalkova.cz"
PROCESSED_CATEGORY = "ClaudeZpracovalRecept"
SINCE_FILE = HERE / "_resolver_since.txt" # poslední zpracované message_id
def log(msg: str) -> None:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
def _norm_rc(s: str) -> str:
return re.sub(r"\D", "", s or "")
def najdi_pacienta_dle_rc(rc: str):
"""RČ → (uuid, jmeno, prijmeni) z medevio_pacient, nebo None."""
rc = _norm_rc(rc)
if not rc:
return None
try:
conn = connect_mysql()
conn.ping(reconnect=True)
cur = conn.cursor()
cur.execute(
"SELECT patient_id, name, surname FROM medevio_pacient "
"WHERE REPLACE(identification_number,'/','') = %s LIMIT 1",
[rc],
)
row = cur.fetchone()
return row if row else None
except Exception as e:
log(f"[uuid lookup chyba] {type(e).__name__}: {e}")
return None
def _load_since():
try:
return int(SINCE_FILE.read_text(encoding="utf-8").strip())
except Exception:
return None
def _save_since(s: int) -> None:
SINCE_FILE.write_text(str(s), encoding="utf-8")
def posli_nove_otazky() -> None:
"""Pošle otázku do Telegramu pro každý pending záznam bez otázky."""
for rec in PEND.cekajici_bez_otazky():
try:
mid = TG.posli_otazku(DLG.format_otazka(rec))
PEND.aktualizuj(rec["id"], otazka_message_id=mid)
log(f"otázka odeslána (pending {rec['id'][:8]}, msg {mid})")
except Exception as e:
log(f"otázku nelze odeslat ({type(e).__name__}: {e}) — zkusím příště")
break # nejspíš výpadek / nenakonfigurováno → nech na příští kolo
def _zaloz_pro_rc(rec: dict, rc: str) -> None:
info = najdi_pacienta_dle_rc(rc)
if not info:
TG.posli_zpravu(f"⚠ RČ {rc} není v Medeviu — nezakládám. Zkus jiné RČ.")
return
uuid_, jmeno, prijmeni = info[0], info[1], info[2]
try:
res = MED.zaloz_pozadavek_recept(
uuid_, rec.get("leky_str", ""), rec.get("pozn_str", "")
)
except Exception as e:
TG.posli_zpravu(f"❌ Chyba při zakládání: {type(e).__name__}: {e}")
return
PEND.aktualizuj(rec["id"], stav="zalozeno",
vysledek={"request_id": res["request_id"], "rc": rc,
"pacient": f"{prijmeni} {jmeno}"})
try:
graph_mail.add_category(MAILBOX, rec["email_message_id"], PROCESSED_CATEGORY)
except Exception as e:
log(f"[mail označení] {type(e).__name__}: {e}")
TG.posli_zpravu(f"✅ Založeno: {prijmeni} {jmeno} (RČ {rc}) — "
f"{rec.get('leky_str', '')}")
log(f"založeno {res['request_id']} pro {prijmeni} {jmeno}")
def zpracuj_odpoved(u: dict) -> None:
"""Zpracuje jednu příchozí Telegram zprávu."""
rid = u.get("reply_to_message_id")
rec = PEND.najdi_dle_otazky(rid) if rid else None
if rec is None:
cekaji = PEND.cekajici()
if len(cekaji) == 1:
rec = cekaji[0] # jediný čekající → ber to na něj
else:
if cekaji:
TG.posli_zpravu("Odpověz prosím jako reply na konkrétní dotaz "
"(čeká jich víc).")
return
if rec.get("stav") != "ceka":
return
d = DLG.parse_odpoved(u.get("text", ""))
if d["akce"] == "preskoc":
PEND.aktualizuj(rec["id"], stav="preskoceno")
TG.posli_zpravu("OK, nezakládám.")
elif d["akce"] == "rc":
_zaloz_pro_rc(rec, d["rc"])
elif d["akce"] == "kandidat":
kand = rec.get("kandidati") or []
i = d["index"] - 1
if 0 <= i < len(kand):
_zaloz_pro_rc(rec, kand[i].get("rc", ""))
else:
TG.posli_zpravu(f"Kandidát {d['index']} neexistuje (mám {len(kand)}).")
else:
TG.posli_zpravu("Nerozumím. Pošli RČ pacienta, číslo kandidáta, nebo „ne“.")
def smycka(poll_s: int = 5) -> None:
try:
TG.priprav() # naprimuj entitu Vlada (jinak fresh session spadne)
except Exception as e:
log(f"[priprav] {type(e).__name__}: {e}")
since = _load_since()
if since is None:
# první start — vezmi aktuální stav jako základ, starou historii ignoruj
since = TG.baseline_since()
_save_since(since)
log(f"baseline since_id={since}")
log("Resolver běží (Ctrl+C ukončí).")
while True:
try:
posli_nove_otazky()
nove, since = TG.nacti_odpovedi(since)
for u in nove:
zpracuj_odpoved(u)
_save_since(since)
except KeyboardInterrupt:
log("Konec.")
break
except Exception as e:
log(f"[smyčka] {type(e).__name__}: {e}")
time.sleep(poll_s)
if __name__ == "__main__":
smycka()
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
recept_telegram.py — Telegram přenos pro recept-resolver.
Používá USER účet agenta (Claude Buzalka @vlado_claude_agent) přes Telethon
z Knihovny/telegram_user.py — VLASTNÍ session "recepty" (per-agent autorizace).
Odpovědi se párují přes Telegram reply (reply_to_msg_id), takže víc agentů na
témž účtu se nepoplete a každý si hlídá jen své odpovědi.
JEDNORÁZOVÝ KROK (dělá uživatel v terminálu — čeká na SMS kód):
python -m Knihovny.telegram_user login --jako recepty
Konfigurace (Medevio/.env): TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_PHONE
(viz Trilium „2026-06-14 Telegram — bot, user účet agenta a MCP server").
"""
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from Knihovny.telegram_user import posli_jako_ja, precti_zpravy, _new_client # noqa: E402
# Vlastní session recept-agenta (jedno přihlášení: login --jako recepty).
SESSION = "recepty"
# Komu agent píše = Vladův hlavní účet (z Trilium / TELEGRAM_CHAT_ID).
VLADO_UID = int(os.environ.get("TELEGRAM_CHAT_ID", "6639316354"))
def priprav() -> None:
"""Načte dialogy → do session cache se uloží entita Vlada. Nová session by ji
jinak neznala a posílání by spadlo na 'Could not find the input entity'.
Volá se jednou při startu resolveru (idempotentní, levné)."""
with _new_client(SESSION) as client:
if client.is_user_authorized():
client.get_dialogs(limit=50)
def posli_otazku(text: str) -> int:
"""Pošle otázku Vladovi z účtu agenta. Vrátí message_id (pro párování reply)."""
msg = posli_jako_ja(VLADO_UID, text, session=SESSION)
return msg.id
def posli_zpravu(text: str) -> None:
"""Pošle prostou zprávu (potvrzení / chybu)."""
posli_jako_ja(VLADO_UID, text, session=SESSION)
def baseline_since() -> int:
"""Aktuální nejvyšší message_id v chatu — výchozí bod při prvním startu
(aby resolver nezpracoval starou historii)."""
zpravy = precti_zpravy(VLADO_UID, limit=1, session=SESSION)
return max((z["id"] for z in zpravy), default=0)
def nacti_odpovedi(since_id: int = 0, limit: int = 50):
"""Vrátí (nove_odpovedi, novy_since_id).
Bere jen PŘÍCHOZÍ zprávy (ne naše) s id > since_id. Každá:
{message_id, text, reply_to_message_id}
"""
zpravy = precti_zpravy(VLADO_UID, limit=limit, session=SESSION)
out = []
new_since = since_id
for z in zpravy:
mid = z["id"]
if mid > new_since:
new_since = mid
if mid > since_id and not z["odeslal_ja"]:
out.append({
"message_id": mid,
"text": z["text"],
"reply_to_message_id": z["reply_na"],
})
out.sort(key=lambda x: x["message_id"]) # od nejstarší
return out, new_since
+80 -15
View File
@@ -26,7 +26,7 @@ import os
import re
import sys
import unicodedata
from datetime import date, timedelta
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
try:
@@ -52,8 +52,11 @@ import recept_pending as _pending # noqa: E402 fronta dotazů (nejistá identi
# =========================
MAILBOX = "ordinace@buzalkova.cz"
# Kolik nejnovějších mailů z Inboxu zpracovat (testovací režim).
NEWEST_N = 5
# DELTA REŽIM: zpracují se všechny maily s receivedDateTime > vodoznak
# (čas posledního zpracovaného mailu, uložen v _last_processed.txt). Při prvním
# běhu (vodoznak chybí) se vodoznak nastaví na nejnovější mail v Inboxu a od
# příště se zpracuje jen to, co přijde POTÉ (historie se nedohání).
MAX_PER_RUN = 200 # pojistka: max mailů na jeden běh (kdyby byl vodoznak hodně zpět)
# 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čí
@@ -85,6 +88,7 @@ _cost = {"input_tokens": 0, "output_tokens": 0, "usd": 0.0, "calls": 0}
HERE = Path(__file__).resolve().parent
LOG_FILE = HERE / "_log_recepty.txt"
WATERMARK_FILE = HERE / "_last_processed.txt" # ISO čas (receivedDateTime) posledního zpracovaného mailu
# =========================
@@ -112,18 +116,59 @@ def log(msg: str) -> None:
# =========================
# ČTENÍ MAILŮ (Graph, read-only)
# =========================
def newest_inbox_messages(mailbox: str, n: int) -> list[dict]:
"""N nejnovějších mailů z Inboxu (bez filtru na přílohy), tělo jako text."""
_SELECT = "id,subject,from,receivedDateTime,bodyPreview,body,categories"
def _load_watermark() -> tuple[str | None, str | None]:
"""Vrátí (receivedDateTime, id) posledního zpracovaného mailu (řádek 1 a 2
v _last_processed.txt). id slouží k odfiltrování hraničního mailu, který se
kvůli sub-sekundové přesnosti vrací i při filtru `gt` na oříznutý čas."""
try:
lines = WATERMARK_FILE.read_text(encoding="utf-8").splitlines()
t = (lines[0].strip() if lines else "") or None
i = (lines[1].strip() if len(lines) > 1 else "") or None
return t, i
except Exception:
return None, None
def _save_watermark(iso: str, msg_id: str = "") -> None:
WATERMARK_FILE.write_text(f"{iso}\n{msg_id}\n", encoding="utf-8")
def newest_received(mailbox: str) -> tuple[str, str]:
"""(receivedDateTime, id) nejnovějšího mailu v Inboxu — seed vodoznaku při
prvním běhu. Když je schránka prázdná, vrátí (aktuální UTC, '')."""
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
params = {"$orderby": "receivedDateTime desc", "$select": "id,receivedDateTime", "$top": 1}
r = requests.get(url, headers=graph_mail._headers(), params=params, timeout=60)
r.raise_for_status()
vals = r.json().get("value", [])
if vals:
return vals[0]["receivedDateTime"], vals[0]["id"]
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), ""
def nove_inbox_messages(mailbox: str, since_iso: str) -> list[dict]:
"""Všechny maily z Inboxu s receivedDateTime > since_iso, od NEJSTARŠÍHO.
Stránkuje přes @odata.nextLink, max MAX_PER_RUN za jeden běh."""
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
params = {
"$orderby": "receivedDateTime desc",
"$select": "id,subject,from,receivedDateTime,bodyPreview,body,categories",
"$top": n,
"$filter": f"receivedDateTime gt {since_iso}",
"$orderby": "receivedDateTime asc",
"$select": _SELECT,
"$top": 50,
}
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
r = requests.get(url, headers=headers, params=params, timeout=60)
r.raise_for_status()
return r.json().get("value", [])[:n]
out: list[dict] = []
while url and len(out) < MAX_PER_RUN:
r = requests.get(url, headers=headers, params=params, timeout=60)
r.raise_for_status()
data = r.json()
out.extend(data.get("value", []))
url = data.get("@odata.nextLink")
params = None # nextLink už nese všechny parametry
return out[:MAX_PER_RUN]
# =========================
@@ -644,7 +689,7 @@ def _medevio_find_patient(rc_normalized: str) -> str | None:
# =========================
def main() -> None:
log("\n" + "=" * 70)
log(f"START — schránka={MAILBOX}, {NEWEST_N} nejnovějších mailů")
log(f"START — schránka={MAILBOX}, DELTA režim (vše po posledním zpracovaném)")
log(f"REŽIM: zakládá požadavky v Medeviu; zpracované maily značí štítkem "
f"'{PROCESSED_CATEGORY}' (a příště přeskakuje)")
@@ -655,8 +700,26 @@ def main() -> None:
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)
log(f"Načteno {len(msgs)} mailů.")
watermark, last_id = _load_watermark()
if watermark is None:
# první běh — neznáme „poslední zpracovaný"; nastav vodoznak na nejnovější
# mail a od příště zpracovávej jen to, co přijde potom (historie se nedohání).
watermark, seed_id = newest_received(MAILBOX)
_save_watermark(watermark, seed_id)
log(f"První běh — vodoznak nastaven na {watermark}. "
f"Příští běh zpracuje maily přijaté po tomto čase.")
return
msgs = nove_inbox_messages(MAILBOX, watermark)
# `gt` na oříznutý (sekundový) čas vrací i hraniční už zpracovaný mail
# (Graph má sub-sekundovou přesnost) → odfiltruj ho podle ID.
if last_id:
msgs = [m for m in msgs if m.get("id") != last_id]
cap = " (dosažen strop MAX_PER_RUN)" if len(msgs) >= MAX_PER_RUN else ""
log(f"Vodoznak: {watermark} → nových mailů: {len(msgs)}{cap}")
if not msgs:
log("Nic nového — končím.")
return
lookup = MedicusLookup()
log(f"Medicus: kartotéka načtena ({len(lookup.patients)} pacientů, "
@@ -789,7 +852,9 @@ def main() -> None:
log("")
lookup.close()
log(f"HOTOVO: {len(msgs)} mailů, žádostí o recept: {requests_found}.")
_save_watermark(msgs[-1]["receivedDateTime"], msgs[-1].get("id", "")) # posun na nejnovější zpracovaný
log(f"HOTOVO: {len(msgs)} mailů, žádostí o recept: {requests_found}. "
f"Nový vodoznak: {msgs[-1]['receivedDateTime']}")
log(
f"CENA AI: {_cost['calls']} volání, "
f"tokeny input={_cost['input_tokens']} output={_cost['output_tokens']}, "