157 lines
8.0 KiB
Markdown
157 lines
8.0 KiB
Markdown
# 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í.
|