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

📅 Week 3 — Calendar

Free/busy scans, ICT-anchored timezone math, conflict re-checks before booking.

สแกน free/busy คำนวณเวลาแบบยึด ICT และเช็คเวลาทับซ้อนก่อนจอง

读取忙闲、以 ICT 为基准做时区换算、订之前再次冲突检查。

📆 Calendar API 🌏 zoneinfo Asia/Bangkok ⚠️ DST traps ⏱️ 90 min

🎯 Learning objectives

1

Two new FunctionTools

list_free_slots and book_event against the Google Calendar API.

list_free_slots และ book_event เชื่อม Google Calendar API

用 Google 日历 API 实现 list_free_slots 与 book_event。

2

Anchor Hermes in ICT

Use zoneinfo.ZoneInfo("Asia/Bangkok") so the model never guesses UTC.

ใช้ zoneinfo.ZoneInfo("Asia/Bangkok") เพื่อไม่ให้โมเดลเดา UTC

用 zoneinfo.ZoneInfo("Asia/Bangkok"),让模型不再瞎猜 UTC。

3

Conflict re-check

Booking must re-query free/busy at the exact moment of insertion — closes the race.

ตอนจองต้อง re-query free/busy ทันที — ปิดช่อง race

下单时再次查 free/busy — 关闭竞争窗口。

🕒 Interactive: the timezone trap

Type a Chiang Mai (ICT) time. Watch where the same moment lands for your foreign clients. Pay attention to London during their DST — that's where most agents hallucinate.

พิมพ์เวลาเชียงใหม่ (ICT) ดูว่าวินาทีเดียวกันตรงกับเวลาใดของลูกค้าต่างประเทศ ระวังลอนดอนช่วง DST — agent หลายตัวสะดุดที่นี่

输入清迈(ICT)时间,看同一时刻在海外客户那边是几点。注意伦敦的夏令时 — 多数代理就在这里出错。

🇹🇭 Chiang Mai (ICT)
14:00
UTC+7 · no DST
🇬🇧 London
--:--
UTC+0 / +1 DST
🇺🇸 New York
--:--
UTC-5 / -4 DST
🇨🇳 Shanghai
--:--
UTC+8 · no DST
🇦🇺 Sydney
--:--
UTC+10 / +11 DST

⚠️ DST is a moving target

London + Sydney swap their offset twice a year. Your agent must compute the offset for the booking date — never hard-code it. zoneinfo does this for you.

ลอนดอน + ซิดนีย์เปลี่ยน offset ปีละ 2 ครั้ง agent ต้อง คำนวณ offset ของวันนั้น — ห้ามฮาร์ดโค้ด zoneinfo ทำให้

伦敦和悉尼每年切换两次时区偏移。代理必须按预订日期 计算 偏移,绝不能写死。zoneinfo 会替你处理。

📚 Key concepts

🇬🇧 English

Free/busy scan — freebusy().query() returns busy intervals without exposing private event details.

Timezone math — always pass explicit +07:00 in API calls; use ZoneInfo("Asia/Bangkok") for local-time math.

Conflict re-check — propose → wait for human confirmation → re-check free/busy → book. The re-check closes the race window.

🇹🇭 ไทย

Free/busy scan — freebusy().query() คืนช่วงเวลาที่ไม่ว่าง โดยไม่เปิดรายละเอียดส่วนตัวของอีเวนต์

Timezone math — ส่ง +07:00 ใน API call เสมอ ใช้ ZoneInfo("Asia/Bangkok") สำหรับคำนวณ local-time

Conflict re-check — เสนอ → รอยืนยัน → re-check → จอง การ re-check ปิดช่อง race

🇨🇳 中文

忙闲查询 — freebusy().query() 返回忙时段,不暴露具体事件细节。

时区计算 — API 调用一律带 +07:00;本地时间计算用 ZoneInfo("Asia/Bangkok")。

下单前重查冲突 — 提议 → 等待确认 → 再次查询 free/busy → 下单。这一步关闭竞争窗口。

🧪 Hands-on lab

1
2
3
4
1

Add calendar scopes

SCOPES += [ "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events", ] # delete ~/.config/krueng/gmail_token.json so consent re-runs with new scopes
2

