8.1 KiB
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 добавляем:
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
Скрипт, который:
- Находит письма с тегом, для которых ещё не выполнен обработчик.
- По тегу вызывает соответствующий обработчик:
urgent→ Telegramtask→ Vikunja APImeeting→ Radicale (создать VEVENT)info→ ничего
- Помечает обработанное письмо (поле
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по архивным.