Show / hide languages:
📋 Course plans · Week 2 / 6

📧 Week 2 — Email

Hermes drafts replies, launchd runs the script at 8 AM, you read drafts over your morning coffee.

Hermes ร่างคำตอบ launchd รันสคริปต์ตอน 8 โมงเช้า คุณดูร่างขณะจิบกาแฟ

Hermes 起草回复,launchd 每天早 8 点跑脚本,你边喝咖啡边审稿。

⏰ launchd / Task Scheduler 📫 Gmail API drafts 🛡️ HITL ⏱️ 90 min

🎯 Learning objectives

1

Triage with labels

Create a Gmail label/filter so Hermes only reads high-value mail.

สร้าง label/filter ของ Gmail ให้ Hermes อ่านเฉพาะอีเมลสำคัญ

建一个 Gmail 标签/筛选器,让 Hermes 只读重要邮件。

2

Draft-then-confirm

Add draft_gmail_reply — saves to Drafts, never sends.

เพิ่ม draft_gmail_reply — บันทึกลง Drafts ไม่ส่ง

添加 draft_gmail_reply — 只存草稿,从不发送。

3

Schedule with the OS

Wire launchd (Mac) / Task Scheduler (Win) to run daily_brief.py at 8 AM — same pattern as the extras' daily_word.py.

ตั้ง launchd (Mac) / Task Scheduler (Win) รัน daily_brief.py 8 โมง — pattern เดียวกับ daily_word.py ใน extras

用 launchd(Mac)/ 任务计划程序(Win)每天 8 点跑 daily_brief.py — 与 extras 里的 daily_word.py 同样模式。

📬 Interactive: triage in action

Click Run daily_brief.py. Watch the agent process only the labelled rows — newsletters and spam never touch Hermes' context window.

คลิก Run daily_brief.py ดู agent ประมวลผลเฉพาะแถวที่ติด label — จดหมายข่าวกับสแปมไม่เข้าถึง context ของ Hermes เลย

点击 Run daily_brief.py,看代理只处理带标签的行 — 邮件订阅和垃圾邮件根本不会进入 Hermes 的上下文。

@AI_Handle Marco (London) — "Do you have a 3-day jungle trek next week?"
no label Newsletter — "10 things every digital nomad …"
@AI_Handle Khun Som — "ขอราคาห้องสำหรับ 4 คืน ค่ะ"
no label Spam — "🎰 Big winner waiting for you!"
@AI_Handle 张先生 — "请问价格和取消政策?"

📚 Key concepts

🇬🇧 English

Auto-triage — Gmail's filter rules are doing the cheap work. Hermes only burns tokens on emails the filter labelled @AI_Handle.

Draft-then-confirm — the FunctionTool calls drafts().create() only. It cannot send. The capability isn't there to abuse.

OS-scheduled agent run — launchd / Task Scheduler runs python daily_brief.py at 8 AM. The agent never schedules itself.

🇹🇭 ไทย

Auto-triage — ตัวกรองของ Gmail ทำงานหนัก Hermes เผา token เฉพาะอีเมลที่ติด @AI_Handle

Draft-then-confirm — FunctionTool เรียก drafts().create() เท่านั้น ส่งไม่ได้ ไม่มีความสามารถให้ใช้ผิด

OS-scheduled agent run — launchd / Task Scheduler รัน python daily_brief.py 8 โมง agent ไม่เคยตั้งเวลาตัวเอง

🇨🇳 中文

自动分流 — Gmail 的过滤规则承担粗活,Hermes 只在带 @AI_Handle 的邮件上消耗 token。

先起草再确认 — FunctionTool 只调 drafts().create()。它没有发送能力,自然无法滥用。

由操作系统调度 — launchd / 任务计划程序每天 8 点跑 python daily_brief.py,代理永远不会自己排定时任务。

🧪 Hands-on lab

1
2
3
4
1

Create the Gmail label + filter

In Gmail → Settings → Filters → Create. Match subject/body containing booking | price | tour | availability | ราคา | 价格 → apply label @AI_Handle.

Gmail → Settings → Filters → Create กรองคำว่า booking | price | tour | availability | ราคา | 价格 → ใส่ label @AI_Handle

Gmail → 设置 → 过滤器 → 新建:匹配 booking | price | tour | availability | ราคา | 价格 → 套用标签 @AI_Handle。

2

Upgrade the OAuth scope

In gmail_auth.py add the compose scope. Delete the cached token so the consent screen runs again.

ใน gmail_auth.py เพิ่ม scope compose ลบ token cache เพื่อให้หน้า consent มาใหม่

