email-assistant / src /email_workflow.py
Brettapps's picture
Email assistant static demo + Obsidian vault workflow
00a67f9 verified
Raw
History Blame Contribute Delete
6.67 kB
#!/usr/bin/env python3
"""
Email Assistant Agent Workforce for brett@brettapps.com
Agents:
1. Triage Agent β€” read inbox, classify, prioritise
2. Draft Agent β€” compose replies, store in Obsidian drafts
3. Summary Agent β€” thread summaries, action items
4. Archive Agent β€” label, move, snooze via Obsidian filing system
"""
import json, os, re, datetime, subprocess, sys
from pathlib import Path
OBSIDIAN_VAULT = Path(os.environ.get("OBSIDIAN_VAULT", "/tmp/email_vault"))
EMAIL_DIR = OBSIDIAN_VAULT / "Email"
for d in [EMAIL_DIR / "Inbox", EMAIL_DIR / "Drafts", EMAIL_DIR / "Sent",
EMAIL_DIR / "Archive", EMAIL_DIR / "Templates", EMAIL_DIR / "Action_Items"]:
d.mkdir(parents=True, exist_ok=True)
PRIORITY_RULES = [
(r"urgent|asap|immediately|critical", "P1-Critical"),
(r"invoice|payment|overdue|deadline", "P2-High"),
(r"meeting|call|schedule|calendar", "P2-High"),
(r" Brettapps|fair dinkum|trifecta|ebook|publish", "P2-High"),
(r"newsletter|unsubscribe|no.?reply", "P4-Low"),
(r"promo|sale|discount|limited.?time", "P5-Spam"),
]
def triage_email(subject: str, body: str, sender: str) -> dict:
text = f"{subject} {body} {sender}".lower()
priority = "P3-Normal"
category = "General"
for pattern, level in PRIORITY_RULES:
if re.search(pattern, text, re.IGNORECASE):
priority = level
break
if "hf." in text or "huggingface" in text or "space" in text:
category = "HF Spaces"
elif "invoice" in text or "payment" in text:
category = "Finance"
elif "meeting" in text or "call" in text:
category = "Meetings"
elif "trifecta" in text or "racing" in text:
category = "Racing"
elif "ebook" in text or "book" in text or "publish" in text:
category = "Publishing"
needs_reply = priority in ("P1-Critical", "P2-High")
return {
"priority": priority,
"category": category,
"needs_reply": needs_reply,
"suggested_label": f"{category} / {priority}",
}
def summarise_thread(emails: list) -> str:
lines = [f"# Thread Summary β€” {datetime.date.today().isoformat()}\n"]
lines.append(f"**Total messages:** {len(emails)}\n")
for i, e in enumerate(emails, 1):
lines.append(f"{i}. **{e.get('subject', '(no subject)')}** β€” {e.get('sender', 'unknown')} ({e.get('date', '')[:10]})")
lines.append("\n## Action Items\n")
action_items = []
for e in emails:
body = e.get("body", "")
if re.search(r"please\s+(review|send|confirm|reply|action|update)", body, re.IGNORECASE):
action_items.append(f"- [ ] {e.get('subject', 'Action required')} β€” from {e.get('sender', 'unknown')}")
if not action_items:
action_items = ["- [ ] Review thread for any outstanding requests"]
lines.extend(action_items[:5])
return "\n".join(lines)
def draft_reply(original: dict, tone: str = "professional") -> str:
name = original.get("sender", "there").split("@")[0].replace(".", " ").title()
templates = {
"professional": f"""Hi {name},
Thank you for your email regarding "{original.get('subject', '')}". I've reviewed your message and will follow up shortly.
Best regards,
Brett Anthony Sjoberg""",
"friendly": f"""Hi {name},
Thanks for reaching out! I appreciate you getting in touch about "{original.get('subject', '')}".
I'll get back to you with more details soon.
Cheers,
Brett""",
"brief": f"""{name} β€” re: {original.get('subject', '')}
Acknowledged. Will follow up.
Brett""",
}
return templates.get(tone, templates["professional"])
def archive_email(email: dict, folder: str = "Archive") -> str:
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
fname = f"{ts}_{email.get('id', 'email')}.md"
target = EMAIL_DIR / folder / fname
md = f"""# {email.get('subject', '(no subject)')}
**From:** {email.get('sender', '')}
**Date:** {email.get('date', '')}
**Priority:** {email.get('triage', {}).get('priority', 'P3-Normal')}
**Category:** {email.get('triage', {}).get('category', 'General')}
**Needs Reply:** {email.get('triage', {}).get('needs_reply', False)}
**Stored:** {ts}
---
{email.get('body', '')}
"""
target.write_text(md, encoding="utf-8")
return str(target)
def build_obsidian_email_vault():
index = f"""# Email Vault
## Brettapps Email System
**Account:** brett@brettapps.com
**Vault Root:** {EMAIL_DIR}
## Folders
| Folder | Purpose |
|--------|---------|
| [Inbox](./Inbox) | New emails awaiting triage |
| [Drafts](./Drafts) | AI-generated reply drafts |
| [Sent](./Sent) | Emails sent |
| [Archive](./Archive) | Processed emails |
| [Templates](./Templates) | Reply templates |
| [Action_Items](./Action_Items) | Tasks extracted from emails |
## Quick Stats
- Inbox: {len(list((EMAIL_DIR/'Inbox').glob('*.md')))} files
- Drafts: {len(list((EMAIL_DIR/'Drafts').glob('*.md')))} files
- Archive: {len(list((EMAIL_DIR/'Archive').glob('*.md')))} files
"""
(EMAIL_DIR / "README.md").write_text(index, encoding="utf-8")
def run_email_workflow(emails: list) -> dict:
"""Process a batch of emails through the workforce."""
results = {"processed": 0, "triaged": [], "drafted": [], "archived": [], "actions": []}
for email in emails:
results["processed"] += 1
# Agent 1: Triage
triage = triage_email(email.get("subject", ""), email.get("body", ""), email.get("sender", ""))
email["triage"] = triage
results["triaged"].append({"id": email.get("id"), "priority": triage["priority"], "category": triage["category"]})
# Agent 2: Draft (if needs reply)
if triage["needs_reply"]:
draft = draft_reply(email, tone="professional")
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
draft_fname = f"draft_{ts}_{email.get('id', 'reply')}.md"
draft_path = EMAIL_DIR / "Drafts" / draft_fname
draft_md = f"""# Draft Reply β€” {email.get('subject', '')}
**To:** {email.get('sender', '')}
**Subject:** Re: {email.get('subject', '')}
**Priority:** {triage['priority']}
**Status:** Pending review
---
{draft}
"""
draft_path.write_text(draft_md, encoding="utf-8")
results["drafted"].append(str(draft_path))
# Agent 3: Summarise
summary = summarise_thread([email])
results["actions"].append({"id": email.get("id"), "summary": summary[:200]})
# Agent 4: Archive
archive_path = archive_email(email, folder="Archive")
results["archived"].append(archive_path)
build_obsidian_email_vault()
return results