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