Files

8.1 KiB
Raw Permalink Blame History

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

Скрипт, который:

  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 по архивным.