notebookvb

This commit is contained in:
Vladimir Buzalka
2026-06-14 08:22:25 +02:00
parent 2346ad7739
commit 9133fe9497
9 changed files with 355 additions and 295 deletions
+168 -10
View File
@@ -44,6 +44,8 @@ import graph_mail # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from Knihovny.medicus_db import get_medicus_db # noqa: E402
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Í
@@ -53,6 +55,19 @@ MAILBOX = "ordinace@buzalkova.cz"
# Kolik nejnovějších mailů z Inboxu zpracovat (testovací režim).
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í.
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"
params = {
"$orderby": "receivedDateTime desc",
"$select": "id,subject,from,receivedDateTime,bodyPreview,body",
"$select": "id,subject,from,receivedDateTime,bodyPreview,body,categories",
"$top": n,
}
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
@@ -433,6 +448,80 @@ class MedicusLookup:
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 (0100), ž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
# =========================
@@ -486,6 +575,18 @@ def _compress_body(body: str) -> str:
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:
"""Formátuje seznam léků pro pole 'Název léků' — čárkami oddělený výčet."""
parts = []
@@ -543,8 +644,16 @@ def _medevio_find_patient(rc_normalized: str) -> str | None:
# =========================
def main() -> None:
log("\n" + "=" * 70)
log(f"START — schránka={MAILBOX}, test na {NEWEST_N} nejnovějších mailech")
log("REŽIM: read-only (ve schránce se nic nemění)")
log(f"START — schránka={MAILBOX}, {NEWEST_N} nejnovějších mailů")
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)
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" 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:
v = classify(msg)
except Exception as e:
@@ -612,21 +730,61 @@ def main() -> None:
for p in candidates:
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:
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 "")
leky_str = _format_leky(v.get("leky") or [])
pozn_str = _format_poznamka(msg)
patient_uuid = _medevio_find_patient(rc)
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:
try:
result = _medevio.zaloz_pozadavek_recept(patient_uuid, leky_str, pozn_str)
log(f" Medevio: [ZALOZENO] požadavek {result['request_id']}"
f" | léky: {leky_str}")
result = _medevio.zaloz_pozadavek_recept(
patient_uuid, leky_str, pozn_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:
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("")