notebookvb
This commit is contained in:
@@ -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']}, "
|
||||
|
||||
Reference in New Issue
Block a user