# 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.json`** — `message_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.json` → `customSchedule.cron` → `update_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í ```powershell 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.json` → `last_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:** `\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í.