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

157 lines
8.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:**
`<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í.