Brettapps commited on
Commit
00a67f9
Β·
verified Β·
1 Parent(s): 2e06b0b

Email assistant static demo + Obsidian vault workflow

Browse files
Files changed (6) hide show
  1. data/sample_emails.json +23 -0
  2. deploy.py +28 -0
  3. hf_config.yaml +6 -0
  4. index.html +170 -18
  5. requirements.txt +2 -0
  6. src/email_workflow.py +181 -0
data/sample_emails.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "e001",
4
+ "subject": "Fair Dinkum Publishing β€” EPUB build ready for review",
5
+ "sender": "designer@brettapps.com",
6
+ "date": "2026-08-12T09:30:00+10:00",
7
+ "body": "Hi Brett, the EPUB and PDF files for the Port Noarlunga guide are ready for proofreading. Please review by EOD. Also need your ABN confirmation for the copyright page."
8
+ },
9
+ {
10
+ "id": "e002",
11
+ "subject": "HF Space deployment β€” Brettapps/FairDinkumPublishing",
12
+ "sender": "deploy@huggingface.co",
13
+ "date": "2026-08-12T11:00:00+10:00",
14
+ "body": "Space build is in progress. Dockerfile builds cleanly, dependencies installed. Will notify when APP_RUNNING."
15
+ },
16
+ {
17
+ "id": "e003",
18
+ "subject": "Newsletter: This week in AI β€” unsubscribe",
19
+ "sender": "no-reply@ainewsletter.com",
20
+ "date": "2026-08-12T07:15:00+10:00",
21
+ "body": "Top stories this week: new MCP servers, Gradio updates, open-source ebook tools. Click here to unsubscribe."
22
+ }
23
+ ]
deploy.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Deploy Brettapps/email-assistant Static HF Space."""
3
+ from __future__ import annotations
4
+ import os
5
+ from pathlib import Path
6
+ from huggingface_hub import HfApi, create_repo, upload_folder
7
+
8
+ SPACE_ID = "Brettapps/email-assistant"
9
+ LOCAL_DIR = Path(__file__).resolve().parents[0]
10
+
11
+ def deploy() -> None:
12
+ token = os.environ.get("HF_ACCESS_TOKEN") or os.environ.get("HF_TOKEN")
13
+ if not token:
14
+ raise EnvironmentError("HF_ACCESS_TOKEN / HF_TOKEN required")
15
+ api = HfApi(token=token)
16
+ try:
17
+ api.whoami()
18
+ except Exception as exc:
19
+ print(f"HF auth failed: {exc}")
20
+ raise SystemExit(1)
21
+ create_repo(repo_id=SPACE_ID, repo_type="space", token=token, exist_ok=True, space_sdk="static")
22
+ print(f"Deploying {LOCAL_DIR} -> {SPACE_ID}")
23
+ upload_folder(folder_path=str(LOCAL_DIR), repo_id=SPACE_ID, repo_type="space",
24
+ commit_message="Email assistant static demo + Obsidian vault workflow", token=token)
25
+ print(f"Done: https://huggingface.co/spaces/{SPACE_ID}")
26
+
27
+ if __name__ == "__main__":
28
+ deploy()
hf_config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ sdk: static
2
+ python_version: 3.11
3
+ files:
4
+ - index.html
5
+ - data/**
6
+ secrets: []
index.html CHANGED
@@ -1,19 +1,171 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en-AU">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Brettapps Email Assistant</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ background: #0f172a; color: #e2e8f0; min-height: 100vh; }
11
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
12
+ h1 { color: #f4a261; font-size: 2rem; margin-bottom: 0.5rem; }
13
+ .subtitle { color: #94a3b8; margin-bottom: 2rem; font-size: 1.1rem; }
14
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
15
+ @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
16
+ .card { background: #1e293b; border-radius: 12px; padding: 1.5rem;
17
+ border: 1px solid #334155; }
18
+ .card h2 { color: #60a5fa; font-size: 1.1rem; margin-bottom: 1rem;
19
+ display: flex; align-items: center; gap: 0.5rem; }
20
+ .badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 4px;
21
+ font-size: 0.75rem; font-weight: 600; margin-right: 0.4rem; }
22
+ .p1 { background: #dc2626; color: white; }
23
+ .p2 { background: #f59e0b; color: black; }
24
+ .p3 { background: #3b82f6; color: white; }
25
+ .p4 { background: #6b7280; color: white; }
26
+ .email-item { padding: 0.8rem; margin: 0.5rem 0; border-radius: 8px;
27
+ background: #0f172a; border-left: 3px solid #f4a261; }
28
+ .email-item .subject { font-weight: 600; color: #f1f5f9; }
29
+ .email-item .from { font-size: 0.85rem; color: #94a3b8; }
30
+ .email-item .priority { margin-top: 0.3rem; }
31
+ textarea { width: 100%; min-height: 300px; background: #0f172a;
32
+ color: #e2e8f0; border: 1px solid #334155; border-radius: 8px;
33
+ padding: 1rem; font-family: 'SF Mono', Monaco, monospace;
34
+ font-size: 0.85rem; resize: vertical; }
35
+ button { background: #f4a261; color: #0f172a; border: none; padding: 0.7rem 1.5rem;
36
+ border-radius: 8px; font-weight: 600; cursor: pointer; margin-top: 1rem; }
37
+ button:hover { background: #fbbf24; }
38
+ .results { background: #0f172a; padding: 1rem; border-radius: 8px;
39
+ margin-top: 1rem; font-family: monospace; font-size: 0.85rem;
40
+ white-space: pre-wrap; max-height: 400px; overflow-y: auto; }
41
+ .vault-tree { font-family: monospace; font-size: 0.8rem; color: #94a3b8; }
42
+ .vault-tree .folder { color: #60a5fa; }
43
+ .vault-tree .file { color: #a5b4fc; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div class="container">
48
+ <h1>πŸ“§ Brettapps Email Assistant</h1>
49
+ <p class="subtitle">brett@brettapps.com β€” AI triage Β· draft Β· archive Β· Obsidian vault</p>
50
+
51
+ <div class="grid">
52
+ <div class="card">
53
+ <h2>πŸ“₯ Email Input</h2>
54
+ <textarea id="emails" placeholder='[
55
+ {
56
+ "id": "e001",
57
+ "subject": "Fair Dinkum Publishing β€” EPUB ready",
58
+ "sender": "designer@brettapps.com",
59
+ "date": "2026-08-12T09:30:00+10:00",
60
+ "body": "Hi Brett, the EPUB files are ready for proofreading..."
61
+ }
62
+ ]'>[
63
+ {
64
+ "id": "e001",
65
+ "subject": "Fair Dinkum Publishing β€” EPUB build ready for review",
66
+ "sender": "designer@brettapps.com",
67
+ "date": "2026-08-12T09:30:00+10:00",
68
+ "body": "Hi Brett, the EPUB and PDF files for the Port Noarlunga guide are ready for proofreading. Please review by EOD. Also need your ABN confirmation for the copyright page."
69
+ },
70
+ {
71
+ "id": "e002",
72
+ "subject": "HF Space deployment β€” Brettapps/FairDinkumPublishing",
73
+ "sender": "deploy@huggingface.co",
74
+ "date": "2026-08-12T11:00:00+10:00",
75
+ "body": "Space build is in progress. Dockerfile builds cleanly, dependencies installed. Will notify when APP_RUNNING."
76
+ },
77
+ {
78
+ "id": "e003",
79
+ "subject": "Newsletter: This week in AI β€” unsubscribe",
80
+ "sender": "no-reply@ainewsletter.com",
81
+ "date": "2026-08-12T07:15:00+10:00",
82
+ "body": "Top stories this week: new MCP servers, Gradio updates. Click here to unsubscribe."
83
+ }
84
+ ]</textarea>
85
+ <button onclick="processEmails()">⚑ Run Email Workforce</button>
86
+ <div id="results" class="results" style="display:none;"></div>
87
+ </div>
88
+
89
+ <div class="card">
90
+ <h2>πŸ—‚οΈ Obsidian Email Vault</h2>
91
+ <div class="vault-tree">
92
+ <div class="folder">πŸ“ Email/</div>
93
+ <div class="folder"> πŸ“ Inbox/</div>
94
+ <div class="file"> πŸ“„ e001_fair_dinkum_publishing_epub.md</div>
95
+ <div class="file"> πŸ“„ e002_hf_space_deployment.md</div>
96
+ <div class="folder"> πŸ“ Drafts/</div>
97
+ <div class="file"> πŸ“„ draft_20260812_re_e001.md</div>
98
+ <div class="folder"> πŸ“ Archive/</div>
99
+ <div class="folder"> πŸ“ Sent/</div>
100
+ <div class="folder"> πŸ“ Templates/</div>
101
+ <div class="folder"> πŸ“ Action_Items/</div>
102
+ <div class="file"> πŸ“„ abn_confirmation_needed.md</div>
103
+ <div class="file"> πŸ“„ epub_review_eod.md</div>
104
+ <div class="file"> πŸ“„ README.md</div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="card" style="margin-top: 1.5rem;">
110
+ <h2>πŸ€– Agent Workforce</h2>
111
+ <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-top: 1rem;">
112
+ <div>
113
+ <strong style="color:#f4a261;">1. Triage Agent</strong>
114
+ <p style="font-size:0.85rem; color:#94a3b8; margin-top:0.3rem;">Classifies emails by priority (P1-P5) and category using regex rules. Flags reply-needed items.</p>
115
+ </div>
116
+ <div>
117
+ <strong style="color:#f4a261;">2. Draft Agent</strong>
118
+ <p style="font-size:0.85rem; color:#94a3b8; margin-top:0.3rem;">Generates contextual reply drafts stored in Obsidian/Drafts for review before sending.</p>
119
+ </div>
120
+ <div>
121
+ <strong style="color:#f4a261;">3. Summary Agent</strong>
122
+ <p style="font-size:0.85rem; color:#94a3b8; margin-top:0.3rem;">Extracts action items from threads using regex patterns. Creates markdown summaries.</p>
123
+ </div>
124
+ <div>
125
+ <strong style="color:#f4a261;">4. Archive Agent</strong>
126
+ <p style="font-size:0.85rem; color:#94a3b8; margin-top:0.3rem;">Files processed emails into Obsidian/Archive with timestamped filenames and metadata.</p>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <script>
133
+ function processEmails() {
134
+ const input = document.getElementById('emails').value;
135
+ const results = document.getElementById('results');
136
+ try {
137
+ const emails = JSON.parse(input);
138
+ let triaged = [], drafted = [], actions = [];
139
+ emails.forEach(e => {
140
+ const text = (e.subject + ' ' + e.body + ' ' + e.sender).toLowerCase();
141
+ let priority = 'P3-Normal', category = 'General', needsReply = false;
142
+ if (/urgent|asap|critical/.test(text)) { priority = 'P1-Critical'; needsReply = true; }
143
+ else if (/invoice|payment|deadline|hf\.|huggingface|space|ebook|publish|trifecta|brettapps/.test(text)) { priority = 'P2-High'; needsReply = true; }
144
+ else if (/newsletter|unsubscribe|no.?reply/.test(text)) priority = 'P4-Low';
145
+ else if (/promo|sale|discount/.test(text)) priority = 'P5-Spam';
146
+ if (/hf\.|huggingface/.test(text)) category = 'HF Spaces';
147
+ else if (/invoice|payment/.test(text)) category = 'Finance';
148
+ else if (/meeting|call/.test(text)) category = 'Meetings';
149
+ else if (/trifecta|racing/.test(text)) category = 'Racing';
150
+ else if (/ebook|book|publish/.test(text)) category = 'Publishing';
151
+ triaged.push({id: e.id, priority, category});
152
+ if (needsReply) {
153
+ drafted.push('draft_' + e.id + '.md');
154
+ const name = e.sender.split('@')[0].replace(/\./g,' ').replace(/\b\w/g,c=>c.toUpperCase());
155
+ actions.push('- [ ] ' + e.subject + ' β€” from ' + e.sender);
156
+ }
157
+ });
158
+ let out = '## Processed: ' + emails.length + ' emails\\n\\n### Triage\\n| ID | Priority | Category |\\n|----|----------|----------|\\n';
159
+ triaged.forEach(t => out += '| ' + t.id + ' | ' + t.priority + ' | ' + t.category + ' |\\n');
160
+ if (drafted.length) { out += '\\n### Drafts (' + drafted.length + ')\\n'; drafted.forEach(d => out += '- `' + d + '`\\n'); }
161
+ if (actions.length) { out += '\\n### Action Items\\n' + actions.join('\\n'); }
162
+ results.style.display = 'block';
163
+ results.textContent = out;
164
+ } catch (err) {
165
+ results.style.display = 'block';
166
+ results.textContent = 'Error: ' + err.message;
167
+ }
168
+ }
169
+ </script>
170
+ </body>
171
  </html>
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=5.0
2
+ Pillow>=10.0
src/email_workflow.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Email Assistant Agent Workforce for brett@brettapps.com
4
+ Agents:
5
+ 1. Triage Agent β€” read inbox, classify, prioritise
6
+ 2. Draft Agent β€” compose replies, store in Obsidian drafts
7
+ 3. Summary Agent β€” thread summaries, action items
8
+ 4. Archive Agent β€” label, move, snooze via Obsidian filing system
9
+ """
10
+ import json, os, re, datetime, subprocess, sys
11
+ from pathlib import Path
12
+
13
+ OBSIDIAN_VAULT = Path(os.environ.get("OBSIDIAN_VAULT", "/tmp/email_vault"))
14
+ EMAIL_DIR = OBSIDIAN_VAULT / "Email"
15
+ for d in [EMAIL_DIR / "Inbox", EMAIL_DIR / "Drafts", EMAIL_DIR / "Sent",
16
+ EMAIL_DIR / "Archive", EMAIL_DIR / "Templates", EMAIL_DIR / "Action_Items"]:
17
+ d.mkdir(parents=True, exist_ok=True)
18
+
19
+ PRIORITY_RULES = [
20
+ (r"urgent|asap|immediately|critical", "P1-Critical"),
21
+ (r"invoice|payment|overdue|deadline", "P2-High"),
22
+ (r"meeting|call|schedule|calendar", "P2-High"),
23
+ (r" Brettapps|fair dinkum|trifecta|ebook|publish", "P2-High"),
24
+ (r"newsletter|unsubscribe|no.?reply", "P4-Low"),
25
+ (r"promo|sale|discount|limited.?time", "P5-Spam"),
26
+ ]
27
+
28
+ def triage_email(subject: str, body: str, sender: str) -> dict:
29
+ text = f"{subject} {body} {sender}".lower()
30
+ priority = "P3-Normal"
31
+ category = "General"
32
+ for pattern, level in PRIORITY_RULES:
33
+ if re.search(pattern, text, re.IGNORECASE):
34
+ priority = level
35
+ break
36
+ if "hf." in text or "huggingface" in text or "space" in text:
37
+ category = "HF Spaces"
38
+ elif "invoice" in text or "payment" in text:
39
+ category = "Finance"
40
+ elif "meeting" in text or "call" in text:
41
+ category = "Meetings"
42
+ elif "trifecta" in text or "racing" in text:
43
+ category = "Racing"
44
+ elif "ebook" in text or "book" in text or "publish" in text:
45
+ category = "Publishing"
46
+ needs_reply = priority in ("P1-Critical", "P2-High")
47
+ return {
48
+ "priority": priority,
49
+ "category": category,
50
+ "needs_reply": needs_reply,
51
+ "suggested_label": f"{category} / {priority}",
52
+ }
53
+
54
+ def summarise_thread(emails: list) -> str:
55
+ lines = [f"# Thread Summary β€” {datetime.date.today().isoformat()}\n"]
56
+ lines.append(f"**Total messages:** {len(emails)}\n")
57
+ for i, e in enumerate(emails, 1):
58
+ lines.append(f"{i}. **{e.get('subject', '(no subject)')}** β€” {e.get('sender', 'unknown')} ({e.get('date', '')[:10]})")
59
+ lines.append("\n## Action Items\n")
60
+ action_items = []
61
+ for e in emails:
62
+ body = e.get("body", "")
63
+ if re.search(r"please\s+(review|send|confirm|reply|action|update)", body, re.IGNORECASE):
64
+ action_items.append(f"- [ ] {e.get('subject', 'Action required')} β€” from {e.get('sender', 'unknown')}")
65
+ if not action_items:
66
+ action_items = ["- [ ] Review thread for any outstanding requests"]
67
+ lines.extend(action_items[:5])
68
+ return "\n".join(lines)
69
+
70
+ def draft_reply(original: dict, tone: str = "professional") -> str:
71
+ name = original.get("sender", "there").split("@")[0].replace(".", " ").title()
72
+ templates = {
73
+ "professional": f"""Hi {name},
74
+
75
+ Thank you for your email regarding "{original.get('subject', '')}". I've reviewed your message and will follow up shortly.
76
+
77
+ Best regards,
78
+ Brett Anthony Sjoberg""",
79
+ "friendly": f"""Hi {name},
80
+
81
+ Thanks for reaching out! I appreciate you getting in touch about "{original.get('subject', '')}".
82
+
83
+ I'll get back to you with more details soon.
84
+
85
+ Cheers,
86
+ Brett""",
87
+ "brief": f"""{name} β€” re: {original.get('subject', '')}
88
+
89
+ Acknowledged. Will follow up.
90
+
91
+ Brett""",
92
+ }
93
+ return templates.get(tone, templates["professional"])
94
+
95
+ def archive_email(email: dict, folder: str = "Archive") -> str:
96
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
97
+ fname = f"{ts}_{email.get('id', 'email')}.md"
98
+ target = EMAIL_DIR / folder / fname
99
+ md = f"""# {email.get('subject', '(no subject)')}
100
+
101
+ **From:** {email.get('sender', '')}
102
+ **Date:** {email.get('date', '')}
103
+ **Priority:** {email.get('triage', {}).get('priority', 'P3-Normal')}
104
+ **Category:** {email.get('triage', {}).get('category', 'General')}
105
+ **Needs Reply:** {email.get('triage', {}).get('needs_reply', False)}
106
+ **Stored:** {ts}
107
+
108
+ ---
109
+
110
+ {email.get('body', '')}
111
+ """
112
+ target.write_text(md, encoding="utf-8")
113
+ return str(target)
114
+
115
+ def build_obsidian_email_vault():
116
+ index = f"""# Email Vault
117
+
118
+ ## Brettapps Email System
119
+ **Account:** brett@brettapps.com
120
+ **Vault Root:** {EMAIL_DIR}
121
+
122
+ ## Folders
123
+
124
+ | Folder | Purpose |
125
+ |--------|---------|
126
+ | [Inbox](./Inbox) | New emails awaiting triage |
127
+ | [Drafts](./Drafts) | AI-generated reply drafts |
128
+ | [Sent](./Sent) | Emails sent |
129
+ | [Archive](./Archive) | Processed emails |
130
+ | [Templates](./Templates) | Reply templates |
131
+ | [Action_Items](./Action_Items) | Tasks extracted from emails |
132
+
133
+ ## Quick Stats
134
+ - Inbox: {len(list((EMAIL_DIR/'Inbox').glob('*.md')))} files
135
+ - Drafts: {len(list((EMAIL_DIR/'Drafts').glob('*.md')))} files
136
+ - Archive: {len(list((EMAIL_DIR/'Archive').glob('*.md')))} files
137
+ """
138
+ (EMAIL_DIR / "README.md").write_text(index, encoding="utf-8")
139
+
140
+ def run_email_workflow(emails: list) -> dict:
141
+ """Process a batch of emails through the workforce."""
142
+ results = {"processed": 0, "triaged": [], "drafted": [], "archived": [], "actions": []}
143
+
144
+ for email in emails:
145
+ results["processed"] += 1
146
+
147
+ # Agent 1: Triage
148
+ triage = triage_email(email.get("subject", ""), email.get("body", ""), email.get("sender", ""))
149
+ email["triage"] = triage
150
+ results["triaged"].append({"id": email.get("id"), "priority": triage["priority"], "category": triage["category"]})
151
+
152
+ # Agent 2: Draft (if needs reply)
153
+ if triage["needs_reply"]:
154
+ draft = draft_reply(email, tone="professional")
155
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
156
+ draft_fname = f"draft_{ts}_{email.get('id', 'reply')}.md"
157
+ draft_path = EMAIL_DIR / "Drafts" / draft_fname
158
+ draft_md = f"""# Draft Reply β€” {email.get('subject', '')}
159
+
160
+ **To:** {email.get('sender', '')}
161
+ **Subject:** Re: {email.get('subject', '')}
162
+ **Priority:** {triage['priority']}
163
+ **Status:** Pending review
164
+
165
+ ---
166
+
167
+ {draft}
168
+ """
169
+ draft_path.write_text(draft_md, encoding="utf-8")
170
+ results["drafted"].append(str(draft_path))
171
+
172
+ # Agent 3: Summarise
173
+ summary = summarise_thread([email])
174
+ results["actions"].append({"id": email.get("id"), "summary": summary[:200]})
175
+
176
+ # Agent 4: Archive
177
+ archive_path = archive_email(email, folder="Archive")
178
+ results["archived"].append(archive_path)
179
+
180
+ build_obsidian_email_vault()
181
+ return results