7550aff102
- mail_archive.py: фикс вложений himalaya --dir → --downloads-dir; attachments/ только при has_attachment; идемпотентно - email_classifier.py: Qwen3:8b (Ollama) → теги info/urgent/task/meeting в frontmatter email.md - email_handlers.py: urgent→Telegram (Bot API+SOCKS5), task→Radicale VTODO, meeting→Radicale VEVENT; handled_* идемпотентность; --dry-run - .gitignore: игнор *.env.bak* - openspec: чейндж email-classification-handlers (в работе) - STATUS/TODO/WALKTHROUGH: прогресс сессии, подводные камни Radicale (http.client, Depth:1, формат дат)
112 lines
8.1 KiB
Markdown
112 lines
8.1 KiB
Markdown
# Design: Классификация писем и подключение обработчиков
|
||
|
||
## Context
|
||
|
||
Пайплайн почты сейчас:
|
||
```
|
||
IMAP → himalaya → email.md (frontmatter + текст) → [конец]
|
||
```
|
||
Вложения теряются (баг `--dir` вместо `--downloads-dir`). Классификации нет.
|
||
Обработчиков нет. Ollama с Qwen3:8b уже используется (contacts_extractor),
|
||
Radicale и Vikunja развёрнуты.
|
||
|
||
Известные ограничения:
|
||
- Vikunja выведена из проекта (лишняя сущность; задачи через Radicale VTODO,
|
||
см. чейндж `remove-vikunja-use-radicale-tasks`).
|
||
- Radicale работает, календарь «Рабочий» и «Задачи» есть (в «Рабочий» я уже
|
||
добавил «Обход в Глории»).
|
||
- Telegram-уведомления: Hermes gateway может слать в Telegram; нужен канал/chat_id.
|
||
- Приватность: вся LLM-обработка локально (Ollama localhost:11434).
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Починить скачивание вложений в каталог письма.
|
||
- Классифицировать каждое письмо локальной моделью, писать тег + обоснование.
|
||
- Подключать обработчики по тегам: urgent→уведомление, task→задача в Vikunja,
|
||
meeting→событие в Radicale, info→ничего.
|
||
- Идемпотентность: письмо обрабатывается один раз.
|
||
|
||
**Non-Goals:**
|
||
- Не делаем веб-интерфейс (это отдельный чейндж «Веб-интерфейс ассистента»).
|
||
- Не делаем сложный NLP / классификацию по нескольким моделям — только Qwen3:8b.
|
||
- Не мигрируем существующие письма (классификация только новых; историю можно
|
||
переклассифицировать отдельно флагом `--force`).
|
||
- Не реализуем умные дедлайны/приоритеты на основе содержания — только теги.
|
||
|
||
## Decisions
|
||
|
||
### D1: Классификатор — отдельный скрипт `email_classifier.py`
|
||
Отдельный скрипт (а не функция в mail_archive.py), потому что:
|
||
- mail_archive.py — no-agent cron каждые 5 мин, он должен быть быстрым и лёгким;
|
||
LLM-вызов медленный (секунды на письмо).
|
||
- Классификация идёт после архивации, отдельным проходом.
|
||
- Легко запускать вручную, менять модель/промпт, добавлять теги.
|
||
Скрипт читает email.md, отдаёт текст Qwen3:8b, получает JSON, пишет в frontmatter.
|
||
|
||
### D2: Формат классификации — отдельное поле в frontmatter
|
||
В `email.md` frontmatter добавляем:
|
||
```yaml
|
||
classification: task,meeting # или info / urgent / task / meeting / unclassified
|
||
classification_reason: "Просят подготовить бюджет и назначить встречу"
|
||
```
|
||
- `classification` — основной тег; `classification_reason` — обоснование.
|
||
- Трекинг обработанных: письмо «обработано», если есть `classification`.
|
||
Для надёжности дополнительно пишем в SQLite (`mail_index.db`) или state JSON
|
||
(когда обработан) — но минимум: поле в frontmatter достаточно.
|
||
|
||
### D3: Обработчики — отдельный скрипт `email_handlers.py`
|
||
Скрипт, который:
|
||
1. Находит письма с тегом, для которых ещё не выполнен обработчик.
|
||
2. По тегу вызывает соответствующий обработчик:
|
||
- `urgent` → Telegram
|
||
- `task` → Vikunja API
|
||
- `meeting` → Radicale (создать VEVENT)
|
||
- `info` → ничего
|
||
3. Помечает обработанное письмо (поле `handled_urgent: true` / `handled_task: true`
|
||
/ `handled_meeting: true`), чтобы не дублировать.
|
||
|
||
### D4: Куда слать встречу — Radicale (календарь Рабочий)
|
||
Событие встречи создаётся в Radicale (cal.nixg.ru), календарь «Рабочий»
|
||
(рабочие встречи). Формат VEVENT с SUMMARY=тема письма, DTSTART из классификации
|
||
(если дата/время указаны) или на ближайший рабочий день 11:00 (по умолчанию).
|
||
Это согласуется с тем, что пользователь уже использует Radicale для календаря
|
||
(и я добавил туда «Обход в Глории»). Дата парсится LLM (в classification_reason
|
||
модель возвращает JSON с датой/временем/продолжительностью).
|
||
|
||
### D5: Куда слать задачу — Radicale (VTODO, календарь «Задачи»)
|
||
Задача создаётся в Radicale как VTODO (CalDAV) в календаре «Задачи»
|
||
(`https://cal.nixg.ru/estorozhenko/Задачи/`), а не в Vikunja. Vikunja выведена
|
||
из проекта (см. чейндж `remove-vikunja-use-radicale-tasks`). SUMMARY=тема письма,
|
||
DESCRIPTION=ссылка на email.md, при наличии даты — DTSTART/DUE.
|
||
|
||
### D6: Уведомления — Telegram через Hermes gateway
|
||
`urgent` шлёт уведомление в Telegram. Используем Hermes gateway (или прямое
|
||
сообщение через API бота). Настройки (chat_id, token) в `.env` / config.
|
||
|
||
### D7: Идемпотентность и трекинг
|
||
- Классификатор: письмо пропускается, если в frontmatter есть `classification`.
|
||
- Обработчики: письмо пропускается, если для его тега уже стоит `handled_*`.
|
||
- Это гарантирует: повторный запуск cron не создаст дубликатов.
|
||
|
||
### D8: Хранение секретов
|
||
- Vikunja API token, Telegram chat_id/token — в `.env` (рядом с compose) или
|
||
в конфиге Hermes. Не хардкодить.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Vikunja нет токена** → снято: Vikunja выведена, обработчик `task` пишет
|
||
VTODO в Radicale. Нужен только Basic-auth Radicale (есть).
|
||
- **Качество классификации Qwen3:8b** → возможны ложные срабатывания
|
||
(письмо помечено `task`, хотя задачи нет). Митигирует: `classification_reason`
|
||
виден пользователю, легко править вручную; теги — не жёсткие.
|
||
- **Письмо с несколькими сущностями** (задача И встреча) → классификация может
|
||
вернуть комбинацию `task,meeting`; обработчики запускаются для каждого тега.
|
||
- **Дата встречи в свободном тексте** → LLM может ошибиться. Митигирует:
|
||
если дата не уверенна, ставим ближайший рабочий день 11:00 + reason «дата не
|
||
найдена точно».
|
||
- **Производительность** → Qwen3:8b на CPU медленный; классификатор должен быть
|
||
дозированным (`--limit N`, как contacts_extractor `--limit 15`).
|
||
- **Обработка старых писем** → не делаем в этом чейндже; при желании отдельный
|
||
проход `--force` по архивным.
|