mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 09:15:09 +00:00
Initial commit: Email Assistant project
This commit is contained in:
@@ -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`
|
||||
Reference in New Issue
Block a user