Files
email-assistant/openspec/changes/email-classification-handlers/specs/email-classification/spec.md
T
hermes 7550aff102 feat: классификация писем Qwen3:8b + обработчики (urgent→TG, task→VTODO, meeting→VEVENT)
- mail_archive.py: фикс вложений himalaya --dir → --downloads-dir; attachments/ только при has_attachment; идемпотентно
- email_classifier.py: Qwen3:8b (Ollama) → теги info/urgent/task/meeting в frontmatter email.md
- email_handlers.py: urgent→Telegram (Bot API+SOCKS5), task→Radicale VTODO, meeting→Radicale VEVENT; handled_* идемпотентность; --dry-run
- .gitignore: игнор *.env.bak*
- openspec: чейндж email-classification-handlers (в работе)
- STATUS/TODO/WALKTHROUGH: прогресс сессии, подводные камни Radicale (http.client, Depth:1, формат дат)
2026-09-13 20:35:58 +00:00

3.8 KiB

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, а проход продолжается