在 gmail_auth.py 加上 compose 权限。删除缓存的 token,让同意页面再跑一次。

SCOPES = [ "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.compose", # NEW — saves drafts ]
3

Add the draft tool

import base64, email.message def draft_gmail_reply(message_id: str, body: str) -> str: """Save a reply to message_id as a draft. NEVER sends.""" svc = build("gmail", "v1", credentials=load_user_creds()) orig = svc.users().messages().get(userId="me", id=message_id, format="metadata").execute() headers = {h["name"]: h["value"] for h in orig["payload"]["headers"]} msg = email.message.EmailMessage(); msg["To"] = headers["From"] msg["Subject"] = "Re: " + headers.get("Subject", "") msg["In-Reply-To"] = headers.get("Message-ID", ""); msg.set_content(body) raw = base64.urlsafe_b64encode(msg.as_bytes()).decode() d = svc.users().drafts().create(userId="me", body={"message": {"raw": raw, "threadId": orig["threadId"]}}).execute() return f"draft saved: {d['id']}" TOOLS.append(FunctionTool.from_defaults(draft_gmail_reply))
4

The entry point + launchd plist

# daily_brief.py — what launchd actually runs from agent_pro import build_agent agent = build_agent() print(agent.chat( "Read unread emails in label '@AI_Handle'. " "Output a 3-bullet summary, then save FAQ replies to Drafts. " "Do NOT send anything." ))
<!-- ~/Library/LaunchAgents/com.kruai.daily.plist (Mac) --> <key>Label</key><string>com.kruai.daily</string> <key>ProgramArguments</key><array> <string>/Users/YOU/mac_stack/.venv/bin/python</string> <string>/Users/YOU/mac_stack/daily_brief.py</string> </array> <key>StartCalendarInterval</key><dict><key>Hour</key><integer>8</integer><key>Minute</key><integer>0</integer></dict>

Install & smoke-test:

ติดตั้ง + ทดสอบ:

安装并冒烟测试:

$ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kruai.daily.plist $ launchctl start com.kruai.daily # fire once now to verify

Windows equivalent — Task Scheduler → Create Basic Task → Daily 08:00 → start python C:\path\to\daily_brief.py.

บน Windows — Task Scheduler → Create Basic Task → Daily 08:00 → รัน python C:\path\to\daily_brief.py

Windows 等价做法 — 任务计划程序 → 创建基本任务 → 每日 08:00 → 启动 python C:\path\to\daily_brief.py。

⚠️ Top gotchas

1. The "send" hallucination

If you ever add a send_gmail tool, the agent will use it even when you said "draft only." Don't add the tool.

ถ้าเพิ่ม send_gmail เมื่อไหร่ agent จะใช้แม้สั่ง "ร่างเท่านั้น" — อย่าเพิ่มเครื่องมือนี้

一旦加了 send_gmail 工具,即使你说"只起草",代理也会用它 — 干脆别加。

2. Sleeping laptop misses 8 AM

launchd can't wake a powered-off Mac. Use pmset repeat wakeorpoweron MTWRF 07:55:00.

launchd ปลุก Mac ที่ปิดเครื่องไม่ได้ ใช้ pmset repeat wakeorpoweron MTWRF 07:55:00

launchd 没法唤醒关机的 Mac,用 pmset repeat wakeorpoweron MTWRF 07:55:00。

3. Context drain from raw inbox

Hermes reading every email burns context. The label filter is what makes this affordable.

Hermes อ่านทุกอีเมล = เผา context ตัวกรอง label คือสิ่งที่ทำให้ทำได้

让 Hermes 读所有邮件会烧光上下文。标签过滤是这个方案能跑下去的关键。

📝 Assessment & homework

✅ Assessment

Practical: Drafts folder contains agent-written replies to dummy emails. Conceptual: why we filter before the agent reads, not after.

ภาคปฏิบัติ: โฟลเดอร์ Drafts มีร่างจาก agent ภาคทฤษฎี: ทำไมต้องกรอง ก่อน agent อ่าน ไม่ใช่หลัง

实操:草稿箱里有代理写的回复。概念:为什么要在代理读邮件之前过滤,而不是之后。

🏠 Homework

Let the 8 AM brief run 3 mornings. Bring the one email the agent answered worst — probably a meeting request, which is exactly Week 3.

ปล่อยให้รัน 3 เช้า เอาอีเมลที่ตอบแย่ที่สุดมาด้วย — น่าจะเป็นการนัดประชุม ซึ่งคือ Week 3 พอดี

让 8 点的简报跑三个早晨,挑出代理答得最差的一封 — 很可能是约会请求,正好是 Week 3 的内容。