Files
email-assistant/context/CONTACTS.md
T

9.2 KiB
Raw Blame History

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"
  ]
}

Логика работы:

  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

Для дедупликации и машинной обработки:

{
  "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