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:
2
TOOLS.md
2
TOOLS.md
@@ -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天摘要
|
||||||
```
|
```
|
||||||
|
|
||||||
**置信度机制**:
|
**置信度机制**:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user