list_free_slots

from zoneinfo import ZoneInfo ICT = ZoneInfo("Asia/Bangkok") def list_free_slots(day_iso: str, duration_min: int = 60) -> str: """1-hour free blocks on day_iso (YYYY-MM-DD), in ICT, 09:00-17:00.""" svc = build("calendar", "v3", credentials=load_user_creds()) fb = svc.freebusy().query(body={ "timeMin": f"{day_iso}T09:00:00+07:00", "timeMax": f"{day_iso}T17:00:00+07:00", "items": [{"id": "primary"}], }).execute() return compute_gaps(fb["calendars"]["primary"]["busy"], duration_min) TOOLS.append(FunctionTool.from_defaults(list_free_slots))
3

book_event with built-in conflict re-check

def book_event(start_iso: str, end_iso: str, summary: str, invitee_email: str) -> str: """Book a calendar event. Re-checks free/busy first to prevent double-booking.""" svc = build("calendar", "v3", credentials=load_user_creds()) fb = svc.freebusy().query(body={ "timeMin": start_iso, "timeMax": end_iso, "items": [{"id": "primary"}], }).execute() if fb["calendars"]["primary"]["busy"]: return "CONFLICT — slot taken since proposal; ask user for another time" ev = svc.events().insert(calendarId="primary", body={ "summary": summary, "start": {"dateTime": start_iso, "timeZone": "Asia/Bangkok"}, "end": {"dateTime": end_iso, "timeZone": "Asia/Bangkok"}, "attendees": [{"email": invitee_email}], }, sendUpdates="all").execute() return f"booked {ev['id']}" TOOLS.append(FunctionTool.from_defaults(book_event))
4

End-to-end prompt

Paste into the agent REPL with last week's London-client email loaded into your @AI_Handle label:

วางใน REPL ของ agent โดยอีเมลลูกค้าลอนดอนของสัปดาห์ที่แล้วอยู่ใน label @AI_Handle:

把上周伦敦客户的邮件加上 @AI_Handle 标签后,在 agent REPL 里粘贴:

you > Read the London-client email in @AI_Handle. Find 3 free 1-hour slots next Tuesday between 09:00 and 17:00 ICT. Convert each to London time accounting for current DST. Use draft_gmail_reply to propose the slots. Do NOT book yet.

⚠️ Top gotchas

1. Hermes thinks in UTC

Without explicit +07:00 in API calls you'll book at the wrong hour. Anchor everything.

ไม่ส่ง +07:00 ใน API จะจองผิดชั่วโมง ต้อง anchor ทุกที่

API 调用没带 +07:00,订单会落在错误时刻。处处都要锚定。

2. Mid-task token expiry

Wrap build() with a retry-on-401 that triggers load_user_creds() again.

ห่อ build() ด้วย retry ตอน 401 เพื่อเรียก load_user_creds() ใหม่

把 build() 包一层 401 重试,让它重新调 load_user_creds()。

3. Race condition on booking

A human can grab the slot between propose and book. The re-check inside book_event is non-negotiable.

คนอาจคว้า slot ระหว่าง propose กับ book การ re-check ใน book_event ขาดไม่ได้

提议与下单之间有人会抢走时段。book_event 内的二次检查不可省略。

📝 Assessment & homework

✅ Assessment

A Google Calendar event created with correct ICT-to-London conversion and the dummy client invited. Explain why book_event does a second free/busy check.

มี event ใน Google Calendar แปลงเวลา ICT-to-London ถูกต้อง อธิบายว่าทำไม book_event ต้อง re-check

Google 日历里有一个 ICT 转伦敦时间正确、邀请了客户的事件。说明 book_event 为什么要做第二次 free/busy 检查。

🏠 Homework

Propose slots to 2 real clients. Extend list_free_slots with a 30-min travel buffer between events (homestay → studio).

เสนอ slot ให้ลูกค้าจริง 2 ราย ขยาย list_free_slots ให้มี travel buffer 30 นาที (homestay → studio)

给两位真实客户提议时段。给 list_free_slots 加 30 分钟通勤缓冲(民宿 → 工作室)。