Spaces:
Running
Running
Email assistant static demo + Obsidian vault workflow
Browse files- data/sample_emails.json +23 -0
- deploy.py +28 -0
- hf_config.yaml +6 -0
- index.html +170 -18
- requirements.txt +2 -0
- 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|