#!/usr/bin/env python3 """ digest.py — Еженедельный дайджест почты. Собирает все письма за 7 дней, группирует по папкам, вызывает LLM для генерации краткого дайджеста. Использование: python3 digest.py — дайджест за 7 дней python3 digest.py --days 14 — за 14 дней python3 digest.py --folder INBOX — только INBOX python3 digest.py --output — только файл, без вывода в stdout """ import json import sys import time import urllib.error import urllib.request from datetime import datetime, timedelta from pathlib import Path import sqlite3 # ─── Конфиг ─────────────────────────────────────────────────────────────────── DB_PATH = Path("/opt/hermes/email/mail_index.db") DIGEST_DIR = Path("/opt/hermes/email/digests") OLLAMA_URL = "http://localhost:11434/api/generate" OLLAMA_MODEL = "qwen3:8b" OLLAMA_TIMEOUT = 60 # ─── Промпты ────────────────────────────────────────────────────────────────── SUMMARIZE_PROMPT = """Ты — ассистент, который делает ежедневный дайджест корпоративной почты. Ниже — список писем за последние {days} дней, сгруппированный по папкам. Твоя задача — написать КРАТКИЙ дайджест на русском языке для руководителя. Формат: 1. Заголовок: "📬 Дайджест почты — {date_range}" 2. Общая статистика: сколько писем, сколько папок затронуто 3. По каждой папке — 1-3 предложения: основные темы, важные отправители, ключевые решения 4. Выдели особенно важные письма (от руководства, по тендерам, договорам, финансам) ВАЖНО: - Пиши ТОЛЬКО на русском - Будь краток — максимум 300 слов - Не перечисляй каждое письмо — выделяй тренды и главное - Если тема понятна из темы письма — группируй - Пропусти технический мусор (уведомления систем, автоответы) Вот данные для анализа: {folder_groups}""" # ─── Вспомогательные ────────────────────────────────────────────────────────── def get_db(): conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row return conn def call_llm(prompt, max_retries=2): """Вызвать Qwen через Ollama API.""" payload = json.dumps({ "model": OLLAMA_MODEL, "prompt": prompt, "stream": False, "options": { "temperature": 0.3, "num_predict": 2048, } }).encode("utf-8") for attempt in range(max_retries + 1): if attempt > 0: time.sleep(2) req = urllib.request.Request( OLLAMA_URL, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: resp = urllib.request.urlopen(req, timeout=OLLAMA_TIMEOUT) data = json.loads(resp.read().decode("utf-8")) return data.get("response", "").strip() except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e: if attempt < max_retries: continue return f"⚠ Ошибка LLM: {e}" return "⚠ Не удалось сгенерировать дайджест" def format_date(d): """Привести дату к dd.mm.yyyy.""" if not d: return "—" for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%d %H:%M:%S%z", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"): try: dt = datetime.strptime(d.rstrip("Z"), fmt) return dt.strftime("%d.%m.%Y") except ValueError: continue return d[:10] # ─── Основная логика ────────────────────────────────────────────────────────── def build_digest(days=7, folder_filter=None): """Собрать дайджест за N дней.""" conn = get_db() c = conn.cursor() cutoff = datetime.now() - timedelta(days=days) # Берём письма за период query = """ SELECT folder, date, from_addr, subject, contacts_extracted, path FROM emails WHERE date >= ? ORDER BY date DESC """ params = [cutoff.strftime("%Y-%m-%d")] if folder_filter: query += " AND folder = ?" params.append(folder_filter) rows = c.execute(query, params).fetchall() conn.close() if not rows: msg = f" Нет писем за последние {days} дней" print(msg) return msg # Группируем по папкам groups = {} for r in rows: f = r["folder"] if f not in groups: groups[f] = [] groups[f].append(r) # Формируем текстовое представление для LLM folder_parts = [] total = 0 for fname, emails in sorted(groups.items()): total += len(emails) display_name = fname.replace("INBOX/", "") if fname.startswith("INBOX") else fname items = [] for e in emails: date_str = format_date(e["date"]) extr = "✓" if e["contacts_extracted"] else " " from_short = e["from_addr"][:40] subj = (e["subject"] or "(без темы)")[:60] items.append(f" [{extr}] {date_str} | {from_short} | {subj}") folder_parts.append(f"─── {display_name} ({len(emails)} писем) ───\n" + "\n".join(items)) folder_text = "\n\n".join(folder_parts) # Дата-диапазон date_range = f"{format_date((datetime.now() - timedelta(days=days)).isoformat())} — {datetime.now().strftime('%d.%m.%Y')}" prompt = SUMMARIZE_PROMPT.format( days=days, date_range=date_range, folder_groups=folder_text ) print(" Генерирую дайджест через LLM...", flush=True) digest = call_llm(prompt) # Форматируем header = f"📬 Дайджест корпоративной почты" subheader = f" {total} писем из {len(groups)} папок | {date_range}" divider = "─" * 60 result = f"{header}\n{subheader}\n{divider}\n\n{digest}\n\n{divider}" # Сохраняем в файл DIGEST_DIR.mkdir(parents=True, exist_ok=True) filename = datetime.now().strftime("digest-%Y-%m-%d.md") filepath = DIGEST_DIR / filename filepath.write_text(result, encoding="utf-8") print(f"\n✅ Дайджест сохранён: {filepath}") print() return result def main(): import argparse parser = argparse.ArgumentParser(description="Еженедельный дайджест почты") parser.add_argument("--days", type=int, default=7, help="Глубина в днях") parser.add_argument("--folder", type=str, help="Фильтр по папке") parser.add_argument("--output", choices=["stdout", "file", "both"], default="both", help="Куда вывести результат") args = parser.parse_args() result = build_digest(days=args.days, folder_filter=args.folder) if args.output in ("stdout", "both"): print() print(result) if __name__ == "__main__": main()