Files
email-assistant/STATUS.md
T

216 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Email Assistant — локальный архив и ассистент почты
**Дата:** 2026-07-19
**Фаза:** 1.5 — Индексация, поиск, дайджесты + Адресная книга (в работе)
**Стек:** Himalaya CLI → Python → SQLite → Ollama (Qwen3:8b) → Yandex Disk
---
## Архитектура проекта
```
/opt/hermes/email-assistant/
├── STATUS.md # этот файл
├── config/
│ ├── himalaya-config.toml # Himalaya IMAP-конфиг
│ └── contacts-cron.sh # обёртка для cron контактов
├── scripts/
│ ├── mail_archive.py # инкрементальный архиватор писем с IMAP
│ ├── mail-archive.sh # shell-обёртка для systemd/cron
│ ├── migrate_to_email_md.py # конвертер meta.json→email.md (deprecated)
│ ├── mail_index.py # SQLite FTS5-индекс всех писем
│ ├── contacts_extractor.py # извлечение контактов через LLM
│ ├── sqlite_search.py # FTS5-поиск по архиву
│ ├── digest.py # еженедельный дайджест почты
│ └── mail_archive.py # основной архиватор
│
├── context/
│ ├── CONTEXT.md # архитектура и план развития
│ ├── SKILL.md # навык для Hermes (email-local-archive)
│ └── CONTACTS.md # описание contacts extractor
/opt/hermes/email/ # архив писем (локальный диск)
├── INBOX/
│ └── YYYY/MM/UID/email.md # YAML-frontmatter + тело
├── Sent/
├── Отправленные/
├── Archive/
├── state/ # mail-archive-last-*.json (last_uid per folder)
├── contacts/ # адресная книга
│ ├── contacts.json # полная база контактов
│ ├── index.json # email → contact_id
│ ├── contacts.vcf # vCard 4.0 для импорта
│ └── last_scan.json # трекинг обработанных
├── digests/ # еженедельные дайджесты
│ └── digest-YYYY-MM-DD.md
└── mail_index.db # SQLite + FTS5 (~5.4 MB)
Hermes cron:
- mail-archive-every-5min (no-agent, скрипт)
- contacts-extractor-every-30m (скрипт, --limit 15)
- digest: пока не поставлен
- mail_index --incremental: пока не поставлен
```
---
## Статус задач
### Фаза 0.5: Рефакторинг формата хранения ✅
- [x] Перейти с meta.json + body.md на один `email.md` с YAML-frontmatter
- [x] Полные заголовки в frontmatter (Message-ID, References, In-Reply-To, CC, Content-Type)
- [x] State файлы в `/opt/hermes/email/state/`
### Фаза 1: Локальный архив ✅
- [x] Структура `/opt/hermes/email-assistant/`
- [x] Himalaya (IMAP mail.corpoffice.tech:143 STARTTLS)
- [x] `mail_archive.py` — инкрементальный архиватор
- [x] Первый запуск: INBOX 585, Sent 515, Отправленные 510, Archive 373
- [x] systemd user timer + Hermes cron (every 5m)
### Фаза 1.5: Индексация, поиск и дайджесты ✅⬜
- [x] `mail_index.py` — SQLite-индекс всех email.md (FTS5 + трекинг контактов)
- [x] `sqlite_search.py` — CLI-поиск по FTS5 (поддержка фильтров from:/subject:/folder)
- [x] `digest.py` — еженедельный дайджест через LLM (Qwen3:8b)
- [ ] Поставить cron на `mail_index.py --incremental` (раз в 5-10 мин)
- [ ] Поставить cron на `digest.py` (раз в неделю)
### Фаза 1.7: Динамическое обнаружение всех подпапок INBOX ❌
- [ ] `mail_archive.py` — список вложенных папок INBOX захардкожен (18 шт.), но на сервере их **137** (включая многоуровневые: INBOX/!Персонал/ОТ и ТБ, INBOX/Бюджет/Винный город/CAPEX 2025, INBOX/Контрагенты/iiko/Тихая гавань и т.д.)
- [ ] `--all` сейчас использует тот же хардкод — не архивирует ~120 подпапок
- [ ] Требуется: динамическое обнаружение IMAP-папок через `himalaya folder list`, рекурсивный обход всех подпапок INBOX (любой глубины), автоматическая архивация новых подпапок при их создании
- [ ] `mail-archive-every-5min` cron должен обновлять список папок динамически, а не из хардкода
### Фаза 1.6: Адресная книга (Contacts Extractor) ✅⬜
- [x] `contacts_extractor.py` — извлечение контактов из подписей через LLM
- [x] clean_body — удаление цитируемой переписки (Outlook/forwards/>)
- [x] SQLite-трекинг обработанных писем (contacts_extracted / contacts_skipped)
- [x] Инкрементальное сохранение каждые 5 писем
- [x] `--limit N` для дозированной обработки
- [x] vCard 4.0 генерация
- [x] Cron already set: `contacts-extractor-every-30m` (--limit 15)
- [ ] Проверить качество извлечения: сейчас 5 контактов найдено, 5 skipped
- [ ] Доделать парсинг темы письма (некоторые темы содержат вшитые заголовки)
### Фаза 2: Векторизация и поиск ⬜
- [ ] Выбор векторизатора (bge-m3 через Ollama — уже есть в Memory OS)
- [ ] Индексация body в Qdrant
- [ ] Поиск по письмам через агента
### Фаза 3: Граф знаний ⬜
- [ ] Извлечение связанных сущностей (отправители, темы, проекты)
---
## Решения и проблемы скриптов
### `mail_archive.py` — Инкрементальный архиватор
**Задача:** Качать письма с IMAP, сохранять в `email.md` с YAML-frontmatter.
**Решение:**
- Для каждой папки хранится `last_uid` в `/opt/hermes/email/state/mail-archive-last-<folder>.json`
- Himalaya читает envelope (from, to, subject, date, message-id) → YAML frontmatter
- `himalaya envelope --page-size 500` для быстрой загрузки списка писем
- Каждое письмо: `himalaya get <uid> | email-to-md.py` → `email.md`
- Инкрементально: добавляет все uid > last_uid, обновляет last_uid
- Проблема: Himalaya v1.2.0 не поддерживает `danger_accept_invalid_certs` — используем `mail.corpoffice.tech` (валидный сертификат)
### `mail_index.py` — SQLite-индекс
**Задача:** Быстрый полнотекстовый поиск по архиву, трекинг обработки контактов.
**Решение:**
- SQLite с FTS5 (unicode61 tokenizer) — 4 таблицы: `emails`, `email_fts`, триггеры синхронизации
- Индексирует: path, uid, folder, date, from, to, subject, body_preview (первые 500 символов)
- Поля `contacts_extracted` / `contacts_skipped` для совместной работы с contacts_extractor
- Режимы: полная переиндексация (`--incremental` игнорирует mtime), поиск (`--search`), статистика (`--stats`)
- Инкрементальный режим: проверяет `file_mtime` — пропускает неизменённые файлы
### `contacts_extractor.py` — Извлечение контактов
**Задача:** Найти в подписи письма имя, должность, телефон, компанию отправителя.
**Решение:**
- Берёт необработанные письма из SQLite (WHERE contacts_extracted=0 AND contacts_skipped=0)
- `clean_body()`: удаляет HTML-теги, трекинг-ссылки, цитируемую переписку (Outlook-заголовки `От:`, `From:`, `Sent:`; forwarded; `>` quotes)
- Отдаёт очищенный текст Qwen3:8b (Ollama, temperature=0.1)
- LLM возвращает JSON: full_name, email, phone, position, company, address, raw_signature
- Дедупликация по email: при повторной встрече обновляет поля
- Инкрементальное сохранение: каждые 5 писем пишет contacts.json + contacts.vcf
- **Текущая проблема:** clean_body может вырезать подпись вместе с цитатами (см. ниже)
### `sqlite_search.py` — FTS5-поиск
**Задача:** Быстрый поиск по архиву писем из консоли.
**Решение:**
- FTS5-запрос к mail_index.db через SQL MATCH
- Поддержка синтаксиса: `"точная фраза"`, `OR`, `-исключение`, `префикс*`
- Пользовательские префиксы: `from:user@mail`, `subject:отчёт` → LIKE-фильтр в WHERE
- `--folder INBOX/!Отчеты` — фильтр по папке
- `--body` — включает тело письма в поиск (медленнее, но полнее)
- Вывод: дата, папка, отправитель, тема, полный путь к файлу
### `digest.py` — Еженедельный дайджест
**Задача:** Сгенерировать краткое резюме всех писем за N дней для руководителя.
**Решение:**
- SQLite-запрос: письма за последние N дней (по дате из frontmatter)
- Группировка по папкам (INBOX/!ВГ Чек листы → "ВГ Чек листы")
- Вызов Qwen3:8b с промптом: "напиши краткий дайджест на русском для руководителя"
- LLM выделяет: общую статистику, темы по папкам, важные отправители
- Сохраняет в `/opt/hermes/email/digests/digest-YYYY-MM-DD.md`
- Режимы: `stdout`, `file`, `both`
---
## Проблема: clean_body вырезает подпись вместе с цитатой
**Корень:** В письмах с цепочкой ответов (Outlook forwarding) подпись отправителя часто находится **после** маркера `От: Стороженко... Отправлено:...`, но до конца цитаты. clean_body отрезает всё от первого найденного маркера, теряя подпись.
**Текущее решение (итерация):**
1. Ищем самый ранний маркер цитирования среди всех паттернов (не первый совпавший)
2. Fallback: если ни один маркер не сработал — ищем `От: / From: / Subject:` в последних 500 символах
**Что ещё можно сделать:**
- Двухпроходная очистка: сначала отрезать цепочки forward-заголовков, потом отделять подпись от тела
- Определять границу подписи по паттернам `С уважением,` / `Best regards,` / `—` — она ближе к концу
- Использовать LLM не только для извлечения, но и для нахождения подписи
---
## Текущие метрики
| Папка | Писем | Контакты извл. |
|-------|-------|-----------------|
| INBOX | 585 | — |
| INBOX подпапки (18 хардкодных) | ~180 | — |
| **Неархивируемые подпапки INBOX** | **~120 папок не синхронизируются** | **—** |
| Sent | 515 | — |
| Отправленные | 510 | — |
| Archive | 373 | — |
| **Всего** | **2073** | **5** |
Индекс: 2073 письма, 5.4 MB SQLite.
Контакты: 5 найдено (clean_body отрезает подпись в большинстве forwarded-писем).
---
## Cron-задачи (Hermes)
| ID | Имя | Расписание | Тип | Статус |
|----|-----|-----------|-----|--------|
| 22c5beb891cc | mail-archive-every-5min | every 5m | no-agent (скрипт) | ✅ |
| 8e181a988392 | contacts-extractor-every-30m | every 30m | скрипт (--limit 15) | ✅ |
| — | mail-index-incremental | not set | — | ❌ |
| — | digest-weekly | not set | — | ❌ |
---
## Конфигурация
- **Himalaya:** `~/.config/himalaya/config.toml`
- **Аккаунт:** `vinogorod`, IMAP `mail.corpoffice.tech:143` (STARTTLS)
- **Почта:** `e.storozhenko@vinogorod.ru`
- **LLM:** Qwen3:8b (Ollama localhost:11434)
- **SMTP:** не настроен
- **Файлы state:** `/opt/hermes/email/state/`