9.2 KiB
9.2 KiB
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:
{
"last_processed": "2026-07-16T18:00:00",
"processed_uids": {
"INBOX": 5787,
"INBOX/!Протоколы": 0,
"Archive": 22
},
"processed_emails": [
"ivanov@example.com",
"petrov@example.com"
]
}
Логика работы:
- При запуске читает
last_scan.json - Сканирует файловую структуру
INBOX/YYYY/MM/UID/email.md(только входящие — неSent) - Для каждого
email.mdпроверяет:- Если UID ≤ last_uid по папке → уже обработано этим запуском
- Если email отправителя уже есть в
contacts.json→ пропустить (или обновить, если прошло >30 дней) - Если файл
email.mdновее датыlast_processed→ обработать
- После обработки обновляет
last_scan.json
Почему не по UID только: письма могут добавиться задним числом (например, после переархивации). Поэтому лучше комбинировать: last_uid + дата модификации email.md.
Промпт для LLM
Цель промпта
Передаётся full text письма (YAML frontmatter отбрасывается, остаётся body). LLM должна:
- Найти подпись в конце письма
- Извлечь из неё контактные данные
- Вернуть строгий 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
Для дедупликации и машинной обработки:
{
"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— для понимания актуальности
Дедупликация
- email — primary key. Если контакт с таким email уже есть → обновить поля (телефон мог поменяться, должность — повысили)
- Если email нет, но совпадает full_name (fuzzy) — всё равно создать новый, но зафиксировать в
note - Если нет ни email, ни full_name (редко) — не сохранять
Интеграция с будущим профилем Hermes
Когда появится отдельный профиль с SOUL.md:
- SOUL.md будет задавать personality и язык
- Contacts extractor переедет туда как skill
- Промпт может расшириться: «вытащи не только подпись, но и все упомянутые в письме контакты»
План реализации
- Создать
scripts/contacts_extractor.py - Логика: сканирование
email.md→ дедупликация поcontacts.json→ вызов Qwen через субпроцесс (curl Ollama API) → запись - Промпт — hardcoded в скрипте (потом можно будет вынести в config/prompts/)
- Systemd timer: mail-contacts.service + mail-contacts.timer (через час после архивации)
- Создать Hermes-скилл
email-contacts