Files
Vladimir Buzalka a7f33afb66 notebookvb
2026-06-10 08:53:01 +02:00

8.0 KiB
Raw Permalink Blame History

EmailAgent — agent na přijaté faktury

Hlídá schránku ordinace@buzalkova.cz a ukládá PDF přílohy přijatých faktur.

Ochrana proti duplicitám (2 vrstvy)

  1. state.jsonmessage_id zpracovaných mailů. Stejný mail se podruhé vůbec nestahuje (hlavní pojistka).
  2. sha256 obsahu — při startu se načtou hashe všech PDF v cílové složce; stažená příloha se porovná hned po stažení. Když je stejný obsah už ve složce (i pod jiným názvem), soubor se přeskočí ([DUPLIKÁT]) a ušetří se i AI volání za pojmenování. Řeší případ, kdy se state.json ztratí nebo dodavatel pošle fakturu dvakrát. Porovnává se obsah, ne název — AI název se mezi běhy může v části [popis] lehce lišit.

Nasazení na unraid (Tower, 24/7)

Cílový adresář: /mnt/user/Scripts/StahovaniFaktur/ (= /scripts/StahovaniFaktur/ v kontejneru python-runner). Běh: docker exec python-runner python3 /scripts/StahovaniFaktur/faktury_agent.py.

  • .env na serveru má STORAGE=dropbox + ANTHROPIC_API_KEY + DROPBOX_* (na serveru není Medevio/.env ani Dropbox mount).
  • Závislosti v kontejneru: dropbox msal pymupdf requests (_ensure_deps() je doinstaluje i samo).
  • Plánování přes unraid User Scripts plugin (ne cron v dockeru):
    • wrapper /boot/config/plugins/user.scripts/scripts/StahovaniFaktur/script (flock + docker exec, loguje do /mnt/user/Scripts/logs/stahovani_faktur.log),
    • rozvrh 0 6,18 * * * v schedule.jsoncustomSchedule.cronupdate_cron/etc/cron.d/root.
    • Pozn.: /boot je FAT32, skript NELZE spustit přímým execem — plugin ho pouští přes startCustom.php (bash), proto to funguje.
  • Pozor na dvojí běh: spouštět jen ze serveru, ne zároveň lokálně (oddělené state.json).

Spuštění

cd U:\OrdinaceProjekt\EmailAgent
python faktury_agent.py

Skript je idempotentní — opakované spuštění nestáhne nic dvakrát (viz state.json). Lze přidat do Windows Task Scheduleru.

Co dělá (tok)

  1. Graph API (graph_mail.py) — načte nové maily s přílohou ze složek Inbox + přímé podsložky (vynechává Junk/Deleted/Sent/Drafts), od posledního běhu (state.jsonlast_run, s překryvem 1 den).
  2. Levný předfiltr (Python, zdarma) — propustí jen maily, kde se slovo faktur* vyskytuje v předmětu, těle, nebo v názvu přílohy. Maily, které neprojdou, se rovnou označí jako zpracované.
  3. AI klasifikace (Claude, placené) — jen na propuštěné maily. Model claude-haiku-4-5 rozhodne je_faktura: true/false a vybere správnou .pdf přílohu (ne ISDOC/XML/obrázek/VOP/dodací list/objednávku).
  4. Stažení vybraného PDF přes Graph.
  5. Ověření obsahu PDF (Python, zdarma) — přečte text PDF (fitz/PyMuPDF) a hledá slovo faktur*:
    • ano → potvrzeno, uloží se.
    • ne → text fakturu neobsahuje (AI nejspíš vybrala špatnou přílohu) → neukládá se, jen zaloguje [PDF NEPOTVRZENO].
    • bez_textu → PDF nemá textovou vrstvu (skenovaná faktura) → uloží se, ale zaloguje [PDF BEZ TEXTU] k ruční kontrole.
  6. Návrh názvu (Claude nad textem PDF)propose_filename() z textu faktury vytěží datum/typ/dodavatele/číslo/popis/částku a vrátí jednotný název YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf. Pravidla převzata z Faktury/FakturyRenameOpenAI.py (např. Distribuce CZ → Ptáček). Při chybě nebo u skenu bez textu se použije původní název přílohy.
  7. Uložení pod navrženým názvem. Konflikty řeší přípona (2), (3)
  8. Kategorie + přesun (Graph, destruktivní) — mail se označí kategorií ClaudeProcessed (zelená) a přesune do Inbox/ProcessedByAgent/Invoices. Děje se i u duplikátu (úklid Inboxu). Vyžaduje Mail.ReadWrite.
  9. Zápis state.json + log _log_faktury.txt.
  10. Summary e-mail — po každém běhu se z SUMMARY_FROM (reports@buzalka.cz) pošle souhrn na SUMMARY_TO (vladimir.buzalka@buzalka.cz) přes Graph (graph_mail.send_mail, vyžaduje Mail.Send). Tělo = log běhu.

