Files
email-assistant/openspec/changes/archive/2026-09-14-email-classification-handlers/design.md
T

112 lines
8.1 KiB
Markdown
Raw 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.
# 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` по архивным.