Initial commit: Email Assistant project

This commit is contained in:
2026-07-19 16:19:14 +00:00
commit 004977b1f8
16 changed files with 2653 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
# Contacts Extractor — извлечение адресной книги из писем
## Описание
Автоматическое извлечение контактных данных отправителей из входящих писем. Использует локальную LLM (Qwen3:8b через Ollama) для парсинга подписей — данные, которые регуляркой не вытянуть: должности, отделы, компании, несколько телефонов.
## Пайплайн
```
mail_archive.py ──→ INBOX/UID/email.md (тупой насос, без LLM)
↓
contacts_extractor.py (LLM через delegate_task)
↓
/mnt/yandex-disk/hermes/email/contacts/
├── contacts.vcf ← импорт в Thunderbird/Outlook/Apple Mail
├── contacts.json ← машинная база (дедупликация)
├── index.json ← uid → contact_id (для быстрых ответов)
└── last_scan.json ← трекинг: какие письма уже обработаны
```
## Трекинг обработанных писем
Хранится в `contacts/last_scan.json`:
```json
{
"last_processed": "2026-07-16T18:00:00",
"processed_uids": {
"INBOX": 5787,
"INBOX/!Протоколы": 0,
"Archive": 22
},
"processed_emails": [
"ivanov@example.com",
"petrov@example.com"
]
}
```
**Логика работы:**
1. При запуске читает `last_scan.json`
2. Сканирует файловую структуру `INBOX/YYYY/MM/UID/email.md` (только входящие — не `Sent`)
3. Для каждого `email.md` проверяет:
- Если UID ≤ last_uid по папке → уже обработано этим запуском
- Если email отправителя уже есть в `contacts.json` → пропустить (или обновить, если прошло >30 дней)
- Если файл `email.md` новее даты `last_processed` → обработать
4. После обработки обновляет `last_scan.json`
**Почему не по UID только:** письма могут добавиться задним числом (например, после переархивации). Поэтому лучше комбинировать: last_uid + дата модификации `email.md`.
## Промпт для LLM
### Цель промпта
Передаётся full text письма (YAML frontmatter отбрасывается, остаётся body). LLM должна:
1. Найти подпись в конце письма
2. Извлечь из неё контактные данные
3. Вернуть строгий JSON
### Вариант промпта (черновик)
```
Ты — экстрактор контактных данных из писем. Твоя задача — найти подпись
отправителя в конце письма и извлечь структурированные данные.
Правила поиска подписи:
- Подпись обычно отделена от тела письма разделителями: "-- \n", "---\n",
"С уважением,", "С наилучшими пожеланиями,", "Best regards,",
"Kind regards,", "С ув.,", "————"—"
- Если разделителя нет — последние 5-15 строк письма это подпись
- Не путай подпись с цитируемым текстом переписки (обычно начинается с ">"
или "On ... wrote:" или "————— Forwarded message —————")
- Ignore boilerplate (disclaimers, confidentiality notices)
Извлеки из подписи:
1. full_name — полное имя (ФИО)
2. email — email адрес (если есть в подписи, иначе null)
3. phone — основной телефон (в международном или местном формате)
4. phone_secondary — дополнительный телефон (если есть)
5. position — должность
6. company — название компании/организации
7. department — отдел (если указан)
8. address — почтовый/юридический адрес (если есть)
9. raw_signature — полный текст найденной подписи (для отладки)
Если никакой подписи не найдено — верни только full_name (из from) и
email, остальные поля null.
Верни ТОЛЬКО JSON, без пояснений:
{"full_name": "...", "email": "...", "phone": null, ...}
Вот текст письма:
[body]
```
### Обсуждение промпта (решения)
| Вопрос | Решение |
|--------|---------|
| Передаём body целиком или последние 50 строк? | **Целиком** — чтобы Qwen видела контекст и не путала подпись с цитатой |
| Поле department нужно? | **Нет** — достаточно company + position |
| vCard версия? | **4.0** — поддержка соцсетей, фото, расширенных полей |
| Отправленные/Sent обрабатываем? | **Только входящие** — INBOX + вложенные папки |
### Архитектура хранилища
```
/opt/hermes/email/ # ← локально (быстрый диск)
├── INBOX/
│ └── ...email.md
├── state/
│ └── mail-archive-last-*.json # last_uid per folder
└── contacts/ # адресная книга
├── contacts.vcf # vCard 4.0 для импорта
├── contacts.json # машинный формат
├── index.json # email → contact_id
└── last_scan.json # трекинг обработанных писем
```
## Трекинг обработанных писем
### Формат vCard
Для импорта в почтовые клиенты (vCard 4.0, RFC 6350):
```
BEGIN:VCARD
VERSION:4.0
FN:Иванов Иван Иванович
N:Иванов;Иван;Иванович;;;
EMAIL;TYPE=WORK:ivan@example.com
TEL;TYPE=WORK:+7-123-456-78-90
TITLE:Генеральный директор
ORG:ООО "Ромашка"
ADR;TYPE=WORK:;;ул. Ленина, д.1;Москва;;123456;Россия
NOTE:Извлечено из письма от 2026-07-16 (UID 11559, INBOX)
END:VCARD
```
### Структура contacts.json
Для дедупликации и машинной обработки:
```json
{
"version": 1,
"contacts": [
{
"id": "md5_of_email",
"email": "ivan@example.com",
"full_name": "Иванов Иван Иванович",
"phone": "+7-123-456-78-90",
"phone_secondary": null,
"position": "Генеральный директор",
"company": "ООО \"Ромашка\"",
"department": null,
"address": null,
"first_seen": "2026-07-16",
"last_seen": "2026-07-16",
"source_uids": ["INBOX/2026/07/11559"],
"source_folders": ["INBOX"]
}
],
"by_email": {
"ivan@example.com": 0
}
}
```
- `by_email` — индекс для O(1) дедупликации
- `source_uids` — массив, чтобы можно было посмотреть, из каких писем извлечён контакт
- `first_seen`/`last_seen` — для понимания актуальности
### Дедупликация
1. **email — primary key.** Если контакт с таким email уже есть → обновить поля (телефон мог поменяться, должность — повысили)
2. Если email нет, но совпадает full_name (fuzzy) — всё равно создать новый, но зафиксировать в `note`
3. Если нет ни email, ни full_name (редко) — не сохранять
### Интеграция с будущим профилем Hermes
Когда появится отдельный профиль с SOUL.md:
- SOUL.md будет задавать personality и язык
- Contacts extractor переедет туда как skill
- Промпт может расшириться: «вытащи не только подпись, но и все упомянутые в письме контакты»
## План реализации
1. Создать `scripts/contacts_extractor.py`
2. Логика: сканирование `email.md` → дедупликация по `contacts.json` → вызов Qwen через субпроцесс (curl Ollama API) → запись
3. Промпт — hardcoded в скрипте (потом можно будет вынести в config/prompts/)
4. Systemd timer: mail-contacts.service + mail-contacts.timer (через час после архивации)
5. Создать Hermes-скилл `email-contacts`
+68
View File
@@ -0,0 +1,68 @@
# Email Assistant — Context & Architecture
## Vision
Полноценный email-ассистент на локальных данных:
1. **Архив** — вся почта в файлах (Фаза 1)
2. **Векторизация** — поиск по смыслу через bge-m3 + Qdrant (Фаза 2)
3. **Граф знаний** — связи между письмами, проектами, людьми (Фаза 3)
4. **Интеллект** — авто-извлечение фактов, задач, рекомендации (Фаза 4)
## Почему файлы, а не БД?
- Версионность и бэкапы (Yandex Disk)
- Прозрачность — можно grep, jq, find без SQL
- Совместимость с Obsidian и другими инструментами
- Поэтапная миграция в Qdrant без потери данных
## Связь с существующей инфраструктурой
### Memory OS
- Уже есть: Qdrant (1024d COSINE + sparse), bge-m3 (Ollama)
- Можно повторно использовать для индексации писем
- Icarus threshold = 0.40
### Yandex Disk
- Путь: `/mnt/yandex-disk/`
- Монтирован как WebDAV/FUSE — работает как локальная ФС
- Файлы доступны из Obsidian и других приложений
## Структура проекта
```
/opt/hermes/email-assistant/
├── STATUS.md # Статус и план работ
├── context/
│ ├── CONTEXT.md # Этот файл — архитектура
│ └── SKILL.md # Навык для Hermes
├── scripts/
│ ├── mail_archive.py # Python-архиватор
│ └── mail-archive.sh # Shell-обёртка
└── data/ # Временные данные/эксперименты
```
## Pipeline
```
IMAP (mail.vinogorod.ru:143)
↓ Himalaya CLI
mail_archive.py
↓ инкрементальная запись
/mnt/yandex-disk/hermes/email/<folder>/YYYY/MM/UID/
├── meta.json
├── body.md
└── attachments/
↓ (Фаза 2)
bge-m3 (Ollama) → эмбеддинги
↓ (Фаза 2)
Qdrant (1024d, COSINE)
↓ (Фаза 3)
Граф: люди, проекты, организации, ключевые даты
↓ (Фаза 4)
Извлечение фактов, формирование задач, рекомендации
```
## Известные ограничения
1. Himalaya v1.2.0 — page-size 2000, нет пагинации по дате (только page/page-size)
2. Нет `danger_accept_invalid_certs` в конфиге — используем `mail.corpoffice.tech` (валидный сертификат)
3. SMTP не настроен — только чтение
4. Пароль в raw-виде в конфиге (потом в keyring)
+119
View File
@@ -0,0 +1,119 @@
---
name: email-local-archive
description: "Локальный архив почты: инкрементальное сохранение писем из IMAP в файловую структуру на Yandex Disk с метаданными, телом и вложениями."
version: 1.0.0
author: estorozhenko
platforms: [linux]
prerequisites:
commands: [himalaya, python3]
metadata:
hermes:
tags: [email, archive, imap, backup, knowledge]
---
# Email Local Archive
Этот навык позволяет агенту архивировать письма из почтового ящика
в локальную файловую систему по пути:
```
/mnt/yandex-disk/hermes/email/<folder>/YYYY/MM/UID/
├── meta.json # envelope (from, to, subject, date, flags)
├── body.md # тело письма (plain text)
└── attachments/ # вложения
```
Используется клиент Himalaya CLI и Python-скрипт
`/opt/hermes/email-assistant/scripts/mail_archive.py`.
## Возможности
- Инкрементальная архивация новых писем по UID
- Поддержка любых папок (INBOX, Отправленные, Archive, Sent, подпапки)
- Ограничение количества писем за один запуск (`--limit`)
- Режим `--all` для архивации всех папок (основные + вложенные INBOX)
- Состояние отслеживается в `~/.local/state/mail-archive-last-*.json`
- Обёртка `mail-archive.sh` для systemd/cron/Hermes
## Инструменты
Используется `terminal` для вызова скрипта, `read_file` для чтения
сохранённых писем, `search_files` для поиска по архиву.
### Основные команды
```bash
# Архивация INBOX (первые 200 писем)
mail-archive.sh --folder INBOX --limit 200
# Архивация Отправленные (первые 50)
mail-archive.sh --folder "Отправленные" --limit 50
# Архивация всех папок (основные)
mail-archive.sh --limit 100
# Архивация всех папок включая вложенные INBOX
mail-archive.sh --all --limit 50
# Справка
mail-archive.sh --help
```
## Поведение агента
1. **"Обнови архив почты"** — выполнить `mail-archive.sh --limit 100`
2. **"Архивируй INBOX"** — `mail-archive.sh --folder INBOX --limit 200`
3. **"Архивируй всё"** — `mail-archive.sh --all --limit 100`
4. **"Найди письмо про <тема>"** — `grep -ril '<тема>' /mnt/yandex-disk/hermes/email/**/body.md`
или `jq 'select(.subject | test("<тема>"))' /mnt/yandex-disk/hermes/email/**/meta.json`
5. **"Проверь статус архива"** — показать содержимое `~/.local/state/mail-archive-last-*.json`
6. **"Сколько писем в архиве"** — `find /mnt/yandex-disk/hermes/email -name meta.json | wc -l`
## Правила
- НЕ изменять и не удалять файлы в `/mnt/yandex-disk/hermes/email/`
или `~/.local/state/mail-archive-last-*.json` без явной команды
- НЕ переписывать логику архивации сырыми командами Himalaya
- Если скрипт падает — сообщить об ошибке пользователю
- Пароль в `backend.auth.raw` в конфиге — не показывать в логах
## Примеры
**Пользователь:** "Обнови локальный архив почты из INBOX"
**Агент (команда):**
```bash
mail-archive.sh --folder INBOX --limit 200
```
**Агент (сообщает результат):**
```
📁 INBOX (last_uid=11559)
✓ UID 11560 (Новое письмо от Иванова)
✓ UID 11561 (Счёт на оплату)
→ last_uid обновлён до 11561, обработано 2
```
---
**Пользователь:** "Архивируй все папки полностью"
**Агент (команда):**
```bash
mail-archive.sh --all --limit 500
```
---
**Пользователь:** "Найди письмо про договор с Торик"
**Агент (поиск):**
```bash
grep -ril 'договор.*торик\|торик.*договор' /mnt/yandex-disk/hermes/email/**/body.md 2>/dev/null
```
**Агент (показывает результат):**
```
/mnt/yandex-disk/hermes/email/INBOX/2025/06/10234/body.md
/mnt/yandex-disk/hermes/email/INBOX/!Торик/2026/03/11050/body.md
```