Files

193 lines
9.2 KiB
Markdown
Raw Permalink 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.
# 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`