docs(calendar): mark plan as completed — all 12 units implemented
This commit is contained in:
parent
394d734d42
commit
d1250cf32b
|
|
@ -0,0 +1,196 @@
|
|||
---
|
||||
date: 2026-06-23
|
||||
topic: calendar-schedule
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
在 AgentKit 客户端右侧面板嵌入行事历(可展开为大抽屉),通过混合模式(ReAct 工具调用 + 时间关键词触发的后处理提取)自动捕获对话中的日程,同时支持完整的手动管理——循环事件、自定义类型、标签、三种视图(日历/卡片/列表),UI 对标 Notion Calendar。日程双向同步 Apple Calendar (CalDAV)、Outlook (Graph API) 和 iCal/ICS,配套独立的多渠道提醒子系统(客户端 + 邮件 + Webhook),个人为主 + 共享邀请。
|
||||
|
||||
## Problem Frame
|
||||
|
||||
Agent 对话中频繁产生可执行的时间安排——会议时间、截止日期、跟进任务——但这些信息当前无处记录。用户必须手动将这些内容转移到外部日历中,这个过程容易遗漏,尤其在长对话中多个时间点被讨论时。痛点不在"缺少一个日历应用",而在"Agent 产生的日程与日历之间没有桥梁":对话结束后,时间承诺就消失了。
|
||||
|
||||
## Key Decisions
|
||||
|
||||
**混合 Agent 识别模式。** ReAct 工具调用捕获实时意图(Agent 在对话中主动调用 `create_event`),后处理提取作为兜底——但仅在对话包含时间关键词时触发 LLM 提取,避免每轮对话都产生额外 LLM 调用。两层机制互补:工具调用覆盖 Agent 已识别的明确日程,后处理覆盖 Agent 未主动识别但对话中隐含的时间安排。
|
||||
|
||||
**右侧面板 + 大抽屉而非独立页面。** 日历紧贴对话上下文,用户不用切换页面就能看到即将到来的日程。需要全屏管理时展开为大抽屉。这牺牲了独立页面的全屏日历体验,但换来了日程与对话的紧密耦合——这是核心痛点的解法。
|
||||
|
||||
**提醒系统作为独立子系统。** 多渠道提醒(客户端推送 + 邮件 + Webhook)需要后台调度器、通知规则、渠道适配器、投递追踪——这不仅是日历的一个功能,而是 AgentKit 目前不存在的全新基础设施。作为独立子系统设计,未来可被其他功能复用。
|
||||
|
||||
**个人日历 + 共享邀请,不做团队日历。** 用户有自己的私人日历,可以邀请其他用户参加事件(类似 Google Calendar 的邀请),但不支持多用户共同编辑同一日历。团队日历是不同的产品形态,不在此次范围内。
|
||||
|
||||
## Actors
|
||||
|
||||
- A1. **终端用户** — 管理个人日历,手动创建/查看/编辑/删除事件,管理事件类型和标签,配置提醒规则
|
||||
- A2. **Agent** — 在 ReAct 循环中自主识别日程意图并调用日历工具创建事件;后处理提取层在时间关键词触发时分析对话内容
|
||||
- A3. **外部日历服务** — Apple Calendar (CalDAV)、Outlook (Microsoft Graph API)、iCal/ICS 通用格式,作为双向同步目标
|
||||
- A4. **提醒子系统** — 后台调度器定期扫描即将到期的事件,根据提醒规则通过配置的渠道分发通知
|
||||
|
||||
## Requirements
|
||||
|
||||
### 事件数据模型
|
||||
|
||||
- R1. 事件包含标题、描述、开始时间、结束时间、全天标记、地点字段
|
||||
- R2. 事件支持循环模式:每日、每周、每月、自定义 RRULE 规则(至少支持 INTERVAL、COUNT、UNTIL、BYDAY)
|
||||
- R3. 事件支持自定义事件类型——用户可创建带名称和颜色的分类(如"会议"蓝色、"截止"红色),事件创建时选择类型
|
||||
- R4. 事件支持多标签——用户可自定义标签,单个事件可挂多个标签,支持按标签筛选
|
||||
- R5. 事件支持时间段——开始时间 + 结束时间,不限于时间点;全天事件和跨天事件均支持
|
||||
|
||||
### 视图模式
|
||||
|
||||
- R6. 日历视图——月/周/日网格布局,支持拖拽创建(拖选时间段直接建事件)、拖拽移动(拖动事件改变时间)
|
||||
- R7. 卡片视图——看板风格,按日期或事件类型分组,卡片可拖拽调整分组
|
||||
- R8. 列表视图——按时间排序的列表,支持行内编辑和批量操作
|
||||
- R9. 三种视图在右侧面板和大抽屉中均可切换使用
|
||||
|
||||
### Agent 自动识别
|
||||
|
||||
- R10. Agent 在 ReAct 循环中可调用日历工具集:`create_event`、`query_events`、`update_event`、`delete_event`
|
||||
- R11. 后处理提取在对话轮次结束后运行,但仅当对话内容包含时间关键词时触发——关键词列表包括但不限于:明天、下周、今天下午、X点、X月X日、开会、截止、deadline、schedule、reminder、提醒、预约
|
||||
- R12. 时间关键词检测使用正则/关键词匹配(零 LLM 调用),命中后才发起 LLM 提取请求
|
||||
- R13. Agent 创建的事件标记 `source="agent"`,并关联到 originating 对话 ID,可在日历中追溯来源
|
||||
- R14. 后处理提取创建的事件标记 `source="post_extract"`,同样关联对话 ID
|
||||
- R15. 用户可在 UI 中区分手动创建、Agent 创建、后处理提取的事件(通过来源标记或颜色区分)
|
||||
|
||||
### 手动管理
|
||||
|
||||
- R16. 用户可通过 UI 手动创建、编辑、删除事件——三种视图均支持创建入口
|
||||
- R17. 日历视图支持拖拽改期——拖动事件到新时间位置即可调整开始/结束时间
|
||||
- R18. 事件类型和标签可在事件编辑界面中设置和修改
|
||||
- R19. 支持批量操作:多选事件后批量删除、批量修改类型/标签
|
||||
|
||||
### 外部日历同步
|
||||
|
||||
- R20. 与 Apple Calendar 通过 CalDAV 协议双向同步——在 AgentKit 创建/修改/删除的事件同步到 Apple Calendar,反之亦然
|
||||
- R21. 与 Outlook Calendar 通过 Microsoft Graph API 双向同步——同上双向
|
||||
- R22. 支持 iCal/ICS 格式的导入和导出——可从 .ics 文件导入事件,可将日历导出为 .ics
|
||||
- R23. 同步冲突采用 last-write-wins 策略,冲突时保留最后修改的版本,并向用户发送冲突通知
|
||||
- R24. 用户可在设置中配置每个外部日历的同步频率和同步范围(哪些事件类型参与同步)
|
||||
|
||||
### 提醒子系统
|
||||
|
||||
- R25. 事件支持提醒规则——可配置多个提醒(如"提前15分钟"、"提前1天"),每个提醒指定渠道
|
||||
- R26. 提醒渠道支持:客户端内通知(WebSocket 推送 / Tauri 系统通知)、邮件、Webhook
|
||||
- R27. 后台调度器定期扫描即将到期的事件,匹配提醒规则,通过配置的渠道分发通知
|
||||
- R28. 用户可为每种事件类型配置默认提醒规则——新建事件时自动继承
|
||||
- R29. 提醒投递状态可追踪(已发送/已读/失败),失败的提醒有重试机制
|
||||
|
||||
### 共享与邀请
|
||||
|
||||
- R30. 用户可邀请其他用户参加事件——通过用户名或邮箱搜索并邀请
|
||||
- R31. 被邀请用户收到通知,可接受/拒绝/暂定
|
||||
- R32. 事件创建者可查看邀请回复状态(谁接受、谁拒绝、谁未回复)
|
||||
- R33. 被邀请用户的事件在其日历中以特殊样式标记(如半透明或边框区分)
|
||||
|
||||
### UI/UX
|
||||
|
||||
- R34. 日历嵌入右侧面板作为标签页(与 Workflow/Monitor 并列),展示今日/近期日程摘要
|
||||
- R35. 右侧面板日历可展开为大抽屉——覆盖大部分屏幕,提供完整的三视图管理体验
|
||||
- R36. UI 遵循 Notion Calendar 设计语言——简洁排版、充足留白、流畅拖拽、柔和配色
|
||||
- R37. Agent 创建事件时,右侧面板实时更新(无需手动刷新),并有视觉提示(如高亮动画)
|
||||
|
||||
## Key Flows
|
||||
|
||||
- F1. Agent 工具调用创建日程
|
||||
- **Trigger:** Agent 在 ReAct 循环中识别到对话内容包含明确的时间安排
|
||||
- **Actors:** A2 (Agent), A1 (用户)
|
||||
- **Steps:** Agent 调用 `create_event` 工具,传入标题/时间/类型等参数 → 工具执行写入 → 返回创建结果 → Agent 在回复中告知用户已创建日程 → 右侧面板实时更新显示新事件
|
||||
- **Outcome:** 事件被创建并标记 `source="agent"`,用户在对话中收到确认
|
||||
- **Covers:** R10, R13, R37
|
||||
|
||||
- F2. 后处理提取创建日程
|
||||
- **Trigger:** 对话轮次结束,对话内容命中时间关键词正则
|
||||
- **Actors:** A2 (Agent 后处理层), A1 (用户)
|
||||
- **Steps:** 关键词检测命中 → 发起 LLM 提取请求,分析对话中的时间安排 → LLM 返回提取结果(0 或多个事件) → 写入日历,标记 `source="post_extract"` → 在对话中插入轻量提示"检测到 N 条日程,已添加到日历"
|
||||
- **Outcome:** 隐含的日程被捕获,用户收到提示可查看/修改
|
||||
- **Covers:** R11, R12, R14
|
||||
|
||||
- F3. 外部日历双向同步
|
||||
- **Trigger:** 定时同步任务触发,或用户手动触发同步
|
||||
- **Actors:** A3 (外部日历服务), A1 (用户)
|
||||
- **Steps:** 拉取外部日历变更 → 与本地事件比对 → 检测冲突 → last-write-wins 解决冲突 → 推送本地变更到外部日历 → 冲突时向用户发送通知
|
||||
- **Outcome:** 本地与外部日历保持一致,冲突被记录和通知
|
||||
- **Covers:** R20, R21, R23
|
||||
|
||||
- F4. 提醒分发
|
||||
- **Trigger:** 后台调度器扫描到事件即将到达提醒时间点
|
||||
- **Actors:** A4 (提醒子系统), A1 (用户)
|
||||
- **Steps:** 调度器匹配提醒规则 → 按规则配置的渠道分发通知(客户端推送 / 邮件 / Webhook) → 记录投递状态 → 失败时重试
|
||||
- **Outcome:** 用户在事件前通过配置的渠道收到提醒
|
||||
- **Covers:** R25, R26, R27, R29
|
||||
|
||||
- F5. 手动创建事件
|
||||
- **Trigger:** 用户在任意视图中触发创建操作(点击新建、拖选时间段、列表中添加)
|
||||
- **Actors:** A1 (用户)
|
||||
- **Steps:** 打开事件编辑表单 → 填写标题/时间/类型/标签/提醒/地点 → 保存 → 事件出现在日历中 → 如配置了外部同步,异步推送到外部日历
|
||||
- **Outcome:** 事件被创建并可管理
|
||||
- **Covers:** R16, R18
|
||||
|
||||
## Acceptance Examples
|
||||
|
||||
- AE1. Agent 在对话中识别日程
|
||||
- **Covers:** R10, R13, R15
|
||||
- **Given:** 用户与 Agent 对话,用户说"下周三下午3点开个产品评审会"
|
||||
- **When:** Agent 在 ReAct 循环中处理该输入
|
||||
- **Then:** Agent 调用 `create_event` 创建事件(标题="产品评审会", 开始时间=下周三15:00, source="agent"),并在回复中告知"已为您创建下周三下午3点的产品评审会日程"
|
||||
|
||||
- AE2. 后处理提取被关键词门控
|
||||
- **Covers:** R11, R12, R14
|
||||
- **Given:** 用户与 Agent 对话,对话内容为"这个方案不错,我们继续优化吧"
|
||||
- **When:** 对话轮次结束,后处理层检查时间关键词
|
||||
- **Then:** 关键词检测未命中(无时间相关词),不发起 LLM 提取请求,不创建任何事件
|
||||
|
||||
- AE3. 后处理提取在关键词命中时运行
|
||||
- **Covers:** R11, R12, R14
|
||||
- **Given:** 用户与 Agent 对话,对话内容为"好的,那下周二之前把文档发给我"
|
||||
- **When:** 对话轮次结束,后处理层检查时间关键词
|
||||
- **Then:** 关键词"下周二"命中 → 发起 LLM 提取 → 创建事件(标题="发送文档", 截止时间=下周二, source="post_extract")→ 对话中插入提示"检测到 1 条日程,已添加到日历"
|
||||
|
||||
- AE4. 同步冲突解决
|
||||
- **Covers:** R23
|
||||
- **Given:** 用户在 AgentKit 修改了事件 A(改到周三),同时在 Outlook 也修改了事件 A(改到周四),同步时检测到冲突
|
||||
- **When:** 同步任务运行
|
||||
- **Then:** 保留最后修改的版本(last-write-wins),向用户发送冲突通知"事件 A 存在同步冲突,已保留最新修改"
|
||||
|
||||
- AE5. 提醒按渠道分发
|
||||
- **Covers:** R25, R26, R27
|
||||
- **Given:** 事件 B 配置了两个提醒:提前30分钟客户端通知 + 提前1天邮件通知
|
||||
- **When:** 调度器扫描到事件 B 的提前1天提醒时间点到达
|
||||
- **Then:** 通过邮件渠道发送提醒 → 记录投递状态为"已发送" → 继续等待提前30分钟的客户端通知时间点
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### Deferred for later
|
||||
|
||||
- Google Calendar 集成——用户未选择,可在后续版本按需添加
|
||||
- 团队共享日历——多用户共同编辑同一日历,当前仅支持个人 + 邀请
|
||||
- 日历分析报表——事件统计、时间分布分析等
|
||||
- 资源预订——会议室、设备等资源预约
|
||||
- 移动端独立 App——当前仅 Web/Tauri 客户端
|
||||
|
||||
### Outside this product's identity
|
||||
|
||||
- 替代专业日历应用——AgentKit 行事历的核心差异化是 Agent 自动识别,不是与 Google Calendar/Notion Calendar 全功能竞争
|
||||
- 项目管理——行事历不承担任务跟踪、甘特图、依赖管理等项目管理职能
|
||||
|
||||
## Dependencies / Assumptions
|
||||
|
||||
- 复用现有认证系统(`src/agentkit/server/auth/models.py` 的 `UserModel`)作为用户身份基础
|
||||
- 复用现有 WebSocket 基础设施实现客户端实时通知推送
|
||||
- 新增依赖:后台调度器(如 APScheduler)用于提醒子系统
|
||||
- 新增依赖:CalDAV 客户端库用于 Apple Calendar 同步
|
||||
- 新增依赖:Microsoft Graph SDK 或直接 HTTP 客户端用于 Outlook 同步
|
||||
- 新增依赖:邮件发送能力(SMTP 或第三方服务)用于邮件提醒渠道
|
||||
- 假设:用户已拥有 Apple Calendar / Outlook 账号并可提供 OAuth/CalDAV 凭据
|
||||
- 假设:AgentKit 服务端可持续运行后台调度器进程(提醒子系统依赖)
|
||||
|
||||
## Sources / Research
|
||||
|
||||
- 现有架构落位参考:路由注册在 `src/agentkit/server/app.py`(第 928-954 行),前端布局在 `src/agentkit/server/frontend/src/components/layout/AgentLayout.vue`(第 146-156 行定义象限标签)
|
||||
- 前端 Pinia stores 位于 `src/agentkit/server/frontend/src/stores/`,API 客户端位于 `src/agentkit/server/frontend/src/api/`
|
||||
- 后端路由模块位于 `src/agentkit/server/routes/`(27 个 .py 文件),工具注册在 `src/agentkit/tools/registry.py`
|
||||
- 用户模型位于 `src/agentkit/server/auth/models.py`(SQLAlchemy 2 + SQLite),可复用 `Base` 创建日历模型
|
||||
- 消息总线 `src/agentkit/bus/message.py` 的 `AgentMessage` 可用于提醒子系统的内部通信
|
||||
- DocumentTool(`src/agentkit/tools/document_tool.py`)可作为 Agent 工具集成的参考模式——工具注册、ReAct 循环调用、结果返回
|
||||
|
|
@ -0,0 +1,722 @@
|
|||
---
|
||||
title: Calendar & Schedule Feature
|
||||
status: completed
|
||||
date: 2026-06-23
|
||||
origin: docs/brainstorms/2026-06-23-calendar-schedule-requirements.md
|
||||
type: feature
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
在 AgentKit 客户端嵌入行事历,通过混合模式(ReAct 工具调用 + 时间关键词触发的后处理提取)自动捕获对话中的日程,支持完整的手动管理——循环事件、自定义类型、标签、三种视图(日历/卡片/列表),双向同步 Apple Calendar (CalDAV)、Outlook (Graph API) 和 iCal/ICS,配套独立的多渠道提醒子系统。
|
||||
|
||||
## Problem Frame
|
||||
|
||||
Agent 对话中频繁产生可执行的时间安排,但这些信息当前无处记录。痛点不在"缺少一个日历应用",而在"Agent 产生的日程与日历之间没有桥梁"。本计划实现这架桥梁:Agent 工具调用 + 后处理提取 + 手动管理 + 外部同步 + 提醒分发,形成完整的日程闭环。
|
||||
|
||||
**Scope: Deep** — 跨前端 UI、后端 API、Agent 工具、外部同步、提醒子系统五大领域。
|
||||
|
||||
## Requirements Traceability
|
||||
|
||||
| Requirement Group | R-IDs | Covered by Units |
|
||||
|---|---|---|
|
||||
| 事件数据模型 | R1-R5 | U1, U2 |
|
||||
| 视图模式 | R6-R9 | U10 (KTD-12: R9 = summary + drawer) |
|
||||
| Agent 自动识别 | R10-R15 | U3, U4 (R15 source distinction: U10) |
|
||||
| 手动管理 | R16-R19 | U10, U11 |
|
||||
| 外部日历同步 | R20-R24 | U6, U7, U8 (SyncManager: U6) |
|
||||
| 提醒子系统 | R25-R29 | U5, U12 |
|
||||
| 共享与邀请 | R30-R33 | U2, U11 (R30 user search: U2, R33 invited styling: U10) |
|
||||
| UI/UX | R34-R37 | U10, U11 |
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
**KTD-1. Mirror documents subsystem for backend structure.** 日历后端镜像 `src/agentkit/documents/` 的结构——`calendar/db.py` (aiosqlite bare-connection)、`calendar/models.py` (dataclass DTO)、`calendar/service.py` (business logic)、`calendar/tool.py` (agent tool)。这是代码库中最近验证过的模式,与现有架构一致,避免引入 SQLAlchemy engine/session 的复杂性。Rationale: aiosqlite bare-connection 已在 auth 和 documents 子系统中验证,轻量且与异步架构兼容。
|
||||
|
||||
**KTD-2. asyncio loop for reminder scheduler, not APScheduler.** 提醒调度器跟随 `src/agentkit/server/task_store.py` 的 `start()`/`stop()` + `asyncio.create_task` 循环模式,在 `app.py` lifespan 中启停。Rationale: 不引入新依赖,与现有后台任务模式一致(task_store cleanup loop、session writer loop)。`ponytail: asyncio.sleep 轮询的精度是秒级,如果未来需要亚秒级调度可升级到 APScheduler。` **Note:** 需求文档提到"新增依赖:后台调度器(如 APScheduler)",本决策选择不引入 APScheduler 而复用 asyncio loop 模式——这是有意识的偏离,因为 task_store 已验证此模式可行,且避免新依赖。
|
||||
|
||||
**KTD-3. python-dateutil.rrule for recurrence expansion.** 使用 `dateutil.rrule` 处理 RRULE 规则展开(RFC 5545 兼容),支持 FREQ/INTERVAL/COUNT/UNTIL/BYDAY。Rationale: 标准库 `datetime` 不支持循环规则,dateutil 是 Python 生态中唯一成熟的 RRULE 实现,且是 icalendar 库的依赖。
|
||||
|
||||
**KTD-4. caldav library for Apple Calendar, httpx for Outlook Graph API.** Apple Calendar 同步使用 `caldav` 库(CalDAV 协议),Outlook 同步直接用 `httpx` 调用 Microsoft Graph REST API(不引入 msgraph SDK)。Rationale: caldav 库是 Python 生态中唯一成熟的 CalDAV 客户端;msgraph SDK 过重(数百 MB),httpx 已在依赖中,Graph REST API 简单直接。
|
||||
|
||||
**KTD-5. icalendar library for ICS import/export.** 使用 `icalendar` 库生成和解析 .ics 文件。Rationale: 手写 iCalendar 格式容易出错(编码、转义、时区),icalendar 库是事实标准。
|
||||
|
||||
**KTD-6. FullCalendar Vue3 for calendar grid views.** 前端日历网格视图(月/周/日)使用 `@fullcalendar/vue3`,样式定制为 Notion Calendar 风格。Rationale: 从零实现带拖拽的月/周/日网格是数千行代码,FullCalendar 是成熟标准库,支持拖拽创建/移动、循环事件展开、视图切换。`ponytail: FullCalendar 是本计划中唯一的大型前端依赖,如果后续需要移除,日历网格组件是独立的可替换层。`
|
||||
|
||||
**KTD-7. Post-processing hook in chat.py after assistant reply persistence.** 后处理提取钩子插入 `src/agentkit/server/routes/chat.py` 的 `_handle_chat_message()` 中,在 assistant 回复持久化之后(REACT 路径约 line 1158,DIRECT_CHAT 路径约 line 971)触发。先做零 LLM 的正则关键词检测,命中后才调用 LLM 提取。Rationale: 这是对话轮次结束的自然插入点,不改变现有流程,仅在回复完成后追加一个异步后台任务。
|
||||
|
||||
**KTD-8. aiosmtplib for email reminder channel.** 邮件提醒渠道使用 `aiosmtplib`(异步 SMTP 客户端)。Rationale: smtplib 是同步的会阻塞事件循环,aiosmtplib 是标准异步替代品。
|
||||
|
||||
**KTD-9. Reuse existing LLM gateway for post-processing extraction.** 后处理提取的 LLM 调用复用 `agentkit.llm` 网关,不新建 LLM 客户端。Rationale: LLM 网关已有 fallback、缓存、用量追踪,直接复用。
|
||||
|
||||
**KTD-10. WebSocket piggyback for real-time calendar push.** 日历实时更新(Agent 创建事件、提醒触发、邀请通知、同步冲突)复用现有 chat WebSocket 连接,新增 `calendar_event_created`、`calendar_reminder`、`calendar_invitation`、`calendar_sync_conflict` 消息类型。Rationale: 避免新建 WS 连接,前端 chat store 已有消息分发机制。
|
||||
|
||||
**KTD-11. UTC storage + local display timezone strategy.** 所有时间字段(`start_time`、`end_time`、`scheduled_time`、`last_modified`、`created_at`)以 ISO 8601 UTC 存储在数据库中,前端显示时由浏览器自动转换为本地时区。RRULE 展开基于 UTC 计算,显示时再转本地。Rationale: 统一存储避免时区歧义,浏览器 `Intl.DateTimeFormat` 或 dayjs UTC 插件处理显示转换。`ponytail: 不支持用户手动选择时区——浏览器本地时区即显示时区,跨时区用户需自行调整设备时区。`
|
||||
|
||||
**KTD-12. Right panel = summary view, drawer = full management.** 右面板日历 tab 仅展示摘要视图(今日 + 未来 3 条事件),点击"展开"按钮打开 80% 宽度的大抽屉,抽屉内包含完整的三视图管理(日历网格/卡片/列表)+ 事件编辑器 + 邀请管理。Rationale: 右面板空间有限,摘要视图满足"快速查看"需求;完整管理功能需要大画布,抽屉模式不离开当前页面上下文。
|
||||
|
||||
## Architecture
|
||||
|
||||
### Module Map
|
||||
|
||||
```
|
||||
src/agentkit/calendar/ # New subsystem (mirrors documents/)
|
||||
__init__.py
|
||||
models.py # @dataclass: CalendarEvent, EventType, Tag, Reminder, ReminderRule, ExternalCalendarConfig, Invitation
|
||||
db.py # aiosqlite: init_calendar_db(), CRUD functions
|
||||
service.py # CalendarService: business logic, event CRUD, type/tag management
|
||||
recurrence.py # RRULE expansion wrapper (dateutil.rrule)
|
||||
scheduler.py # ReminderScheduler: start/stop asyncio loop, scan + dispatch
|
||||
reminders.py # ReminderDispatcher: client push, email, webhook channels
|
||||
extraction.py # PostProcessingExtractor: keyword gate + LLM extraction
|
||||
sync/
|
||||
__init__.py
|
||||
base.py # AbstractSyncProvider interface
|
||||
caldav_provider.py # Apple Calendar sync via caldav library
|
||||
outlook_provider.py # Outlook sync via httpx + Graph API
|
||||
ics_provider.py # ICS import/export via icalendar library
|
||||
manager.py # SyncManager: orchestrate providers, conflict resolution
|
||||
|
||||
src/agentkit/tools/calendar_tool.py # CalendarTool(Tool): create_event, query_events, update_event, delete_event
|
||||
|
||||
src/agentkit/server/routes/calendar.py # APIRouter(prefix="/calendar"): REST endpoints
|
||||
|
||||
src/agentkit/server/frontend/src/
|
||||
api/calendar.ts # CalendarApiClient extends BaseApiClient
|
||||
stores/calendar.ts # useCalendarStore (Pinia Composition API)
|
||||
components/calendar/
|
||||
CalendarPanel.vue # Right panel tab content (summary view only — KTD-12)
|
||||
CalendarDrawer.vue # Large drawer wrapper (full management — KTD-12)
|
||||
CalendarGrid.vue # FullCalendar wrapper (month/week/day)
|
||||
CardView.vue # Kanban-style card view
|
||||
ListView.vue # Sorted list view
|
||||
EventBadge.vue # Source distinction badge (manual/agent/post_extract — G3)
|
||||
EventEditor.vue # Event create/edit form (a-drawer)
|
||||
ReminderConfig.vue # Reminder rule configuration
|
||||
SyncSettings.vue # External calendar sync config
|
||||
InvitationManager.vue # Invitation response UI
|
||||
views/CalendarView.vue # Top-level route page (optional, for drawer mode)
|
||||
```
|
||||
|
||||
### Wiring Points
|
||||
|
||||
| Integration | File | Location |
|
||||
|---|---|---|
|
||||
| DB init + service | `src/agentkit/server/app.py` | lifespan startup (~line 263, next to `init_documents_db()`) |
|
||||
| Route registration | `src/agentkit/server/app.py` | ~line 954, `app.include_router(calendar.router, prefix="/api/v1")` |
|
||||
| Tool registration | `src/agentkit/server/app.py` | ~line 268, `agent._tool_registry.register(CalendarTool(...))` |
|
||||
| Scheduler start | `src/agentkit/server/app.py` | lifespan startup (~line 151, next to `task_store.start_cleanup()`) |
|
||||
| Scheduler stop | `src/agentkit/server/app.py` | lifespan shutdown (~line 430) |
|
||||
| SyncManager start | `src/agentkit/server/app.py` | lifespan startup (next to reminder scheduler) |
|
||||
| SyncManager stop | `src/agentkit/server/app.py` | lifespan shutdown (next to reminder scheduler stop) |
|
||||
| Post-processing hook | `src/agentkit/server/routes/chat.py` | ~line 1158 (REACT), ~line 971 (DIRECT_CHAT) |
|
||||
| WebSocket push | `src/agentkit/server/routes/chat.py` | `chat_manager.send_json(session_id, {...})` |
|
||||
| Frontend right panel tab | `src/agentkit/server/frontend/src/components/layout/AgentLayout.vue` | `bottomRightTabs` array (~line 153) |
|
||||
| Frontend WS handler | `src/agentkit/server/frontend/src/stores/chat.ts` | `handleWsMessage()` switch (~line 586) |
|
||||
| Frontend WS types | `src/agentkit/server/frontend/src/api/types.ts` | `WsServerMessage` union (~line 111) |
|
||||
|
||||
### Data Model
|
||||
|
||||
```python
|
||||
# src/agentkit/calendar/models.py (dataclasses, mirrors documents/models.py)
|
||||
|
||||
@dataclass
|
||||
class EventType:
|
||||
id: str # UUID
|
||||
user_id: str # FK to users
|
||||
name: str # "会议", "截止", "个人"
|
||||
color: str # "#4A90D9"
|
||||
is_default: bool # system-provided types
|
||||
|
||||
@dataclass
|
||||
class Tag:
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
class CalendarEvent:
|
||||
id: str
|
||||
user_id: str
|
||||
title: str
|
||||
description: str
|
||||
start_time: str # ISO 8601 UTC (see KTD-11)
|
||||
end_time: str # ISO 8601 UTC
|
||||
is_all_day: bool
|
||||
location: str
|
||||
event_type_id: str | None
|
||||
rrule: str | None # RFC 5545 RRULE string, e.g. "FREQ=WEEKLY;BYDAY=MO;COUNT=10"
|
||||
source: str # "manual" | "agent" | "post_extract"
|
||||
is_invited: bool # True if this event arrived via invitation (R33 — special styling)
|
||||
conversation_id: str | None # origin traceability for agent/extracted events
|
||||
external_id: str | None # ID in external calendar (for sync)
|
||||
external_provider: str | None # "caldav" | "outlook"
|
||||
last_modified: str # ISO 8601 UTC, for conflict resolution (last-write-wins)
|
||||
created_at: str # ISO 8601 UTC
|
||||
|
||||
@dataclass
|
||||
class EventTag: # many-to-many junction
|
||||
event_id: str
|
||||
tag_id: str
|
||||
|
||||
@dataclass
|
||||
class ReminderRule:
|
||||
id: str
|
||||
event_id: str # FK to events (nullable for type-level defaults)
|
||||
event_type_id: str # FK to event_types (for default reminders)
|
||||
offset_minutes: int # -15 = 15 min before, -1440 = 1 day before
|
||||
channels: list[str] # ["client", "email", "webhook"]
|
||||
|
||||
@dataclass
|
||||
class ReminderDelivery:
|
||||
id: str
|
||||
reminder_rule_id: str
|
||||
event_id: str
|
||||
scheduled_time: str
|
||||
status: str # "pending" | "sent" | "failed" | "read"
|
||||
channel: str
|
||||
attempts: int
|
||||
last_error: str | None
|
||||
|
||||
@dataclass
|
||||
class ExternalCalendarConfig:
|
||||
id: str
|
||||
user_id: str
|
||||
provider: str # "caldav" | "outlook"
|
||||
credentials: str # encrypted JSON (CalDAV URL+user+app_password, or OAuth refresh_token)
|
||||
sync_frequency: int # minutes
|
||||
sync_scope: list[str] # event type IDs to sync, empty = all
|
||||
last_sync: str | None
|
||||
sync_token: str | None # delta token for incremental sync
|
||||
|
||||
@dataclass
|
||||
class Invitation:
|
||||
id: str
|
||||
event_id: str
|
||||
inviter_user_id: str
|
||||
invitee_email: str
|
||||
status: str # "pending" | "accepted" | "declined" | "tentative"
|
||||
responded_at: str | None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Units
|
||||
|
||||
### U1. Backend data model & storage
|
||||
|
||||
- **Goal:** Define calendar dataclasses, aiosqlite DB schema with CRUD functions, and RRULE recurrence expansion wrapper.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/__init__.py` (new)
|
||||
- `src/agentkit/calendar/models.py` (new — dataclasses above)
|
||||
- `src/agentkit/calendar/db.py` (new — `init_calendar_db()`, `insert_event()`, `get_event()`, `list_events()`, `update_event()`, `delete_event()`, same for types/tags/reminders/configs/invitations)
|
||||
- `src/agentkit/calendar/recurrence.py` (new — `expand_rrule(rrule_str, start_time, range_start, range_end) -> list[str]` wrapper around `dateutil.rrule`)
|
||||
- **Patterns:** Mirror `src/agentkit/documents/db.py` (aiosqlite bare-connection, `PRAGMA journal_mode=WAL`, module-level async functions). Mirror `src/agentkit/documents/models.py` for dataclass DTO pattern. `recurrence.py` wraps `dateutil.rrule.rrulestr()` + `between()` for date-range expansion.
|
||||
- **DB schema:** 7 tables — `calendar_events`, `calendar_event_types`, `calendar_tags`, `calendar_event_tags`, `calendar_reminder_rules`, `calendar_reminder_deliveries`, `calendar_external_configs`, `calendar_invitations`. All with `String(36)` UUIDs, ISO 8601 UTC timestamps (see KTD-11), indexes on `user_id` and `start_time`.
|
||||
- **RRULE expansion (G1, G9):** `recurrence.py` provides `expand_rrule(rrule_str, dtstart, range_start, range_end)` returning a list of ISO 8601 UTC occurrence strings within the range. Used by: (a) `CalendarService.list_events()` to expand recurring events for display, (b) `ReminderScheduler` to find occurrences entering reminder window, (c) sync providers to push individual occurrences if external calendar requires expanded events. All times in UTC per KTD-11.
|
||||
- **Test file:** `tests/unit/calendar/test_db.py`, `tests/unit/calendar/test_recurrence.py`
|
||||
- **Test scenarios:**
|
||||
- `test_init_calendar_db_creates_all_tables` — call `init_calendar_db()`, verify tables exist
|
||||
- `test_insert_and_get_event_roundtrip` — insert event, fetch by id, all fields preserved (including `is_invited`)
|
||||
- `test_list_events_by_user_filtered_by_date_range` — insert 3 events, query range covering 2
|
||||
- `test_update_event_modifies_fields` — insert, update title, verify
|
||||
- `test_delete_event_removes_record` — insert, delete, verify gone
|
||||
- `test_event_type_crud` — create/list/update/delete event types
|
||||
- `test_tag_many_to_many` — event with 3 tags, query by tag returns event
|
||||
- `test_reminder_rule_crud` — create rule for event, create default rule for type
|
||||
- `test_external_config_stores_encrypted_credentials` — insert config, verify credentials field is opaque
|
||||
- `test_expand_rrule_weekly_count` — `FREQ=WEEKLY;BYDAY=MO;COUNT=4` from Monday → 4 occurrences
|
||||
- `test_expand_rrule_daily_range_filter` — `FREQ=DAILY` starting Jan 1, range Jan 3–Jan 5 → 3 occurrences
|
||||
- `test_expand_rrule_until_clause` — `FREQ=DAILY;UNTIL=20260131` → occurrences stop at Jan 31
|
||||
- `test_expand_rrule_no_rrule_returns_single` — `rrule=None` → returns `[start_time]` only
|
||||
- `test_expand_rrule_all_day_event` — all-day event with RRULE, verify date (not datetime) expansion
|
||||
- `test_expand_rrule_dst_boundary` — event crossing DST transition, verify UTC consistency
|
||||
- **Dependencies:** None (foundation unit)
|
||||
|
||||
### U2. Backend calendar service & REST API
|
||||
|
||||
- **Goal:** Business logic layer + REST endpoints for full calendar CRUD, event types, tags, invitations, and non-admin user search for invitations.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/service.py` (new — `CalendarService` class)
|
||||
- `src/agentkit/server/routes/calendar.py` (new — `APIRouter(prefix="/calendar", tags=["calendar"])`)
|
||||
- **Patterns:** Mirror `src/agentkit/documents/service.py` (service class with injected dependencies) and `src/agentkit/server/routes/documents.py` (route module with `_verify_api_key` or `require_authenticated` dependency, Pydantic request/response models, `request.app.state.calendar_service` access).
|
||||
- **Service methods:** `create_event()`, `get_event()`, `list_events()` (with RRULE expansion via `recurrence.expand_rrule`), `update_event()`, `delete_event()`, `list_event_types()`, `create_event_type()`, `update_event_type()`, `list_tags()`, `create_tag()`, `create_invitation()`, `respond_to_invitation()`, `list_invitations()`, `search_users(q)` (returns `[{username, email}]` for invitation picker — G5/A3).
|
||||
- **REST endpoints:**
|
||||
- `POST /api/v1/calendar/events` — create event
|
||||
- `GET /api/v1/calendar/events?start=...&end=...&type_id=...&tag_id=...` — list with filters (tag_id filter covers G2)
|
||||
- `GET /api/v1/calendar/events/{id}` — get single
|
||||
- `PATCH /api/v1/calendar/events/{id}` — update
|
||||
- `DELETE /api/v1/calendar/events/{id}` — delete
|
||||
- `POST /api/v1/calendar/events/{id}/invitations` — invite user by email (creates Invitation + pushes `calendar_invitation` WS to invitee — G6)
|
||||
- `POST /api/v1/calendar/invitations/{id}/respond` — accept/decline/tentative
|
||||
- `GET /api/v1/calendar/invitations` — list pending invitations for current user
|
||||
- `GET /api/v1/calendar/users/search?q=xxx` — non-admin user search by username/email, returns `[{username, email}]` only (no user_id leak) — G5/A3. Auth: any authenticated user.
|
||||
- `GET /api/v1/calendar/event-types` — list types
|
||||
- `POST /api/v1/calendar/event-types` — create type
|
||||
- `PATCH /api/v1/calendar/event-types/{id}` — update type
|
||||
- `GET /api/v1/calendar/tags` — list tags
|
||||
- `POST /api/v1/calendar/tags` — create tag
|
||||
- **User search implementation (G5/A3):** `search_users(q)` queries `users` table (from `src/agentkit/server/auth/models.py` `UserModel`) by `username ILIKE '%q%' OR email ILIKE '%q%'`, returns top 10 results as `[{username, email}]`. Does NOT require admin permission — uses standard authenticated user dependency. Existing `GET /api/v1/admin/users` requires admin and has no username/email search, so this new endpoint is necessary.
|
||||
- **Invitation notification (G6):** When `create_invitation()` is called, after persisting the `Invitation` record, push `calendar_invitation` WS message to the invitee's active session(s) via `chat_manager.send_json()`. Invitee frontend shows a notification + updates invitation list.
|
||||
- **Wiring:** `app.py` lifespan — `await init_calendar_db()`, `app.state.calendar_service = CalendarService()`, `app.include_router(calendar.router, prefix="/api/v1")`.
|
||||
- **Test file:** `tests/unit/calendar/test_service.py`, `tests/unit/calendar/test_routes.py`
|
||||
- **Test scenarios:**
|
||||
- `test_create_event_with_type_and_tags` — create event with type_id and 2 tags, verify all linked
|
||||
- `test_list_events_filters_by_date_range` — 3 events across days, filter returns correct subset
|
||||
- `test_list_events_filters_by_type_and_tag` — filter combination (G2)
|
||||
- `test_list_events_filters_by_tag_only` — 5 events, 2 with tag X, filter tag_id=X returns only 2 (G2)
|
||||
- `test_list_events_expands_recurring` — event with `FREQ=DAILY;COUNT=3`, list range covering 2 days → returns 2 expanded occurrences
|
||||
- `test_update_event_partial_fields` — PATCH only title, other fields unchanged
|
||||
- `test_delete_event_cascades_reminders_and_tags` — delete event, verify reminder rules and junction rows removed
|
||||
- `test_create_invitation_pushes_ws_to_invitee` — create invitation, verify `chat_manager.send_json()` called with `calendar_invitation` type (G6)
|
||||
- `test_respond_to_invitation_updates_status` — invite, respond "accepted", verify status + `responded_at` set
|
||||
- `test_search_users_by_username` — seed 3 users, search "alice" returns matching user with username+email only (G5)
|
||||
- `test_search_users_by_email` — search by email domain returns matches (G5)
|
||||
- `test_search_users_no_match_returns_empty` — search "zzz" returns `[]`
|
||||
- `test_search_users_requires_auth` — no auth header → 401
|
||||
- `test_search_users_returns_max_10` — seed 15 matching users, verify only 10 returned
|
||||
- `test_event_type_default_color_persistence` — create type with color, verify roundtrip
|
||||
- `test_route_create_event_requires_auth` — no auth header → 401
|
||||
- `test_route_list_events_returns_paginated` — large result set with limit/offset
|
||||
- **Dependencies:** U1
|
||||
|
||||
### U3. Agent calendar tool (ReAct integration)
|
||||
|
||||
- **Goal:** CalendarTool for ReAct loop — Agent autonomously calls `create_event`, `query_events`, `update_event`, `delete_event`.
|
||||
- **Files:**
|
||||
- `src/agentkit/tools/calendar_tool.py` (new — `CalendarTool(Tool)`)
|
||||
- **Patterns:** Mirror `src/agentkit/tools/document_tool.py` — extend `Tool` ABC, define `input_schema` as JSON Schema dict, `execute(**kwargs)` dispatches by `action` parameter, returns `{"success": True/False, ...}` dict, errors caught not raised.
|
||||
- **Tool actions:**
|
||||
- `create_event` — params: title, start_time, end_time, description, location, event_type_name, tag_names, rrule, is_all_day
|
||||
- `query_events` — params: start_date, end_date, limit
|
||||
- `update_event` — params: event_id, fields to update
|
||||
- `delete_event` — params: event_id
|
||||
- **Source marking:** All events created via tool set `source="agent"` and `conversation_id` from execution context.
|
||||
- **Wiring:** `app.py` ~line 268 — `agent._tool_registry.register(CalendarTool(service=calendar_service))`.
|
||||
- **Test file:** `tests/unit/tools/test_calendar_tool.py`
|
||||
- **Test scenarios:**
|
||||
- `test_create_event_action_returns_success` — call execute with create_event action, verify event in DB with source="agent"
|
||||
- `test_create_event_with_recurrence_sets_rrule` — rrule param stored correctly
|
||||
- `test_query_events_returns_list` — create 2 events, query, verify both returned
|
||||
- `test_update_event_action_modifies_fields` — create then update title
|
||||
- `test_delete_event_action_removes_record` — create then delete
|
||||
- `test_invalid_action_returns_error` — unknown action → `{"success": False, "error": "..."}`
|
||||
- `test_missing_required_field_returns_error` — create_event without title → error
|
||||
- `test_created_event_has_conversation_id` — verify conversation_id is set from context
|
||||
- **Dependencies:** U2
|
||||
|
||||
### U4. Post-processing extraction (keyword gating + LLM)
|
||||
|
||||
- **Goal:** After conversation turn, detect time keywords (zero-LLM regex), if hit then call LLM to extract schedule info, create events with `source="post_extract"`.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/extraction.py` (new — `PostProcessingExtractor` class)
|
||||
- **Patterns:** Keyword regex follows the `_TOOL_CONTEXT_RE` / greeting regex pattern in `src/agentkit/chat/request_preprocessor.py`. LLM call via `agentkit.llm` gateway (reuse existing `LLMGateway.complete()` or `acomplete()`).
|
||||
- **Keyword regex:** `明天|后天|下周|本周|今天下午|今天上午|上午|下午|晚上|\d+点|\d+月\d+日|\d+号|开会|截止|deadline|schedule|reminder|提醒|预约|约定|安排`
|
||||
- **LLM extraction prompt:** System prompt instructs LLM to extract events as JSON array `[{title, start_time, end_time, description, event_type}]` from conversation text. Empty array if no events.
|
||||
- **Flow:**
|
||||
1. Hook fires after assistant reply persisted in `chat.py` (~line 1158 REACT, ~line 971 DIRECT_CHAT)
|
||||
2. `asyncio.create_task(extractor.extract(conversation_text, conversation_id, user_id))`
|
||||
3. Regex keyword scan — if no match, return immediately (zero LLM cost)
|
||||
4. If match, call LLM gateway with extraction prompt
|
||||
5. Parse JSON response, create events with `source="post_extract"`
|
||||
6. Push `calendar_event_created` WS message to client
|
||||
- **Wiring:** `chat.py` — after `sm.append_message()` for assistant reply, call `asyncio.create_task(post_extractor.extract(...))`. Extractor instance on `app.state.post_extractor`.
|
||||
- **Test file:** `tests/unit/calendar/test_extraction.py`
|
||||
- **Test scenarios:**
|
||||
- `test_keyword_regex_matches_chinese_time_words` — "明天下午3点" matches, "继续优化吧" doesn't
|
||||
- `test_keyword_regex_matches_english_time_words` — "deadline tomorrow" matches
|
||||
- `test_no_keyword_skips_llm_call` — mock LLM gateway, verify `.complete()` never called when no keyword
|
||||
- `test_keyword_hit_triggers_llm_extraction` — mock LLM returns `[{title: "会议", start_time: "..."}]`, verify event created with source="post_extract"
|
||||
- `test_llm_returns_empty_array_creates_nothing` — LLM returns `[]`, no events created
|
||||
- `test_malformed_llm_response_handled_gracefully` — LLM returns invalid JSON, no crash, logged
|
||||
- `test_extracted_events_have_conversation_id` — verify conversation_id set
|
||||
- `test_extraction_does_not_block_chat_response` — verify extract is async, chat reply returns before extraction completes
|
||||
- **Dependencies:** U2, U3 (for source marking pattern)
|
||||
|
||||
### U5. Reminder subsystem (scheduler + multi-channel dispatch)
|
||||
|
||||
- **Goal:** Background scheduler scans upcoming events, matches reminder rules, dispatches via client push / email / webhook, tracks delivery status with retry.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/scheduler.py` (new — `ReminderScheduler` class)
|
||||
- `src/agentkit/calendar/reminders.py` (new — `ReminderDispatcher` class)
|
||||
- **Patterns:** Scheduler follows `src/agentkit/server/task_store.py` `start()`/`stop()` + `asyncio.create_task` loop pattern. Dispatcher uses strategy pattern — one method per channel.
|
||||
- **Scheduler loop:**
|
||||
1. Every 60 seconds: query events where `start_time + offset_minutes` is within next 60s window
|
||||
2. For each match, check if `ReminderDelivery` already exists (idempotency)
|
||||
3. If not, create `ReminderDelivery` records (one per channel) and dispatch
|
||||
4. Failed deliveries retried up to 3 times with exponential backoff
|
||||
- **Dispatcher channels:**
|
||||
- `client` — `chat_manager.send_json(session_id, {"type": "calendar_reminder", "data": {...}})`
|
||||
- `email` — `aiosmtplib.send()` with SMTP config from `agentkit.yaml`
|
||||
- `webhook` — `httpx.post()` to user-configured webhook URL
|
||||
- **Default reminders:** When event created, if its `event_type_id` has default `ReminderRule`s, clone them to the event.
|
||||
- **Wiring:** `app.py` lifespan startup — `reminder_scheduler = ReminderScheduler(calendar_service, dispatcher); app.state.reminder_scheduler = reminder_scheduler; await reminder_scheduler.start()`. Shutdown — `await reminder_scheduler.stop()`.
|
||||
- **Test file:** `tests/unit/calendar/test_scheduler.py`, `tests/unit/calendar/test_reminders.py`
|
||||
- **Test scenarios:**
|
||||
- `test_scheduler_finds_event_within_reminder_window` — event 10 min away, rule offset -15min, scheduler finds it
|
||||
- `test_scheduler_skips_event_outside_window` — event 2 hours away, rule offset -15min, not found
|
||||
- `test_idempotent_delivery_no_duplicate` — scheduler runs twice, only one delivery record created
|
||||
- `test_client_channel_sends_ws_message` — mock chat_manager, verify send_json called
|
||||
- `test_email_channel_sends_smtp` — mock aiosmtplib, verify send called with correct params
|
||||
- `test_webhook_channel_posts_to_url` — mock httpx, verify POST called
|
||||
- `test_failed_delivery_retries_up_to_3_times` — mock channel to fail, verify 3 attempts
|
||||
- `test_default_reminders_inherited_from_event_type` — create event with type that has default rules, verify rules cloned
|
||||
- `test_scheduler_start_stop_lifecycle` — start(), verify task running, stop(), verify cancelled
|
||||
- **Dependencies:** U1, U2
|
||||
|
||||
### U6. External sync — CalDAV (Apple Calendar)
|
||||
|
||||
- **Goal:** Bidirectional sync with Apple Calendar via CalDAV protocol using `caldav` library, plus SyncManager orchestration.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/sync/base.py` (new — `AbstractSyncProvider` interface: `pull_changes()`, `push_changes()`, `test_connection()`)
|
||||
- `src/agentkit/calendar/sync/caldav_provider.py` (new — `CalDAVSyncProvider`)
|
||||
- `src/agentkit/calendar/sync/manager.py` (new — `SyncManager` class — G8)
|
||||
- **Patterns:** Provider interface allows future providers (Google Calendar) without changing SyncManager. CalDAV connection uses `caldav.DAVClient(url)` with user Apple ID + app-specific password.
|
||||
- **SyncManager (G8):** `SyncManager` orchestrates all providers. Methods: `sync_all(user_id)` (iterates user's `ExternalCalendarConfig`s, calls provider pull/push), `sync_provider(config_id)` (single provider), `resolve_conflict(local_event, remote_event)` (last-write-wins + WS notification). Called by: (a) scheduled sync (asyncio loop, same pattern as reminder scheduler), (b) manual "Sync Now" button, (c) after local event creation/update (push to external). `SyncManager` is registered on `app.state.sync_manager` and started/stopped in `app.py` lifespan.
|
||||
- **Sync flow:**
|
||||
1. `pull_changes()` — `calendar.date_search(start, end)` to fetch events in range, match by `external_id` (CalDAV UID), create/update local events
|
||||
2. `push_changes()` — for local events modified since `last_sync`, `calendar.save_event(ical_data)` to push to CalDAV
|
||||
3. Conflict: `last_modified` comparison, last-write-wins, log conflict + push `calendar_sync_conflict` WS notification to user (G4)
|
||||
- **ICS generation:** Use `icalendar` library to convert `CalendarEvent` → iCalendar VEVENT (with RRULE if present).
|
||||
- **Config:** `ExternalCalendarConfig` stores CalDAV URL, username, app-specific password (encrypted). User configures via settings UI.
|
||||
- **Test file:** `tests/unit/calendar/test_sync_caldav.py`, `tests/unit/calendar/test_sync_manager.py`
|
||||
- **Test scenarios:**
|
||||
- `test_caldav_provider_pull_creates_local_events` — mock caldav client returning 2 events, verify local events created with external_id set
|
||||
- `test_caldav_provider_pull_updates_existing_event` — event already exists (matched by external_id), remote modified, local updated
|
||||
- `test_caldav_provider_push_creates_remote_event` — local event with no external_id, push creates remote, external_id stored
|
||||
- `test_caldav_provider_push_updates_remote_event` — local event modified, push updates remote
|
||||
- `test_caldav_conflict_last_write_wins` — both sides modified, newer last_modified wins, conflict logged
|
||||
- `test_caldav_conflict_sends_ws_notification` — conflict detected, verify `chat_manager.send_json()` called with `calendar_sync_conflict` type (G4)
|
||||
- `test_caldav_rrule_roundtrip` — event with RRULE synced, verify RRULE preserved in both directions
|
||||
- `test_caldav_test_connection_success` — mock returns calendars, test_connection() returns True
|
||||
- `test_caldav_test_connection_failure` — mock raises, test_connection() returns False with error message
|
||||
- `test_sync_manager_sync_all_iterates_providers` — 2 configs, verify both providers called (G8)
|
||||
- `test_sync_manager_sync_provider_updates_last_sync` — sync completes, verify config.last_sync updated (G8)
|
||||
- `test_sync_manager_resolve_conflict_notifies_user` — conflict, verify WS push (G8/G4)
|
||||
- **Dependencies:** U2, U8 (ICS generation)
|
||||
|
||||
### U7. External sync — Outlook (Microsoft Graph API)
|
||||
|
||||
- **Goal:** Bidirectional sync with Outlook Calendar via Microsoft Graph REST API using `httpx`.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/sync/outlook_provider.py` (new — `OutlookSyncProvider`)
|
||||
- **Patterns:** Same `AbstractSyncProvider` interface. OAuth2 flow — user authorizes via browser, app stores refresh token, auto-refreshes access token. Graph API endpoints: `GET /me/calendarView/delta` for incremental pull, `POST /me/events` for create, `PATCH /me/events/{id}` for update.
|
||||
- **Delta sync:** Use Graph API delta query (`$deltaToken` / `$skipToken`) for incremental changes — first call is full sync, subsequent calls are incremental.
|
||||
- **OAuth flow:**
|
||||
1. User clicks "Connect Outlook" in settings UI
|
||||
2. Redirect to Microsoft login with `Calendars.ReadWrite` scope
|
||||
3. Callback receives authorization code, exchanges for access + refresh token
|
||||
4. Tokens stored in `ExternalCalendarConfig.credentials` (encrypted JSON)
|
||||
5. On token expiry (401), auto-refresh using refresh token
|
||||
- **Config:** `ExternalCalendarConfig` stores OAuth client_id, refresh_token, access_token (with expiry). Client ID configured in `agentkit.yaml`.
|
||||
- **Test file:** `tests/unit/calendar/test_sync_outlook.py`
|
||||
- **Test scenarios:**
|
||||
- `test_outlook_provider_pull_delta_initial_sync` — mock httpx returning 3 events, verify local events created
|
||||
- `test_outlook_provider_pull_delta_incremental` — mock returning 1 new + 1 updated, verify only those processed
|
||||
- `test_outlook_provider_push_creates_remote_event` — local event pushed, remote ID stored
|
||||
- `test_outlook_provider_push_updates_remote_event` — local modification pushed
|
||||
- `test_outlook_token_refresh_on_401` — mock 401 response, verify refresh token used, request retried
|
||||
- `test_outlook_conflict_last_write_wins` — both sides modified, newer wins
|
||||
- `test_outlook_rrule_roundtrip` — recurring event synced, pattern preserved
|
||||
- `test_outlook_test_connection_success` — mock Graph /me returns user, True
|
||||
- **Dependencies:** U2, U8 (ICS for RRULE conversion)
|
||||
|
||||
### U8. iCal/ICS import/export
|
||||
|
||||
- **Goal:** Import .ics files to local calendar, export local calendar to .ics file, using `icalendar` library.
|
||||
- **Files:**
|
||||
- `src/agentkit/calendar/sync/ics_provider.py` (new — `ICSProvider`)
|
||||
- **Patterns:** `icalendar.Calendar.from_ical(ics_bytes)` for import, `icalendar.Calendar()` + `add_component()` for export. Map VEVENT fields to `CalendarEvent` dataclass.
|
||||
- **Import flow:** Parse ICS → for each VEVENT, create `CalendarEvent` (map SUMMARY→title, DTSTART→start_time, RRULE→rrule, etc.) → skip events with duplicate UID already imported.
|
||||
- **Export flow:** Query events in date range → for each, create VEVENT component → assemble into Calendar → serialize to .ics bytes → return as downloadable file.
|
||||
- **REST endpoints** (in `routes/calendar.py`):
|
||||
- `POST /api/v1/calendar/import-ics` — upload .ics file, parse, create events
|
||||
- `GET /api/v1/calendar/export-ics?start=...&end=...` — download .ics file
|
||||
- **Test file:** `tests/unit/calendar/test_ics_provider.py`
|
||||
- **Test scenarios:**
|
||||
- `test_import_simple_ics_creates_event` — single VEVENT ICS, verify event created with correct fields
|
||||
- `test_import_recurring_ics_preserves_rrule` — VEVENT with RRULE, verify rrule field set
|
||||
- `test_import_all_day_event` — DTSTART is date not datetime, verify is_all_day=True
|
||||
- `test_import_skips_duplicate_uid` — import same ICS twice, second import creates no new events
|
||||
- `test_export_produces_valid_ics` — create event, export, parse result with icalendar, verify roundtrip
|
||||
- `test_export_includes_recurrence` — event with rrule, export, verify RRULE in ICS
|
||||
- `test_export_date_range_filter` — 3 events, export range covering 2, verify only 2 in ICS
|
||||
- `test_import_malformed_ics_raises_error` — invalid ICS bytes, verify graceful error
|
||||
- **Dependencies:** U2
|
||||
|
||||
### U9. Frontend store, API client & types
|
||||
|
||||
- **Goal:** Pinia store, API client, and TypeScript types for calendar feature.
|
||||
- **Files:**
|
||||
- `src/agentkit/server/frontend/src/api/calendar.ts` (new — `CalendarApiClient extends BaseApiClient`, types co-located)
|
||||
- `src/agentkit/server/frontend/src/stores/calendar.ts` (new — `useCalendarStore` Composition API)
|
||||
- `src/agentkit/server/frontend/src/api/types.ts` (modify — add `calendar_event_created`, `calendar_reminder`, `calendar_invitation`, `calendar_sync_conflict` to `WsServerMessage` union)
|
||||
- **Patterns:** API client mirrors `src/agentkit/server/frontend/src/api/documents.ts` (extend `BaseApiClient`, singleton export, types co-located, runtime type guard `isCalendarEvent`). Store mirrors `src/agentkit/server/frontend/src/stores/settings.ts` (Composition API, `ref()` state, `computed()` getters, async actions with try/catch + `isLoading`).
|
||||
- **API client methods:** `listEvents()`, `createEvent()`, `updateEvent()`, `deleteEvent()`, `listEventTypes()`, `createEventType()`, `listTags()`, `createTag()`, `importIcs()`, `exportIcs()`, `createInvitation()`, `respondToInvitation()`, `listInvitations()`, `searchUsers(q)` (G5/A3), `listExternalConfigs()`, `createExternalConfig()`, `testExternalConnection()`, `syncNow(configId)`.
|
||||
- **Store state:** `events`, `eventTypes`, `tags`, `isLoading`, `error`, `selectedEvent`, `viewMode` ('calendar'|'card'|'list'), `dateRange`, `pendingInvitations`, `syncConflicts`.
|
||||
- **Store actions:** `loadEvents()`, `createEvent()`, `updateEvent()`, `deleteEvent()`, `loadEventTypes()`, `loadTags()`, `setViewMode()`, `loadInvitations()`, `respondToInvitation()`, `searchUsers(q)`, `handleWsEvent()` (dispatch for `calendar_event_created`, `calendar_reminder`, `calendar_invitation`, `calendar_sync_conflict` WS messages).
|
||||
- **WS integration:** `stores/chat.ts` `handleWsMessage()` — add `case 'calendar_event_created'`, `case 'calendar_reminder'`, `case 'calendar_invitation'`, `case 'calendar_sync_conflict'` branches that call into calendar store (lazy import pattern to avoid circular deps).
|
||||
- **Test file:** `tests/unit/frontend/calendar/store.test.ts` (if frontend test infra exists; otherwise manual verification via `npm run typecheck`)
|
||||
- **Test scenarios:**
|
||||
- Type check: `npm run typecheck` passes with new types
|
||||
- `test_loadEvents_populates_state` — mock API, call loadEvents, verify events ref populated
|
||||
- `test_createEvent_adds_to_state` — call createEvent, verify events array updated
|
||||
- `test_handleWsEvent_calendar_event_created` — call handleWsEvent with calendar_event_created payload, verify event added to state
|
||||
- `test_handleWsEvent_calendar_reminder` — call handleWsEvent with reminder payload, verify notification shown
|
||||
- `test_handleWsEvent_calendar_invitation` — call handleWsEvent with invitation payload, verify pendingInvitations updated + notification shown (G6)
|
||||
- `test_handleWsEvent_calendar_sync_conflict` — call handleWsEvent with conflict payload, verify syncConflicts updated + alert shown (G4)
|
||||
- `test_searchUsers_returns_results` — call searchUsers("ali"), verify results populated (G5)
|
||||
- `test_viewMode_switch` — setViewMode('card'), verify state
|
||||
- **Dependencies:** U2 (API contract must exist)
|
||||
|
||||
### U10. Frontend calendar views (3 views + right panel tab + drawer)
|
||||
|
||||
- **Goal:** Three view modes (calendar grid / card / list), right panel tab integration (summary only), expandable drawer (full management), with event source distinction and invited-event styling.
|
||||
- **Files:**
|
||||
- `src/agentkit/server/frontend/src/components/calendar/CalendarPanel.vue` (new — right panel tab, summary view only per KTD-12)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue` (new — large drawer wrapper, full management per KTD-12)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue` (new — FullCalendar wrapper)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/CardView.vue` (new — kanban card view)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/ListView.vue` (new — sorted list view)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/EventBadge.vue` (new — source distinction badge/icon component — G3)
|
||||
- `src/agentkit/server/frontend/src/components/layout/AgentLayout.vue` (modify — add `calendar` to `bottomRightTabs`)
|
||||
- `src/agentkit/server/frontend/src/components/layout/tabs/CalendarTab.vue` (new — tab content wrapper)
|
||||
- `package.json` (modify — add `@fullcalendar/vue3`, `@fullcalendar/daygrid`, `@fullcalendar/timegrid`, `@fullcalendar/interaction` deps)
|
||||
- **Patterns:** Right panel tab follows existing `QuadrantPanel` tab pattern (add to `bottomRightTabs`, add named slot). Drawer follows `a-drawer` pattern from `SkillDetail.vue` (but wider, ~80% viewport). FullCalendar initialized in `CalendarGrid.vue` with Vue3 wrapper.
|
||||
- **Event source distinction (G3, R15):** Each event displays a visual indicator of its origin:
|
||||
- `manual` — no badge (default)
|
||||
- `agent` — small robot icon (🤖 or `RobotOutlined`) in event card/grid item
|
||||
- `post_extract` — small sparkle icon (✨ or `ThunderboltOutlined`) in event card/grid item
|
||||
- `EventBadge.vue` component renders the appropriate icon based on `event.source` field, used in CalendarGrid/CardView/ListView
|
||||
- **Invited event styling (G7, R33):** Events with `is_invited=True` render with a distinct visual treatment:
|
||||
- Dashed border instead of solid
|
||||
- Subtle background tint (e.g., light blue overlay)
|
||||
- "受邀" tag displayed next to title
|
||||
- Clicking an invited event opens InvitationManager (respond UI) instead of EventEditor
|
||||
- **Timezone display (KTD-11):** All times received from API are UTC ISO 8601. Frontend converts to local timezone for display using `dayjs(utcTime).local()` or native `Intl.DateTimeFormat`. No user-facing timezone selector — browser local timezone is used.
|
||||
- **CalendarPanel (right panel tab — summary only per KTD-12):**
|
||||
- Shows today's events + next 3 upcoming events as compact list
|
||||
- Each item shows time + title + source badge (G3)
|
||||
- "Expand" button opens `CalendarDrawer`
|
||||
- Visual highlight animation when new event created (via WS event trigger)
|
||||
- Does NOT include view mode switcher, event editor, or full calendar grid — those live in the drawer
|
||||
- **CalendarDrawer (large drawer — full management per KTD-12):**
|
||||
- `a-drawer` with `width="80%"` and `placement="right"`
|
||||
- View mode switcher (calendar/card/list) at top
|
||||
- Contains `CalendarGrid`, `CardView`, or `ListView` based on selected mode
|
||||
- "New Event" button opens `EventEditor` (U11)
|
||||
- Invitation management accessible via tab/section (U11)
|
||||
- **CalendarGrid (FullCalendar):**
|
||||
- Month/Week/Day views via FullCalendar
|
||||
- Drag-to-create (select time range → open EventEditor)
|
||||
- Drag-to-move (drag event to new time → call updateEvent)
|
||||
- Events colored by `event_type.color`
|
||||
- Recurring events expanded by backend (U2 `list_events` with RRULE expansion) — FullCalendar receives pre-expanded occurrences
|
||||
- Invited events (is_invited=True) styled with dashed border class (G7)
|
||||
- Styled with Notion Calendar aesthetic: clean borders, ample whitespace, soft colors
|
||||
- **CardView:**
|
||||
- Kanban-style, grouped by date or event type (toggle)
|
||||
- Cards draggable between groups (changes date or type)
|
||||
- Each card shows source badge (G3) and invited styling (G7)
|
||||
- **ListView:**
|
||||
- Sorted by start_time, supports inline edit of title, batch select with checkbox
|
||||
- Source column with badge (G3)
|
||||
- **Wiring:** `AgentLayout.vue` — add `{ key: 'calendar', label: '日历', icon: CalendarOutlined }` to `bottomRightTabs`, add `<template #calendar><CalendarTab /></template>` slot.
|
||||
- **Test file:** Manual verification + `npm run typecheck`
|
||||
- **Test scenarios:**
|
||||
- Type check passes
|
||||
- Right panel shows calendar tab with today's events (summary only — no grid)
|
||||
- Each event in panel shows correct source badge (manual=none, agent=robot, post_extract=sparkle) (G3)
|
||||
- Clicking "Expand" opens drawer with 80% width
|
||||
- Drawer contains view mode switcher + full calendar grid
|
||||
- Calendar grid shows month view by default, can switch to week/day
|
||||
- Drag-select time range opens event editor
|
||||
- Drag event to new time updates event
|
||||
- Invited events show dashed border + "受邀" tag in all views (G7)
|
||||
- Clicking invited event opens invitation response UI, not editor (G7)
|
||||
- Card view groups by date, cards draggable, source badge visible (G3)
|
||||
- List view sorted by time, inline edit works, source column visible (G3)
|
||||
- New event from Agent triggers highlight animation in panel
|
||||
- Times displayed in browser local timezone (KTD-11)
|
||||
- **Dependencies:** U9
|
||||
|
||||
### U11. Frontend event editor & management UI
|
||||
|
||||
- **Goal:** Event create/edit form, drag-and-drop rescheduling, batch operations, invitation management.
|
||||
- **Files:**
|
||||
- `src/agentkit/server/frontend/src/components/calendar/EventEditor.vue` (new — `a-drawer` width 560)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/InvitationManager.vue` (new — invitation list + respond buttons)
|
||||
- **Patterns:** Event editor follows `a-drawer` + form pattern from `SkillDetail.vue`. Form uses Ant Design Vue form components (`a-form`, `a-input`, `a-date-picker`, `a-time-picker`, `a-select`, `a-tag`).
|
||||
- **EventEditor fields:**
|
||||
- Title (a-input, required)
|
||||
- Description (a-textarea)
|
||||
- Date range (a-range-picker) + time (a-time-picker, hidden if all-day)
|
||||
- All-day toggle (a-switch)
|
||||
- Location (a-input)
|
||||
- Event type (a-select, colored options)
|
||||
- Tags (a-select mode="tags", user can create new)
|
||||
- Recurrence (a-select: none/daily/weekly/monthly/custom + custom RRULE builder)
|
||||
- Reminders (list of offset + channel selectors, add/remove)
|
||||
- **Batch operations:** In ListView, checkbox multi-select → toolbar with "Delete", "Change Type", "Add Tag" buttons.
|
||||
- **InvitationManager:** Shows list of invitees with status icons, "Invite" button opens user search dialog (calls `GET /api/v1/calendar/users/search?q=xxx` from U2 — G5/A3), invitee view shows accept/decline/tentative buttons. Also displays incoming invitations (received via `calendar_invitation` WS push from U2 — G6) with response buttons.
|
||||
- **Test file:** Manual verification + `npm run typecheck`
|
||||
- **Test scenarios:**
|
||||
- Type check passes
|
||||
- Create event with all fields, verify saved
|
||||
- Edit existing event, verify changes persisted
|
||||
- Set recurrence weekly, verify RRULE generated
|
||||
- Add 2 reminders (15min client + 1day email), verify saved
|
||||
- Batch select 3 events, delete, verify removed
|
||||
- Batch select, change type, verify all updated
|
||||
- Invite user by email, verify invitation created (G5)
|
||||
- User search dialog returns results from `/api/v1/calendar/users/search` (G5)
|
||||
- Invitation response (accept) updates status
|
||||
- Incoming invitation notification displays when `calendar_invitation` WS received (G6)
|
||||
- **Dependencies:** U9, U10
|
||||
|
||||
### U12. Frontend reminder & external sync settings UI
|
||||
|
||||
- **Goal:** Reminder notification display, external calendar connection settings, sync status.
|
||||
- **Files:**
|
||||
- `src/agentkit/server/frontend/src/components/calendar/ReminderConfig.vue` (new — default reminder rules per event type)
|
||||
- `src/agentkit/server/frontend/src/components/calendar/SyncSettings.vue` (new — external calendar config)
|
||||
- **Patterns:** Settings panels follow existing settings component patterns. Notification display uses Ant Design Vue `a-notification` or `a-message` component.
|
||||
- **ReminderConfig:**
|
||||
- Per event type: list of default reminder rules (offset + channels)
|
||||
- Add/remove rules, save to backend
|
||||
- When new event created with this type, rules auto-inherited
|
||||
- **SyncSettings:**
|
||||
- List of configured external calendars (provider, status, last_sync)
|
||||
- "Add Apple Calendar" → form for CalDAV URL + Apple ID + app-specific password
|
||||
- "Add Outlook" → OAuth redirect button
|
||||
- Per-provider: sync frequency, sync scope (which event types), "Sync Now" button, "Test Connection" button
|
||||
- Conflict notifications displayed as alerts when `calendar_sync_conflict` WS message received (G4) — shows event title, provider, conflict details, and resolution taken (last-write-wins)
|
||||
- **Notification display:**
|
||||
- `calendar_reminder` WS message → `a-notification.warning()` with event title + time
|
||||
- `calendar_invitation` WS message → `a-notification.info()` with inviter name + event title, link to respond (G6)
|
||||
- `calendar_sync_conflict` WS message → `a-alert` in SyncSettings panel showing conflict details (G4)
|
||||
- Tauri mode: also trigger system notification via Tauri notification API
|
||||
- **Test file:** Manual verification + `npm run typecheck`
|
||||
- **Test scenarios:**
|
||||
- Type check passes
|
||||
- Add default reminder rule to "会议" type, verify saved
|
||||
- Create event of type "会议", verify reminder inherited
|
||||
- Add Apple Calendar config, test connection (mock success)
|
||||
- Add Outlook config via OAuth (mock redirect)
|
||||
- "Sync Now" triggers sync, status updates
|
||||
- Reminder WS message displays notification
|
||||
- Invitation WS message displays notification with respond link (G6)
|
||||
- Conflict WS message displays alert in SyncSettings (G4)
|
||||
- **Dependencies:** U9, U10
|
||||
|
||||
---
|
||||
|
||||
## Key Flows
|
||||
|
||||
- F1. Agent tool-call creates event
|
||||
- **Trigger:** Agent identifies schedule in ReAct loop
|
||||
- **Actors:** A2 (Agent), A1 (User)
|
||||
- **Steps:** Agent calls `CalendarTool.execute(action="create_event", ...)` → service writes to DB with `source="agent"` → WS push `calendar_event_created` to client → right panel highlights new event → Agent tells user "已创建日程"
|
||||
- **Covers:** R10, R13, R37
|
||||
|
||||
- F2. Post-processing extraction (keyword-gated)
|
||||
- **Trigger:** Conversation turn ends, assistant reply persisted
|
||||
- **Actors:** A2 (post-processing layer), A1 (User)
|
||||
- **Steps:** `asyncio.create_task(extractor.extract(...))` → regex keyword scan → if no match, return (zero LLM) → if match, LLM extraction → parse JSON → create events with `source="post_extract"` → WS push → insert "检测到 N 条日程" hint in chat
|
||||
- **Covers:** R11, R12, R14
|
||||
|
||||
- F3. External calendar sync
|
||||
- **Trigger:** Scheduled sync task (every N minutes per provider config, run by `SyncManager`) or manual "Sync Now"
|
||||
- **Actors:** A3 (external service), A1 (User)
|
||||
- **Steps:** `SyncManager.sync(provider)` → `provider.pull_changes()` (fetch remote, match by external_id, create/update local) → `provider.push_changes()` (push local changes to remote) → conflict detection (last_modified comparison) → last-write-wins → `calendar_sync_conflict` WS notification to user (G4)
|
||||
- **Covers:** R20, R21, R23
|
||||
|
||||
- F4. Reminder dispatch
|
||||
- **Trigger:** Scheduler loop (every 60s) finds event entering reminder window
|
||||
- **Actors:** A4 (reminder subsystem), A1 (User)
|
||||
- **Steps:** Query events where `start_time + offset_minutes` in next 60s → check idempotency (delivery exists?) → create `ReminderDelivery` records → dispatch per channel (client WS / email SMTP / webhook HTTP) → update delivery status → retry on failure (up to 3x)
|
||||
- **Covers:** R25, R26, R27, R29
|
||||
|
||||
- F5. Manual event creation
|
||||
- **Trigger:** User clicks "New" or drag-selects time range in calendar view
|
||||
- **Actors:** A1 (User)
|
||||
- **Steps:** Open `EventEditor` drawer → fill form (title/time/type/tags/reminders) → save → `calendarApi.createEvent()` → store updates → event appears in view → if external sync configured, async push to external calendar
|
||||
- **Covers:** R16, R18
|
||||
|
||||
## Dependencies
|
||||
|
||||
### New Python dependencies (add to `pyproject.toml`)
|
||||
|
||||
| Package | Version | Used by |
|
||||
|---|---|---|
|
||||
| `python-dateutil>=2.9` | latest | U1 (RRULE expansion) |
|
||||
| `icalendar>=6.0` | latest | U6, U7, U8 (ICS format) |
|
||||
| `caldav>=1.3` | latest | U6 (Apple Calendar sync) |
|
||||
| `aiosmtplib>=3.0` | latest | U5 (email reminder channel) |
|
||||
|
||||
### New Node dependencies (add to `package.json`)
|
||||
|
||||
| Package | Used by |
|
||||
|---|---|
|
||||
| `@fullcalendar/vue3` | U10 (calendar grid) |
|
||||
| `@fullcalendar/daygrid` | U10 (month view) |
|
||||
| `@fullcalendar/timegrid` | U10 (week/day view) |
|
||||
| `@fullcalendar/interaction` | U10 (drag-and-drop) |
|
||||
|
||||
### Existing dependencies reused
|
||||
|
||||
- `httpx>=0.27` — Outlook Graph API calls (U7), webhook reminders (U5)
|
||||
- `aiosqlite>=0.20` — local storage (U1)
|
||||
- `pydantic>=2.0` — request/response models (U2)
|
||||
- `fastapi>=0.110` — REST routes (U2)
|
||||
- `redis>=5.0` — optional cache for sync tokens (U6, U7)
|
||||
|
||||
### Sequencing
|
||||
|
||||
```
|
||||
Phase 1 (Foundation): U1 → U2
|
||||
Phase 2 (Agent): U3, U4 (parallel, both depend on U2)
|
||||
Phase 3 (Reminders): U5 (depends on U1, U2)
|
||||
Phase 4 (External sync): U8 → U6, U7 (U6/U7 parallel, both depend on U8)
|
||||
Phase 5 (Frontend base): U9 (depends on U2 API contract)
|
||||
Phase 6 (Frontend UI): U10 → U11, U12 (U11/U12 parallel, both depend on U10)
|
||||
```
|
||||
|
||||
Units within the same phase can be developed in parallel. Phases are sequential — each phase's units depend on the prior phase's output.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|---|---|---|
|
||||
| FullCalendar bundle size impacts frontend load | Medium | Lazy-load calendar components (`defineAsyncComponent`), only load when tab activated |
|
||||
| CalDAV iCloud requires app-specific password (user friction) | Medium | Document setup steps in UI, provide "How to get app-specific password" link |
|
||||
| Outlook OAuth flow complexity in Tauri desktop mode | Medium | Use system browser for OAuth (not embedded webview), redirect to localhost callback |
|
||||
| Post-processing LLM extraction adds latency/cost | Medium | Keyword gate ensures LLM only called when time keywords present; extraction is async, doesn't block chat |
|
||||
| RRULE edge cases (DST, timezone, Feb 29) | Low | `dateutil.rrule` handles RFC 5545 edge cases; store all times in UTC, convert at display |
|
||||
| Sync conflict data loss | High | Last-write-wins with conflict notification; never auto-delete, always log |
|
||||
| Reminder scheduler misses events if server restarts | Medium | On startup, scan for events in reminder window that have no delivery records and dispatch them |
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### In scope (this plan)
|
||||
|
||||
- Personal calendar with manual + Agent + post-processing event creation
|
||||
- Three view modes (calendar grid, card, list)
|
||||
- Apple Calendar (CalDAV), Outlook (Graph API), iCal/ICS bidirectional sync
|
||||
- Multi-channel reminder subsystem (client push, email, webhook)
|
||||
- Event invitations with accept/decline/tentative
|
||||
- Right panel tab + expandable drawer UX
|
||||
|
||||
### Deferred for later
|
||||
|
||||
- Google Calendar integration (not selected by user, add CalDAV/Google API provider later)
|
||||
- Team shared calendars (multi-user co-editing same calendar)
|
||||
- Calendar analytics/reports (event statistics, time distribution)
|
||||
- Resource booking (meeting rooms, equipment)
|
||||
- Mobile native app (current is Web/Tauri only)
|
||||
- Calendar subscription (read-only iCal feed URL for others to subscribe)
|
||||
|
||||
### Outside this product's identity
|
||||
|
||||
- Replacing professional calendar applications (Google Calendar, Notion Calendar) — AgentKit calendar's differentiation is Agent auto-detection, not full-featured calendar competition
|
||||
- Project management (task tracking, Gantt charts, dependency management)
|
||||
Loading…
Reference in New Issue