Na konci běhu skript vypíše cenu AI za běh — počet volání, tokeny a částku v USD i Kč (kurz USD_TO_CZK, ceník modelů v PRICING). Orientačně ~0,1 Kč na fakturu (klasifikace + pojmenování).

Konfigurace (faktury_agent.py, sekce NASTAVENÍ)

Konstanta Význam
MAILBOX sledovaná schránka
TARGET_SUBPATH podsložka v Dropboxu — root přes Knihovny/najdi_dropbox.py
FIRST_RUN_DAYS kolik dní dozadu při prvním běhu (prázdný state)
ANTHROPIC_MODEL model pro klasifikaci faktura ano/ne
ANTHROPIC_NAMING_MODEL model pro návrh názvu souboru
FAKTUR_RE regex předfiltru (faktur)
CATEGORY / CATEGORY_COLOR kategorie přidaná zpracovanému mailu + barva
PROCESSED_FOLDER_PARTS cílová podsložka přesunu (pod Inbox)
SKIP_FOLDERS složky vynechané při skenování

Úložiště — lokální vs. Dropbox API (storage.py)

Cíl je stejná složka, ale dvěma cestami podle prostředí (STORAGE):

STORAGE Backend Kdy
local (default) LocalStorage — filesystem přes najdi_dropbox.get_dropbox_root() + TARGET_SUBPATH Windows (je tu Dropbox mount)
dropbox DropboxStorage — Dropbox HTTP API, cesta DROPBOX_TARGET_PATH unraid/server (bez Dropbox mountu)

Společné rozhraní: load_hashes(), hash_bytes(data), save(name, data), describe(). Dedup: lokálně sha256 obsahu, přes API Dropbox content_hash (blokový sha256 z metadat — nestahuje soubory). Listing v Dropboxu stránkuje.

Cílová složka: <Dropbox>\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté\ (Dropbox API: /Ordinace/.../#040 Faktury přijaté, Full Dropbox app).

Po zpracování (destruktivní): CATEGORY (= ClaudeProcessed, barva CATEGORY_COLOR) + přesun do Inbox/ + PROCESSED_FOLDER_PARTS (= ProcessedByAgent/Invoices). Tato složka se při skenování přeskakuje (SKIP_FOLDERS).

Autentizace

  • Microsoft Graph — app registrace (application permissions) sdílená s Knihovny/EmailMessagingGraph.py. Vyžaduje granty Mail.Read (čtení/přílohy) a Mail.ReadWrite (kategorie + přesun mailů) a Mail.Send (summary e-mail). Credentials jsou natvrdo v graph_mail.py (tenant/client/secret).
  • Anthropic — klíč ANTHROPIC_API_KEY z Medevio/.env.
  • Dropbox API (jen STORAGE=dropbox) — DROPBOX_APP_KEY / DROPBOX_APP_SECRET / DROPBOX_APP_REFRESH_TOKEN z EmailAgent/.env (gitignored). Full Dropbox app, účet vladimir.buzalka@buzalka.cz. Refresh token = trvalý, access token se obnovuje sám.

Závislosti

msal, requests, fitz (PyMuPDF), a pro STORAGE=dropbox navíc dropbox (pip install dropbox). Na unraid/python-runner je nutné dropbox doinstalovat.

Soubory

Soubor Popis
faktury_agent.py hlavní skript
graph_mail.py vrstva nad Graphem (čtení/zápis zpráv, stahování příloh)
storage.py úložiště faktur — LocalStorage / DropboxStorage
.env Dropbox credentials + volitelně STORAGE (gitignored)
state.json ID zpracovaných mailů + last_run
_log_faktury.txt log běhů

Poznámky / TODO

  • Destruktivní — zpracovaný mail se kategorizuje a přesouvá z Inboxu. Maily se nemažou. Přesun mění message_id; idempotenci hlídá state.json (původní id) i to, že přesunuté maily jsou v nesnímané podsložce.
  • Maily, které předfiltr nepropustí, se uloží do processed_ids jako vyřízené — když se prompt/pravidla později změní, znovu se nepřehodnotí. Pro re-test smaž příslušné ID ze state.json.
  • Graph nezvládne $orderby spolu s filtrem hasAttachments (InefficientFilter) — proto se na serveru neřadí.