notebookvb

This commit is contained in:
Vladimir Buzalka
2026-06-10 08:53:01 +02:00
parent 4723f9b174
commit a7f33afb66
9 changed files with 1293 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
# 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í.