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:
@@ -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.
|
||||
- **New or ambiguous senders** start at 50% and get queued.
|
||||
- **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.
|
||||
|
||||
### Confidence Computation
|
||||
@@ -96,6 +97,10 @@ chmod +x email-processor.sh
|
||||
./email-processor.sh review all delete # delete all pending
|
||||
./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 ---
|
||||
./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
|
||||
./email-processor.sh stats
|
||||
|
||||
# 8. Quick glance at what was processed today
|
||||
./email-processor.sh digest
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
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
|
||||
decision_store.py # Decision history, confidence computation, few-shot retrieval
|
||||
config.json # Ollama + automation settings
|
||||
|
||||
@@ -14,7 +14,7 @@ auto decision.
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
@@ -200,6 +200,33 @@ def get_known_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():
|
||||
"""Compute aggregate statistics across the full decision history.
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
# ./email-processor.sh review all delete # act on all pending
|
||||
# ./email-processor.sh review accept # accept all suggestions
|
||||
# ./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.
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ Subcommands:
|
||||
python main.py review all <action> # act on all pending
|
||||
python main.py review accept # accept all suggestions
|
||||
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):
|
||||
delete -> himalaya message delete <id> (moves to Trash)
|
||||
@@ -633,6 +635,56 @@ def cmd_stats():
|
||||
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
|
||||
#
|
||||
@@ -695,7 +747,10 @@ if __name__ == "__main__":
|
||||
elif subcommand == "stats":
|
||||
cmd_stats()
|
||||
|
||||
elif subcommand == "digest":
|
||||
cmd_digest(days=recent if recent else 1)
|
||||
|
||||
else:
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user