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
+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']}, "