Add digest command to email processor

Read-only summary of recent decisions, grouped by action with
[auto]/[user] markers. Supports --recent N for multi-day lookback.
This commit is contained in:
Yanxin Lu
2026-03-13 11:17:43 -07:00
parent 3c54098b1d
commit 36143fcd93
5 changed files with 97 additions and 3 deletions

View File

@@ -89,6 +89,8 @@ agent-browser close
./email-processor.sh review accept # 接受所有建议 ./email-processor.sh review accept # 接受所有建议
./email-processor.sh review 1 delete # 处理第1封删除 ./email-processor.sh review 1 delete # 处理第1封删除
./email-processor.sh stats # 查看统计 ./email-processor.sh stats # 查看统计
./email-processor.sh digest # 今日处理摘要
./email-processor.sh digest --recent 3 # 最近3天摘要
``` ```
**置信度机制**: **置信度机制**:

View File

@@ -60,6 +60,7 @@ The system separates **classification** (what the LLM does) from **confidence**
- **Repeat senders** with consistent tag signatures reach 85%+ confidence and get auto-acted during `scan`. They never touch the pending queue. - **Repeat senders** with consistent tag signatures reach 85%+ confidence and get auto-acted during `scan`. They never touch the pending queue.
- **New or ambiguous senders** start at 50% and get queued. - **New or ambiguous senders** start at 50% and get queued.
- **You occasionally run `review list`** to handle stragglers — each decision further builds history. - **You occasionally run `review list`** to handle stragglers — each decision further builds history.
- **`digest` gives a quick glance** at what was processed recently — subject lines grouped by action, with `[auto]`/`[user]` markers.
- **`stats` shows your automation rate** climbing over time. - **`stats` shows your automation rate** climbing over time.
### Confidence Computation ### Confidence Computation
@@ -96,6 +97,10 @@ chmod +x email-processor.sh
./email-processor.sh review all delete # delete all pending ./email-processor.sh review all delete # delete all pending
./email-processor.sh review accept # accept all suggestions ./email-processor.sh review accept # accept all suggestions
# --- Digest ---
./email-processor.sh digest # today's processed emails
./email-processor.sh digest --recent 3 # last 3 days
# --- Other --- # --- Other ---
./email-processor.sh stats # show decision history ./email-processor.sh stats # show decision history
``` ```
@@ -244,13 +249,16 @@ ollama list # should show kamekichi128/qwen3-4b-instruct-2507:latest
# 7. Check that the decision was recorded # 7. Check that the decision was recorded
./email-processor.sh stats ./email-processor.sh stats
# 8. Quick glance at what was processed today
./email-processor.sh digest
``` ```
## File Structure ## File Structure
``` ```
email_processor/ email_processor/
main.py # Entry point — scan/review/stats subcommands main.py # Entry point — scan/review/stats/digest subcommands
classifier.py # LLM prompt builder + response parser, tag taxonomy classifier.py # LLM prompt builder + response parser, tag taxonomy
decision_store.py # Decision history, confidence computation, few-shot retrieval decision_store.py # Decision history, confidence computation, few-shot retrieval
config.json # Ollama + automation settings config.json # Ollama + automation settings

View File

@@ -14,7 +14,7 @@ auto decision.
import json import json
import re import re
from datetime import datetime from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from collections import Counter from collections import Counter
@@ -200,6 +200,33 @@ def get_known_labels():
return labels return labels
def get_recent_decisions(days=1):
"""Return recent decisions grouped by action.
Args:
days: number of days to look back (default 1 = today).
Returns:
dict of action -> list of entries, e.g. {"delete": [...], "archive": [...]}.
Returns empty dict if no decisions found in the period.
"""
history = _load_history()
if not history:
return {}
cutoff = datetime.now() - timedelta(days=days)
grouped = {}
for entry in history:
try:
ts = datetime.fromisoformat(entry["timestamp"])
except (KeyError, ValueError):
continue
if ts >= cutoff:
action = entry.get("action", "unknown")
grouped.setdefault(action, []).append(entry)
return grouped
def get_all_stats(): def get_all_stats():
"""Compute aggregate statistics across the full decision history. """Compute aggregate statistics across the full decision history.

View File

@@ -11,6 +11,8 @@
# ./email-processor.sh review all delete # act on all pending # ./email-processor.sh review all delete # act on all pending
# ./email-processor.sh review accept # accept all suggestions # ./email-processor.sh review accept # accept all suggestions
# ./email-processor.sh stats # show history stats # ./email-processor.sh stats # show history stats
# ./email-processor.sh digest # today's processed emails
# ./email-processor.sh digest --recent 3 # last 3 days
# #
# Requires: uv, himalaya, Ollama running with model. # Requires: uv, himalaya, Ollama running with model.

View File

@@ -19,6 +19,8 @@ Subcommands:
python main.py review all <action> # act on all pending python main.py review all <action> # act on all pending
python main.py review accept # accept all suggestions python main.py review accept # accept all suggestions
python main.py stats # show decision history python main.py stats # show decision history
python main.py digest # today's processed emails
python main.py digest --recent 3 # last 3 days
Action mapping (what each classification does to the email): Action mapping (what each classification does to the email):
delete -> himalaya message delete <id> (moves to Trash) delete -> himalaya message delete <id> (moves to Trash)
@@ -633,6 +635,56 @@ def cmd_stats():
print(f"\nKnown labels: {', '.join(sorted(labels))}") print(f"\nKnown labels: {', '.join(sorted(labels))}")
# ---------------------------------------------------------------------------
# Subcommand: digest
# ---------------------------------------------------------------------------
def cmd_digest(days=1):
"""Print a compact summary of recently processed emails, grouped by action.
Shows both auto and user decisions with a marker to distinguish them.
"""
grouped = decision_store.get_recent_decisions(days)
if not grouped:
period = "today" if days == 1 else f"last {days} days"
print(f"No processed emails in this period ({period}).")
return
period_label = "today" if days == 1 else f"last {days} days"
print(f"Email digest ({period_label})")
print("=" * 40)
# Map action names to display labels
action_labels = {
"delete": "Deleted",
"archive": "Archived",
"keep": "Kept",
"mark_read": "Marked read",
}
total = 0
auto_count = 0
user_count = 0
for action, entries in sorted(grouped.items()):
label = action_labels.get(action, action.replace("label:", "Labeled ").title())
print(f"\n{label} ({len(entries)}):")
for entry in entries:
source = entry.get("source", "?")
sender = entry.get("sender", "unknown")
subject = entry.get("subject", "(no subject)")
print(f" [{source}] {sender}")
print(f" {subject}")
total += 1
if source == "auto":
auto_count += 1
else:
user_count += 1
print(f"\nTotal: {total} emails processed ({auto_count} auto, {user_count} user)")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Entry point & argument parsing # Entry point & argument parsing
# #
@@ -695,7 +747,10 @@ if __name__ == "__main__":
elif subcommand == "stats": elif subcommand == "stats":
cmd_stats() cmd_stats()
elif subcommand == "digest":
cmd_digest(days=recent if recent else 1)
else: else:
print(f"Unknown subcommand: {subcommand}") print(f"Unknown subcommand: {subcommand}")
print("Usage: python main.py [scan|review|stats] [--recent N] [--dry-run]") print("Usage: python main.py [scan|review|stats|digest] [--recent N] [--dry-run]")
sys.exit(1) sys.exit(1)