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