mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 21:25:10 +00:00
Задача 8: классификация и обработчики — живой прогон, cron, archive change
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
## Purpose
|
||||
|
||||
Скачивание вложений письма в каталог этого письма. Сейчас `mail_archive.py`
|
||||
вызывает `himalaya attachment download --dir`, но правильный флаг в Himalaya —
|
||||
`--downloads-dir`, из-за чего команда падает (exit 2), ошибка молча глотается
|
||||
`except: pass`, и папка `attachments/` всегда пустая. Вложения теряются.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Вложения сохраняются в каталог письма
|
||||
|
||||
Для каждого письма с вложениями (флаг `has_attachment: true` в frontmatter)
|
||||
вложения MUST быть сохранены в подкаталог `attachments/` каталога письма
|
||||
(`/opt/hermes/email/<folder>/YYYY/MM/<uid>/attachments/`).
|
||||
|
||||
#### Scenario: Письмо с вложением архивировано
|
||||
- **WHEN** `mail_archive.py` заархивировал письмо с `has_attachment: true`
|
||||
- **THEN** файлы вложений лежат в `<msg_dir>/attachments/` и совпадают с вложениями на IMAP-сервере
|
||||
|
||||
### Requirement: Правильный флаг Himalaya
|
||||
|
||||
Скачивание вложений MUST использовать флаг `--downloads-dir` (а не несуществующий
|
||||
`--dir`) команды `himalaya attachment download`, и передавать ему каталог письма.
|
||||
|
||||
#### Scenario: Вызов himalaya с корректным флагом
|
||||
- **WHEN** `get_attachments()` выполняется для письма
|
||||
- **THEN** используется `himalaya attachment download --folder <folder> --downloads-dir <msg_dir>/attachments <uid>`, exit code 0 при успехе
|
||||
|
||||
### Requirement: Учёт отсутствия вложений
|
||||
|
||||
Если письмо не имеет вложений (`has_attachment: false` или команда вернула
|
||||
«нет вложений»), скрипт MUST NOT создавать пустую папку `attachments/` и MUST NOT
|
||||
считать это ошибкой.
|
||||
|
||||
#### Scenario: Письмо без вложений
|
||||
- **WHEN** `mail_archive.py` обрабатывает письмо без вложений
|
||||
- **THEN** каталог `attachments/` не создаётся, ошибка не логируется
|
||||
|
||||
### Requirement: Повторная обработка существующих писем
|
||||
|
||||
Повторный запуск `mail_archive.py` MUST NOT повторно качать уже сохранённые
|
||||
вложения (проверка по наличию каталога/файлов).
|
||||
|
||||
#### Scenario: Повторный запуск
|
||||
- **WHEN** `mail_archive.py` запущен повторно на письме с уже скачанными вложениями
|
||||
- **THEN** вложения не скачиваются повторно (идемпотентность)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
## Purpose
|
||||
|
||||
Классификация писем локальной LLM: после скачивания письма модель определяет тип
|
||||
письма (информационное, требует срочного ответа, содержит задачу, содержит
|
||||
встречу) и записывает тег + обоснование в frontmatter файла email.md. Обработка
|
||||
приватна — модель Qwen3:8b запущена локально через Ollama, текст письма не
|
||||
покидает хост.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Классификация каждого нового письма
|
||||
|
||||
Каждое письмо, заархивированное `mail_archive.py`, MUST быть классифицировано
|
||||
локальной моделью не позднее одного прохода классификатора после архивации.
|
||||
|
||||
#### Scenario: Новое письмо после архивации
|
||||
- **WHEN** `mail_archive.py` сохранил новое письмо в `/opt/hermes/email/**/email.md` без поля `classification`
|
||||
- **THEN** `email_classifier.py` обработает его и запишет в frontmatter поле `classification` с одним из значений: `info`, `urgent`, `task`, `meeting` (или комбинацию через запятую)
|
||||
|
||||
### Requirement: Приватность обработки
|
||||
|
||||
Классификация MUST выполняться локальной моделью (Qwen3:8b через Ollama на
|
||||
localhost:11434) и MUST NOT отправлять текст письма в облачные API.
|
||||
|
||||
#### Scenario: Локальная модель доступна
|
||||
- **WHEN** классификатор запущен
|
||||
- **THEN** запросы к LLM идут только на `http://localhost:11434` (Ollama), никаких внешних HTTP-вызовов с телом письма
|
||||
|
||||
### Requirement: Обоснование классификации
|
||||
|
||||
Классификатор MUST записывать краткое обоснование решения в frontmatter
|
||||
(поле `classification_reason`), чтобы пользователь видел, почему письмо помечено
|
||||
именно так.
|
||||
|
||||
#### Scenario: Обоснование для письма
|
||||
- **WHEN** `email_classifier.py` классифицировал письмо
|
||||
- **THEN** в frontmatter записано `classification_reason` с 1-2 предложениями на русском
|
||||
|
||||
### Requirement: Идемпотентность
|
||||
|
||||
Письмо MUST обрабатываться классификатором только один раз; повторный запуск
|
||||
MUST NOT переклассифицировать уже обработанные письма (если не задан флаг
|
||||
принудительной переклассификации).
|
||||
|
||||
#### Scenario: Повторный запуск классификатора
|
||||
- **WHEN** `email_classifier.py` запущен повторно на уже обработанном письме (есть `classification`)
|
||||
- **THEN** письмо пропускается без повторного вызова LLM
|
||||
|
||||
### Requirement: Обработка ошибок классификатора
|
||||
|
||||
Если LLM не ответила или вернула невалидный JSON, классификатор MUST пометить
|
||||
письмо как `unclassified` и продолжить со следующим письмом, не прерывая весь
|
||||
проход.
|
||||
|
||||
#### Scenario: LLM вернула невалидный ответ
|
||||
- **WHEN** модель не ответила или вернула не-JSON
|
||||
- **THEN** письмо получает `classification: unclassified`, а проход продолжается
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
## Purpose
|
||||
|
||||
Подключение обработчиков по тегам классификации письма: уведомление в мессенджер
|
||||
для срочных писем, создание задачи в Vikunja для писем с задачей, создание
|
||||
события в Radicale для писем со встречей. Обработчики запускаются автоматически
|
||||
после классификации и работают идемпотентно.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Уведомление в мессенджер для срочных писем
|
||||
|
||||
Письмо с тегом `urgent` MUST вызывать отправку уведомления в мессенджер
|
||||
(Telegram) с отправителем, темой и первыми строками текста.
|
||||
|
||||
#### Scenario: Срочное письмо
|
||||
- **WHEN** `email_classifier.py` пометил письмо тегом `urgent`
|
||||
- **THEN** `email_handlers.py` отправляет в Telegram уведомление с from/subject/превью
|
||||
|
||||
### Requirement: Создание задачи в Radicale (VTODO) для писем с задачей
|
||||
|
||||
Письмо с тегом `task` MUST создавать задачу в Radicale (CalDAV, календарь
|
||||
«Задачи») как VTODO с темой письма в SUMMARY и ссылкой на письмо в DESCRIPTION.
|
||||
|
||||
#### Scenario: Письмо с задачей
|
||||
- **WHEN** `email_classifier.py` пометил письмо тегом `task`
|
||||
- **THEN** в Radicale (календарь Задачи) создаётся VTODO: SUMMARY=тема письма, DESCRIPTION=ссылка на `email.md`
|
||||
|
||||
### Requirement: Создание события в Radicale для писем со встречей
|
||||
|
||||
Письмо с тегом `meeting` MUST создавать событие в календаре Radicale (Рабочий)
|
||||
с темой письма как SUMMARY и извлечённой датой/временем, если они указаны.
|
||||
|
||||
#### Scenario: Письмо со встречей
|
||||
- **WHEN** `email_classifier.py` пометил письмо тегом `meeting` и в классификации есть дата/время
|
||||
- **THEN** в Radicale (календарь Рабочий) создаётся VEVENT с SUMMARY=тема письма
|
||||
|
||||
### Requirement: Идемпотентность обработчиков
|
||||
|
||||
Обработчик MUST запускаться для каждого письма один раз; повторный запуск на
|
||||
уже обработанном письме MUST NOT создавать дубликат задачи/события/уведомления.
|
||||
|
||||
#### Scenario: Повторный запуск обработчиков
|
||||
- **WHEN** `email_handlers.py` запущен повторно на письме, для которого уже созданы задача/событие
|
||||
- **THEN** дубликаты не создаются (трекинг обработанных в state)
|
||||
|
||||
### Requirement: Информационные письма не создают обработчиков
|
||||
|
||||
Письмо с тегом `info` MUST NOT вызывать уведомления, задач или событий; оно
|
||||
только помечается тегом в frontmatter.
|
||||
|
||||
#### Scenario: Информационное письмо
|
||||
- **WHEN** `email_classifier.py` пометил письмо тегом `info`
|
||||
- **THEN** `email_handlers.py` не создаёт ни уведомления, ни задачи, ни события
|
||||
|
||||
### Requirement: Уведомление о недоступности обработчика
|
||||
|
||||
Если обработчик не может выполниться (Radicale недоступен, нет учётных данных),
|
||||
MUST быть записана ошибка в лог, и письмо MUST остаться помеченным тегом для
|
||||
повторной попытки (не теряться).
|
||||
|
||||
#### Scenario: Radicale недоступен
|
||||
- **WHEN** `email_handlers.py` пытается создать задачу/событие, но Radicale недоступен
|
||||
- **THEN** ошибка пишется в лог, письмо остаётся с тегом `task`/`meeting`, повторная попытка возможна
|
||||
Reference in New Issue
Block a user