Spaces:
Running
Running
Download src/email_workflow.py from Brettapps/email-assistant: direct link, hf CLI and curl.
- Browser
- Download file 8.24 kB
-
https://huggingface.co/spaces/Brettapps/email-assistant/resolve/main/src/email_workflow.py
- Command line
-
hf download hf://spaces/Brettapps/email-assistant/src/email_workflow.py
-
curl -L -o email_workflow.py https://huggingface.co/spaces/Brettapps/email-assistant/resolve/main/src/email_workflow.py
8.24 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 | |
| 5. Image Agent — generate images via HuggingFace FLUX | |
| """ | |
| 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", | |
| EMAIL_DIR / "Images"]: | |
| 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 | | |
| | [Images](./Images) | Generated images | | |
| ## 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 | |
| - Images: {len(list((EMAIL_DIR/'Images').glob('*.png')))} files | |
| """ | |
| (EMAIL_DIR / "README.md").write_text(index, encoding="utf-8") | |
| def generate_image_from_email(email: dict, prompt: str | None = None) -> dict: | |
| """Generate an image for an email request using HF FLUX.""" | |
| from src.agents.image_agent import generate_image, generate_from_email | |
| if prompt: | |
| result = generate_image(prompt) | |
| else: | |
| result = generate_from_email(email) | |
| if "error" not in result: | |
| email["image_result"] = result | |
| ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") | |
| img_dir = EMAIL_DIR / "Images" | |
| meta_path = img_dir / f"{ts}_image_request.json" | |
| with open(meta_path, "w") as f: | |
| json.dump({ | |
| "source_email": { | |
| "id": email.get("id"), | |
| "subject": email.get("subject"), | |
| "sender": email.get("sender"), | |
| }, | |
| "image_result": result, | |
| }, f, indent=2) | |
| return result | |
| def run_email_workflow(emails: list) -> dict: | |
| """Process a batch of emails through the workforce.""" | |
| results = {"processed": 0, "triaged": [], "drafted": [], "archived": [], "actions": [], "images": []} | |
| 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) | |
| # Agent 5: Image generation (if email requests an image) | |
| body_lower = email.get("body", "").lower() | |
| subject_lower = email.get("subject", "").lower() | |
| if any(w in body_lower or w in subject_lower for w in ["image", "picture", "generate", "create", "draw", "make a"]): | |
| img_result = generate_image_from_email(email) | |
| results["images"].append({"id": email.get("id"), "result": img_result}) | |
| build_obsidian_email_vault() | |
| return results |