Pārlūkot izejas kodu

Merge pull request #765 from Henry2513/feature/MeetingActionAgent

[毕业设计] MeetingActionAgent - 双智能体会议纪要助手
Sizhou Chen 1 nedēļu atpakaļ
vecāks
revīzija
2de2c140e0

+ 6 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/.env.example

@@ -0,0 +1,6 @@
+# 复制本文件为 .env,并填写 OpenAI-compatible 模型服务配置。
+# 不要提交真实 API 密钥。
+LLM_MODEL_ID=
+LLM_API_KEY=
+LLM_BASE_URL=
+LLM_TIMEOUT=60

+ 10 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/.gitignore

@@ -0,0 +1,10 @@
+.env
+.venv/
+__pycache__/
+*.py[cod]
+.pytest_cache/
+.coverage
+htmlcov/
+.ipynb_checkpoints/
+outputs/meeting_result.json
+outputs/meeting_result.md

+ 155 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/README.md

@@ -0,0 +1,155 @@
+# MeetingActionAgent
+
+> 使用两个 HelloAgents 智能体,把会议文字记录整理成可追溯、可执行的会议纪要。
+
+## 项目简介
+
+这是一个生产力工具类的双 Agent 小项目。范围比较简单:输入一段中文会议记录,然后完成下面四步:
+
+1. `MinutesAgent` 提取会议日期、摘要、决策、行动项和待确认问题。
+2. `ReviewAgent` 对照原文检查遗漏、编造和日期冲突。
+3. 审核未通过时,系统最多修正一次。
+4. 最终生成结构化 JSON 和易读的 Markdown 会议纪要。
+
+第一版有一条明确规则:原文没有的信息不猜。未知负责人或日期会显示为“未提供”。
+
+## 核心功能
+
+- 双 Agent 顺序协作:提取与审核职责分离
+- Pydantic 数据校验:及时发现缺字段和错误类型
+- 有限重试:最多四次模型调用,不会无限循环
+- 证据追溯:每个行动项保留对应原文
+
+## 工作流程
+
+```text
+会议文字记录
+  → MinutesAgent 生成结构化草稿
+  → ReviewAgent 对照原文审核
+  → 必要时修正并复核一次
+  → 保存 JSON 与 Markdown
+```
+
+## 项目结构
+
+```text
+Henry2513-MeetingActionAgent/
+├── README.md
+├── requirements.txt
+├── main.ipynb
+├── .env.example
+├── .gitignore
+├── data/
+│   ├── sample_meeting.txt
+│   └── edge_case_meeting.txt
+└── outputs/
+    ├── example_result.json
+    └── example_minutes.md
+```
+
+## 快速开始
+
+### 环境要求
+
+- Python 3.11+
+- 一个可用的 OpenAI-compatible LLM API
+
+### 1. 创建环境
+
+```powershell
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+python -m pip install -r requirements.txt
+```
+
+### 2. 配置模型
+
+复制 `.env.example` 为 `.env`,填写一个 OpenAI-compatible 模型服务:
+
+```dotenv
+LLM_MODEL_ID=your-model-id
+LLM_API_KEY=your-api-key
+LLM_BASE_URL=https://your-provider.example/v1
+```
+
+`.env` 已被 Git 忽略,禁止提交真实密钥。
+
+### 3. 运行 Notebook
+
+```powershell
+jupyter lab
+```
+
+打开 `main.ipynb`,确认内核使用当前项目的 `.venv`,然后从上到下运行。
+
+Notebook 会直接调用真实模型分析 `data/sample_meeting.txt`。
+请从 `Henry2513-MeetingActionAgent` 项目目录启动 Jupyter;Notebook 不兼容其他工作目录。
+
+## 输入与输出
+
+默认输入是 UTF-8 `.txt` 文件,也可以在 Notebook 中直接替换为粘贴的文本。
+
+输出包含:
+
+- `MeetingResult` JSON:包含会议日期、摘要、决策、行动项和待确认问题
+- Markdown 会议纪要:便于人阅读和分享
+- 审核状态:`passed` 或 `needs_manual_review`
+
+## 使用示例与测试
+
+- `data/sample_meeting.txt`:正常会议,包含明确任务和负责人。
+- `data/edge_case_meeting.txt`:包含模糊建议、日期冲突和缺失负责人。
+- Notebook 自检覆盖 JSON 代码围栏、缺失可选字段、空行动项、空输入和非法优先级。
+- `outputs/` 中的文件是正常会议的示例结果,可用于理解输出格式。
+
+## 技术栈
+
+- Python 3.11+
+- HelloAgents 0.2.9
+- Pydantic 2
+- JupyterLab
+
+## 项目亮点
+
+- 提取与审核分开执行,ReviewAgent 必须回到会议原文逐项核对。
+- 每个行动项保留原文证据,方便人工确认结果是否可靠。
+- 整个流程最多调用模型四次,并且只允许一次审核后修正。
+
+## 性能评估
+
+本项目是入门规模的流程验证,没有进行大规模准确率基准测试。提交前使用 `data/sample_meeting.txt` 完成了一次真实运行:
+
+- 共调用模型 2 次,最终审核状态为 `passed`。
+- 提取 4 个行动项和 1 个已确认决策。
+- 4 条行动项证据都能在会议原文中找到。
+- Notebook 的 6 组离线自检全部通过。
+
+实际响应时间会受到所选模型和 API 服务状态影响,因此不在这里给出固定数值。
+
+## 当前限制
+
+- 只处理文字记录,不处理录音或实时会议。
+- 不搜索互联网,也不使用数据库、RAG、MCP 或长期记忆。
+- LLM 输出存在不确定性;`needs_manual_review` 的结果必须人工确认。
+- 示例数据均为虚构内容,不应输入敏感会议数据。
+
+## 未来计划
+
+- [ ] 增加日期标准化工具,同时保留原文日期用于核对。
+- [ ] 支持连续处理多份会议记录并分别保存结果。
+
+## 贡献指南
+
+欢迎通过 Issue 或 Pull Request 提出问题和改进建议。
+
+## 许可证
+
+遵循 Hello-Agents 仓库的 CC BY-NC-SA 4.0 License。
+
+## 作者
+
+- GitHub: [@Henry2513](https://github.com/Henry2513)
+
+## 致谢
+
+感谢 Datawhale 社区和 Hello-Agents 项目提供的教程与框架。

+ 8 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/data/edge_case_meeting.txt

@@ -0,0 +1,8 @@
+会议主题:移动端发布时间讨论
+会议日期:2026-07-27
+参会者:王宁、赵可、孙然
+
+王宁:可以考虑下个月上线,但这只是一个初步想法。
+赵可:测试环境需要有人确认,目前还没有负责人。
+孙然:会议前的记录写产品文档周三完成,但今天讨论时又有人说周五完成。
+王宁:等测试环境确定后,我们再决定最终发布时间。

+ 9 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/data/sample_meeting.txt

@@ -0,0 +1,9 @@
+会议主题:新用户注册功能迭代
+会议日期:2026-07-27
+参会者:林晓、周明、陈悦
+
+林晓:注册页面和表单校验由我负责,7月31日前完成。
+周明:后端注册接口我来做,截止8月3日。
+陈悦:我会在接口完成后执行联调测试,但测试环境地址还没有确定。
+周明:决定邮箱验证码首版有效期设为5分钟。
+林晓:产品说明文档需要补充异常流程,负责人会后确认。

+ 560 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/main.ipynb

@@ -0,0 +1,560 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "4384048b",
+   "metadata": {},
+   "source": [
+    "# MeetingActionAgent:双智能体会议纪要助手\n",
+    "\n",
+    "> 输入会议文字记录,由 MinutesAgent 提取纪要,再由 ReviewAgent 对照原文审核。\n",
+    "\n",
+    "作者:[@Henry2513](https://github.com/Henry2513)\n",
+    "\n",
+    "日期:2026-08-04\n",
+    "\n",
+    "**适合读者**\n",
+    "- 第一次学习 Agent 或 HelloAgents 的开发者\n",
+    "- 希望理解“生成 Agent + 审核 Agent”协作方式的学习者\n",
+    "\n",
+    "**前置条件**\n",
+    "- Python 3.11+\n",
+    "- 已安装 `requirements.txt`\n",
+    "- 真实运行时需要一个 OpenAI-compatible LLM API\n",
+    "\n",
+    "**学习目标**\n",
+    "- 使用两个 `SimpleAgent` 顺序协作\n",
+    "- 用 Pydantic 校验模型返回的 JSON\n",
+    "- 限制重试次数并生成 JSON、Markdown 两种结果\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e05efd5f",
+   "metadata": {},
+   "source": [
+    "## 学习路线\n",
+    "\n",
+    "1. 加载环境与项目路径\n",
+    "2. 定义会议纪要数据结构\n",
+    "3. 解析并渲染结构化结果\n",
+    "4. 创建 MinutesAgent 和 ReviewAgent\n",
+    "5. 编排提取、审核和一次修正\n",
+    "6. 运行真实双 Agent 流程和自检\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5e843101",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from __future__ import annotations\n",
+    "\n",
+    "import json\n",
+    "from pathlib import Path\n",
+    "from typing import Literal\n",
+    "\n",
+    "from dotenv import load_dotenv\n",
+    "from hello_agents import HelloAgentsLLM, SimpleAgent\n",
+    "from pydantic import BaseModel, Field, ValidationError\n",
+    "\n",
+    "PROJECT_ROOT = Path.cwd()\n",
+    "load_dotenv(PROJECT_ROOT / \".env\")\n",
+    "print(f\"项目目录: {PROJECT_ROOT}\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0877f32d",
+   "metadata": {},
+   "source": [
+    "## 1. 双 Agent 架构\n",
+    "\n",
+    "```text\n",
+    "会议文字记录\n",
+    "  → MinutesAgent:只提取原文支持的信息\n",
+    "  → ReviewAgent:检查遗漏、编造和冲突\n",
+    "  → 必要时修正并复核一次\n",
+    "  → JSON + Markdown\n",
+    "```\n",
+    "\n",
+    "第一版不使用工具调用。普通 Python 代码负责读取、校验和保存文件,Agent 只负责语言理解与审核。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dcfe633b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 表示从会议原文中提取的一条行动项。\n",
+    "class ActionItem(BaseModel):\n",
+    "    task: str = Field(min_length=1)\n",
+    "    owner: str | None = None\n",
+    "    due_date_raw: str | None = None\n",
+    "    priority: Literal[\"高\", \"中\", \"低\", \"未说明\"] = \"未说明\"\n",
+    "    evidence: str = Field(min_length=1)\n",
+    "\n",
+    "\n",
+    "# 表示最终输出的完整会议纪要及其审核状态。\n",
+    "class MeetingResult(BaseModel):\n",
+    "    title: str = Field(min_length=1)\n",
+    "    meeting_date: str | None = None\n",
+    "    participants: list[str] = Field(default_factory=list)\n",
+    "    summary: str = Field(min_length=1)\n",
+    "    decisions: list[str] = Field(default_factory=list)\n",
+    "    action_items: list[ActionItem] = Field(default_factory=list)\n",
+    "    open_questions: list[str] = Field(default_factory=list)\n",
+    "    review_status: Literal[\"pending\", \"passed\", \"needs_manual_review\"] = \"pending\"\n",
+    "    review_issues: list[str] = Field(default_factory=list)\n",
+    "\n",
+    "\n",
+    "# 表示 ReviewAgent 对纪要草稿的审核结论和问题。\n",
+    "class ReviewResult(BaseModel):\n",
+    "    passed: bool\n",
+    "    issues: list[str] = Field(default_factory=list)\n",
+    "    missing_items: list[str] = Field(default_factory=list)\n",
+    "    unsupported_items: list[str] = Field(default_factory=list)\n",
+    "    revision_advice: list[str] = Field(default_factory=list)\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3184f77e",
+   "metadata": {},
+   "source": [
+    "## 2. 解析 Agent 返回的 JSON\n",
+    "\n",
+    "模型有时会把 JSON 包在 Markdown 代码围栏中,或者在前后添加一句解释。下面的函数先提取最外层 JSON 对象,再交给 Pydantic 校验。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "fb68843b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 从模型响应中截取并解析 JSON 对象。\n",
+    "def extract_json_object(text: str) -> dict:\n",
+    "    start = text.find(\"{\")\n",
+    "    end = text.rfind(\"}\")\n",
+    "    if start == -1 or end == -1 or end < start:\n",
+    "        raise ValueError(\"模型响应中没有完整的 JSON 对象\")\n",
+    "    return json.loads(text[start : end + 1])\n",
+    "\n",
+    "\n",
+    "# 将模型响应解析并验证为指定的 Pydantic 模型。\n",
+    "def parse_model_response(text: str, model_type: type[BaseModel]) -> BaseModel:\n",
+    "    return model_type.model_validate(extract_json_object(text))\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ad5a8562",
+   "metadata": {},
+   "source": [
+    "## 3. 把结构化结果转换为 Markdown\n",
+    "\n",
+    "Markdown 由普通 Python 生成,避免让模型重复改写已经审核过的内容。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "84801fbf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 将可空文本整理成适合 Markdown 表格的内容。\n",
+    "def markdown_cell(value: str | None) -> str:\n",
+    "    if not value:\n",
+    "        return \"未提供\"\n",
+    "    return value.replace(\"|\", \"\\\\|\").replace(\"\\n\", \" \")\n",
+    "\n",
+    "\n",
+    "# 将会议纪要转换成 Markdown 文本。\n",
+    "def to_markdown(result: MeetingResult) -> str:\n",
+    "    meeting_date = markdown_cell(result.meeting_date)\n",
+    "    participants = \"、\".join(result.participants) if result.participants else \"未提供\"\n",
+    "    lines = [\n",
+    "        f\"# {result.title}\",\n",
+    "        \"\",\n",
+    "        f\"**会议日期:** {meeting_date}\",\n",
+    "        \"\",\n",
+    "        f\"**参会者:** {participants}\",\n",
+    "        \"\",\n",
+    "        \"## 会议摘要\",\n",
+    "        \"\",\n",
+    "        result.summary,\n",
+    "        \"\",\n",
+    "        \"## 已确认决策\",\n",
+    "        \"\",\n",
+    "    ]\n",
+    "    lines.extend([f\"- {item}\" for item in result.decisions] or [\"- 无\"])\n",
+    "    lines.extend([\n",
+    "        \"\",\n",
+    "        \"## 行动项\",\n",
+    "        \"\",\n",
+    "        \"| 任务 | 负责人 | 截止日期(原文) | 优先级 | 原文证据 |\",\n",
+    "        \"|---|---|---|---|---|\",\n",
+    "    ])\n",
+    "    if result.action_items:\n",
+    "        for item in result.action_items:\n",
+    "            lines.append(\n",
+    "                \"| \"\n",
+    "                + \" | \".join([\n",
+    "                    markdown_cell(item.task),\n",
+    "                    markdown_cell(item.owner),\n",
+    "                    markdown_cell(item.due_date_raw),\n",
+    "                    markdown_cell(item.priority),\n",
+    "                    markdown_cell(item.evidence),\n",
+    "                ])\n",
+    "                + \" |\"\n",
+    "            )\n",
+    "    else:\n",
+    "        lines.append(\"| 无 | 未提供 | 未提供 | 未说明 | 未提供 |\")\n",
+    "\n",
+    "    lines.extend([\"\", \"## 待确认问题\", \"\"])\n",
+    "    lines.extend([f\"- {item}\" for item in result.open_questions] or [\"- 无\"])\n",
+    "    lines.extend([\"\", \"## 审核状态\", \"\", f\"`{result.review_status}`\"])\n",
+    "    if result.review_issues:\n",
+    "        lines.extend([\"\", \"### 审核问题\", \"\"])\n",
+    "        lines.extend([f\"- {item}\" for item in result.review_issues])\n",
+    "    return \"\\n\".join(lines) + \"\\n\"\n",
+    "\n",
+    "\n",
+    "# 将会议结果保存为 JSON 和 Markdown 文件。\n",
+    "def save_result(result: MeetingResult, stem: str = \"meeting_result\") -> tuple[Path, Path]:\n",
+    "    output_dir = PROJECT_ROOT / \"outputs\"\n",
+    "    json_path = output_dir / f\"{stem}.json\"\n",
+    "    markdown_path = output_dir / f\"{stem}.md\"\n",
+    "    json_path.write_text(result.model_dump_json(indent=2), encoding=\"utf-8\")\n",
+    "    markdown_path.write_text(to_markdown(result), encoding=\"utf-8\")\n",
+    "    return json_path, markdown_path\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "757240ce",
+   "metadata": {},
+   "source": [
+    "## 4. Agent 职责与提示词\n",
+    "\n",
+    "- MinutesAgent 只能提取原文支持的信息,未知字段必须保持为空。\n",
+    "- ReviewAgent 必须同时查看原文和草稿,重点检查遗漏、编造、日期冲突,以及“建议”是否被误写为“决定”。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "db220c24",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "MINUTES_SYSTEM_PROMPT = \"\"\"你是严谨的中文会议纪要提取专家。\n",
+    "只使用会议原文明确支持的信息,不得补充常识或猜测。\n",
+    "严格区分讨论、建议和已确认决策。\n",
+    "每个行动项必须保留一段原文证据;未知负责人、会议日期或截止日期必须为 null。\n",
+    "只返回符合用户给定 Schema 的 JSON,不要返回 Markdown 或额外解释。\"\"\"\n",
+    "\n",
+    "REVIEW_SYSTEM_PROMPT = \"\"\"你是独立的会议纪要审核员。\n",
+    "必须逐项对照会议原文和纪要草稿,检查遗漏、编造、模糊行动项和日期冲突。\n",
+    "不能因为文字通顺就判定通过,也不能使用外部信息。\n",
+    "只返回符合用户给定 Schema 的 JSON,不要返回 Markdown 或额外解释。\"\"\"\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a3276b50",
+   "metadata": {},
+   "source": [
+    "## 5. 有限调用与格式修复\n",
+    "\n",
+    "整个流程共享四次模型调用预算。JSON 首次解析失败时允许请求一次格式修复,但修复同样计入预算。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d3d42947",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class CallBudget:\n",
+    "    # 初始化模型调用次数上限。\n",
+    "    def __init__(self, maximum: int = 4) -> None:\n",
+    "        self.maximum = maximum\n",
+    "        self.used = 0\n",
+    "\n",
+    "    # 计算剩余的模型调用次数。\n",
+    "    @property\n",
+    "    def remaining(self) -> int:\n",
+    "        return self.maximum - self.used\n",
+    "\n",
+    "    # 在次数限制内执行一次 Agent 调用。\n",
+    "    def run(self, agent, prompt: str) -> str:\n",
+    "        if self.remaining <= 0:\n",
+    "            raise RuntimeError(\"已达到四次模型调用上限\")\n",
+    "        self.used += 1\n",
+    "        return agent.run(prompt)\n",
+    "\n",
+    "\n",
+    "# 调用 Agent 并将响应解析为指定的数据模型。\n",
+    "def run_structured(\n",
+    "    agent,\n",
+    "    prompt: str,\n",
+    "    model_type: type[BaseModel],\n",
+    "    budget: CallBudget,\n",
+    ") -> BaseModel:\n",
+    "    schema = json.dumps(model_type.model_json_schema(), ensure_ascii=False)\n",
+    "    full_prompt = f\"{prompt}\\n\\n必须遵循以下 JSON Schema:\\n{schema}\"\n",
+    "    raw_response = budget.run(agent, full_prompt)\n",
+    "    try:\n",
+    "        return parse_model_response(raw_response, model_type)\n",
+    "    except ValueError as error:\n",
+    "        if budget.remaining <= 0:\n",
+    "            raise RuntimeError(f\"JSON 校验失败且没有剩余调用次数:{error}\") from error\n",
+    "        repair_prompt = (\n",
+    "            \"上一次响应无法通过 JSON 校验。不要改变内容含义,只修复格式。\\n\"\n",
+    "            f\"校验错误:{error}\\n\"\n",
+    "            f\"原响应:\\n{raw_response}\\n\"\n",
+    "            f\"目标 Schema:\\n{schema}\\n\"\n",
+    "            \"只返回修复后的 JSON。\"\n",
+    "        )\n",
+    "        repaired_response = budget.run(agent, repair_prompt)\n",
+    "        return parse_model_response(repaired_response, model_type)\n",
+    "\n",
+    "\n",
+    "# 创建 MinutesAgent 和 ReviewAgent。\n",
+    "def build_agents():\n",
+    "    llm = HelloAgentsLLM()\n",
+    "    minutes_agent = SimpleAgent(name=\"MinutesAgent\", llm=llm, system_prompt=MINUTES_SYSTEM_PROMPT)\n",
+    "    review_agent = SimpleAgent(name=\"ReviewAgent\", llm=llm, system_prompt=REVIEW_SYSTEM_PROMPT)\n",
+    "    return minutes_agent, review_agent\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e05c43ac",
+   "metadata": {},
+   "source": [
+    "## 6. 完整分析流程\n",
+    "\n",
+    "首次审核通过时只调用两次模型;未通过且仍有两次预算时,MinutesAgent 修正一次,再由 ReviewAgent 最终复核。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "225d6a8f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# 检查并清理输入的会议文本。\n",
+    "def validate_transcript(transcript: str) -> str:\n",
+    "    cleaned = transcript.strip()\n",
+    "    if len(cleaned) < 20:\n",
+    "        raise ValueError(\"会议记录过短,请至少提供 20 个字符\")\n",
+    "    return cleaned\n",
+    "\n",
+    "\n",
+    "# 组合审核会议纪要所需的提示词。\n",
+    "def make_review_prompt(transcript: str, draft: MeetingResult) -> str:\n",
+    "    return (\n",
+    "        \"请审核以下会议纪要草稿。\\n\\n\"\n",
+    "        f\"【会议原文】\\n{transcript}\\n\\n\"\n",
+    "        f\"【纪要草稿】\\n{draft.model_dump_json(indent=2)}\"\n",
+    "    )\n",
+    "\n",
+    "\n",
+    "# 执行纪要提取、审核和必要时修正的完整流程。\n",
+    "def analyze_meeting(transcript: str) -> tuple[MeetingResult, ReviewResult, int]:\n",
+    "    transcript = validate_transcript(transcript)\n",
+    "    minutes_agent, review_agent = build_agents()\n",
+    "    budget = CallBudget(maximum=4)\n",
+    "\n",
+    "    draft_prompt = f\"请从以下会议原文提取结构化纪要:\\n\\n{transcript}\"\n",
+    "    draft = run_structured(minutes_agent, draft_prompt, MeetingResult, budget)\n",
+    "    review = run_structured(review_agent, make_review_prompt(transcript, draft), ReviewResult, budget)\n",
+    "\n",
+    "    if review.passed:\n",
+    "        final_result = draft.model_copy(update={\"review_status\": \"passed\", \"review_issues\": []})\n",
+    "        return final_result, review, budget.used\n",
+    "\n",
+    "    issues = review.issues + review.missing_items + review.unsupported_items\n",
+    "    if budget.remaining < 2:\n",
+    "        final_result = draft.model_copy(\n",
+    "            update={\"review_status\": \"needs_manual_review\", \"review_issues\": issues}\n",
+    "        )\n",
+    "        return final_result, review, budget.used\n",
+    "\n",
+    "    revision_prompt = (\n",
+    "        \"请根据审核意见修正纪要。仍然只能使用会议原文支持的信息。\\n\\n\"\n",
+    "        f\"【会议原文】\\n{transcript}\\n\\n\"\n",
+    "        f\"【原草稿】\\n{draft.model_dump_json(indent=2)}\\n\\n\"\n",
+    "        f\"【审核意见】\\n{review.model_dump_json(indent=2)}\"\n",
+    "    )\n",
+    "    revised = run_structured(minutes_agent, revision_prompt, MeetingResult, budget)\n",
+    "    final_review = run_structured(review_agent, make_review_prompt(transcript, revised), ReviewResult, budget)\n",
+    "    final_issues = final_review.issues + final_review.missing_items + final_review.unsupported_items\n",
+    "    status = \"passed\" if final_review.passed else \"needs_manual_review\"\n",
+    "    final_result = revised.model_copy(update={\"review_status\": status, \"review_issues\": final_issues})\n",
+    "    return final_result, final_review, budget.used\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8d2fe2d1",
+   "metadata": {},
+   "source": [
+    "## 7. 运行双 Agent 会议示例\n",
+    "\n",
+    "本单元格直接读取模型配置并执行 MinutesAgent、ReviewAgent。运行前必须在 `.env` 中填写 `LLM_MODEL_ID`、`LLM_API_KEY` 和 `LLM_BASE_URL`。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ce32abf9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sample_transcript = (PROJECT_ROOT / \"data\" / \"sample_meeting.txt\").read_text(encoding=\"utf-8\")\n",
+    "result, _, call_count = analyze_meeting(sample_transcript)\n",
+    "json_path, markdown_path = save_result(result)\n",
+    "print(f\"模型调用次数: {call_count}\")\n",
+    "print(f\"审核状态: {result.review_status}\")\n",
+    "print(f\"已保存: {json_path.name}, {markdown_path.name}\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "ce2a14c3",
+   "metadata": {},
+   "source": [
+    "## 8. 结构与编排自检\n",
+    "\n",
+    "这些检查不调用模型,验证 Pydantic 结构、JSON 提取、缺失字段、空行动项、空输入和 Markdown 渲染。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bb6f6818",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "expected_path = PROJECT_ROOT / \"outputs\" / \"example_result.json\"\n",
+    "expected_result = MeetingResult.model_validate_json(expected_path.read_text(encoding=\"utf-8\"))\n",
+    "\n",
+    "fenced = \"```json\\n\" + expected_result.model_dump_json() + \"\\n```\"\n",
+    "assert parse_model_response(fenced, MeetingResult).title == \"新用户注册功能迭代\"\n",
+    "assert expected_result.meeting_date == \"2026-07-27\"\n",
+    "assert expected_result.action_items[-1].owner is None\n",
+    "\n",
+    "empty_actions = MeetingResult(\n",
+    "    title=\"信息同步会\",\n",
+    "    participants=[],\n",
+    "    summary=\"本次会议仅同步信息,没有形成行动项。\",\n",
+    "    decisions=[],\n",
+    "    action_items=[],\n",
+    "    open_questions=[],\n",
+    ")\n",
+    "assert empty_actions.action_items == []\n",
+    "assert \"| 无 |\" in to_markdown(empty_actions)\n",
+    "\n",
+    "try:\n",
+    "    validate_transcript(\"太短\")\n",
+    "except ValueError:\n",
+    "    pass\n",
+    "else:\n",
+    "    raise AssertionError(\"过短会议记录应被拒绝\")\n",
+    "\n",
+    "try:\n",
+    "    ActionItem(task=\"测试\", owner=None, due_date_raw=None, priority=\"紧急\", evidence=\"原文\")\n",
+    "except ValidationError:\n",
+    "    pass\n",
+    "else:\n",
+    "    raise AssertionError(\"非法优先级应被 Pydantic 拒绝\")\n",
+    "\n",
+    "rendered = to_markdown(expected_result)\n",
+    "tracked_markdown = (PROJECT_ROOT / \"outputs\" / \"example_minutes.md\").read_text(encoding=\"utf-8\")\n",
+    "assert rendered == tracked_markdown\n",
+    "print(\"自检通过:6 组\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "abc42119",
+   "metadata": {},
+   "source": [
+    "## 练习:分析边界会议\n",
+    "\n",
+    "打开 `data/edge_case_meeting.txt`,先人工预测结果:\n",
+    "\n",
+    "1. “可以考虑下个月上线”是否属于已确认决策?\n",
+    "2. 测试环境确认任务是否有负责人?\n",
+    "3. 产品文档的日期是否存在冲突?\n",
+    "\n",
+    "启用真实模型后,可以把下面的 `edge_transcript` 传给 `analyze_meeting`,再与预测比较。\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f8882d28",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "edge_transcript = (PROJECT_ROOT / \"data\" / \"edge_case_meeting.txt\").read_text(encoding=\"utf-8\")\n",
+    "answer_scaffold = {\n",
+    "    \"confirmed_launch_decision\": False,\n",
+    "    \"test_environment_owner\": None,\n",
+    "    \"document_date_conflict\": True,\n",
+    "}\n",
+    "answer_scaffold\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "23f22c4d",
+   "metadata": {},
+   "source": [
+    "## 常见问题与下一步\n",
+    "\n",
+    "- **把建议写成决定**:Reviewer 必须检查“考虑、建议、可能”等措辞。\n",
+    "- **编造负责人或日期**:未知值保持 `null`,最终 Markdown 显示“未提供”。\n",
+    "- **JSON 不稳定**:允许一次格式修复,但仍受四次调用预算限制。\n",
+    "- **多份会议连续分析**:每次调用 `analyze_meeting` 都会创建新的 Agent,避免历史记录互相污染。\n",
+    "\n",
+    "第二版可增加日期标准化工具,但第一版保持双 Agent、无工具调用。\n"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "MeetingActionAgent",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.13.11"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

+ 32 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/outputs/example_minutes.md

@@ -0,0 +1,32 @@
+# 新用户注册功能迭代
+
+**会议日期:** 2026-07-27
+
+**参会者:** 林晓、周明、陈悦
+
+## 会议摘要
+
+团队明确了新用户注册功能的前后端分工、联调安排和验证码有效期,同时记录了测试环境及产品文档负责人待确认的问题。
+
+## 已确认决策
+
+- 邮箱验证码首版有效期设为5分钟
+
+## 行动项
+
+| 任务 | 负责人 | 截止日期(原文) | 优先级 | 原文证据 |
+|---|---|---|---|---|
+| 完成注册页面和表单校验 | 林晓 | 7月31日前 | 未说明 | 林晓:注册页面和表单校验由我负责,7月31日前完成。 |
+| 完成后端注册接口 | 周明 | 8月3日 | 未说明 | 周明:后端注册接口我来做,截止8月3日。 |
+| 执行注册功能联调测试 | 陈悦 | 未提供 | 未说明 | 陈悦:我会在接口完成后执行联调测试,但测试环境地址还没有确定。 |
+| 补充产品说明文档中的异常流程 | 未提供 | 未提供 | 未说明 | 林晓:产品说明文档需要补充异常流程,负责人会后确认。 |
+
+## 待确认问题
+
+- 联调测试的具体截止日期是什么?
+- 测试环境地址何时能够确定?
+- 谁负责补充产品说明文档中的异常流程,截止日期是什么?
+
+## 审核状态
+
+`passed`

+ 50 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/outputs/example_result.json

@@ -0,0 +1,50 @@
+{
+  "title": "新用户注册功能迭代",
+  "meeting_date": "2026-07-27",
+  "participants": [
+    "林晓",
+    "周明",
+    "陈悦"
+  ],
+  "summary": "团队明确了新用户注册功能的前后端分工、联调安排和验证码有效期,同时记录了测试环境及产品文档负责人待确认的问题。",
+  "decisions": [
+    "邮箱验证码首版有效期设为5分钟"
+  ],
+  "action_items": [
+    {
+      "task": "完成注册页面和表单校验",
+      "owner": "林晓",
+      "due_date_raw": "7月31日前",
+      "priority": "未说明",
+      "evidence": "林晓:注册页面和表单校验由我负责,7月31日前完成。"
+    },
+    {
+      "task": "完成后端注册接口",
+      "owner": "周明",
+      "due_date_raw": "8月3日",
+      "priority": "未说明",
+      "evidence": "周明:后端注册接口我来做,截止8月3日。"
+    },
+    {
+      "task": "执行注册功能联调测试",
+      "owner": "陈悦",
+      "due_date_raw": null,
+      "priority": "未说明",
+      "evidence": "陈悦:我会在接口完成后执行联调测试,但测试环境地址还没有确定。"
+    },
+    {
+      "task": "补充产品说明文档中的异常流程",
+      "owner": null,
+      "due_date_raw": null,
+      "priority": "未说明",
+      "evidence": "林晓:产品说明文档需要补充异常流程,负责人会后确认。"
+    }
+  ],
+  "open_questions": [
+    "联调测试的具体截止日期是什么?",
+    "测试环境地址何时能够确定?",
+    "谁负责补充产品说明文档中的异常流程,截止日期是什么?"
+  ],
+  "review_status": "passed",
+  "review_issues": []
+}

+ 12 - 0
Co-creation-projects/Henry2513-MeetingActionAgent/requirements.txt

@@ -0,0 +1,12 @@
+# 核心依赖
+hello-agents==0.2.9
+openai>=1.109,<2
+pydantic>=2.12,<3
+python-dotenv>=1.2,<2
+
+# hello-agents 0.2.9 在导入时需要此依赖
+huggingface-hub>=1.20,<2
+
+# Notebook 环境
+jupyterlab>=4.4,<5
+ipykernel>=6.30,<8