diff --git a/skills/openmaic-classroom/SKILL.md b/skills/openmaic-classroom/SKILL.md new file mode 100644 index 00000000..c60a7b0e --- /dev/null +++ b/skills/openmaic-classroom/SKILL.md @@ -0,0 +1,275 @@ +--- +name: openmaic-classroom +description: 将 RAG 检索结果或文档块转换为 OpenMAIC 互动课程。当用户要求将知识库内容、检索到的文档片段、或上传的文档转换为教学课件/互动课堂时使用此技能。支持纯需求生成和基于 PDF 内容的课程生成。 +--- + +# OpenMAIC Classroom Generator + +将 WeKnora 知识库中的 RAG 检索结果或文档内容转换为 OpenMAIC 互动课程。 + +## 核心能力 + +1. **RAG → 课程**: 将知识检索结果提炼为教学需求(requirement),通过 OpenMAIC API 生成互动课程 +2. **PDF → 课程**: 解析用户上传的 PDF,结合内容生成课程 +3. **文档块 → 课程集**: 将多个文档块/知识片段组织为多阶段课程集 + +## 能力边界 + +> **通过 WeKnora 注册的 mcp-api-requester MCP 工具,你可以直接调用 OpenMAIC API**(HTTP POST/GET 请求)。 +> 该工具在 WeKnora agent 中注册为 `mcp_{service_name}_{tool_name}` 格式(如 `mcp_mcp_api_requester_make_request`)。 +> 不再需要向用户提供 curl 命令——直接在后台调用 API 并交付结果。 +> 如果 MCP 工具未配置或不可用,则降级为展示 curl 命令让用户手动执行。 + +> **⚠️ MCP 可用性检查(每次读取此文件后必须执行):** +> 在开始工作前,检查可用的 MCP 工具列表中是否存在名称包含 `mcp_api_requester` 的工具。 +> - **如果未找到**:必须提醒用户:"未检测到 `mcp_api_requester` MCP 服务。请先从 https://github.com/yryuu/mcp-api-requester 下载代码并部署,然后在 WeKnora 中注册该 MCP 服务。" +> - **如果已找到**:继续后续流程,使用该 MCP 工具调用 API。 + +## 模式选择 + +OpenMAIC 有两种使用模式,**根据用户场景选择**: + +| 模式 | Base URL | 认证方式 | 适用场景 | +|------|----------|----------|----------| +| 托管模式(推荐快速使用) | `https://open.maic.chat` | `Authorization: Bearer ` | 用户有 open.maic.chat 访问码,无需本地部署 | +| 本地模式 | `http://host.docker.internal:3000`(WeKnora 在容器内)或 `http://localhost:3000`(本地直接运行) | 无认证(本地自部署) | 用户自行部署了 OpenMAIC 实例 | + +**判断规则**: +- 用户提到"在线服务"、"open.maic.chat"、"访问码" → 使用托管模式 +- 用户提到"本地部署"、"自建" → 使用本地模式 +- 用户未明确说明时,优先询问用户使用哪个模式 + +## 前置条件 + +| 配置项 | 说明 | +|--------|------| +| 模式 | 托管模式 或 本地模式(见上方判断规则) | +| `accessCode` | 托管模式必需——访问码(以 `sk-` 开头),由用户在 open.maic.chat 获取 | +| 健康检查 | 调用前验证服务可用:`GET /api/health` | + +## 使用场景 + +当用户请求涉及以下内容时,使用此技能: +- "把这个文档做成课件" +- "基于检索结果生成课程" +- "为这个知识点创建互动课堂" +- "将知识库内容转换为教学材料" + +## 工作流程 + +### Phase 1: 确认输入源 + +确认课程生成的输入来源(三选一): + +1. **纯需求生成**: 用户直接描述教学主题,无需额外文档 + → 直接使用用户描述作为 `requirement`,**无需调用脚本** +2. **RAG 检索结果**: 先通过 `knowledge_search` 检索相关知识,再将结果组织为 requirement + → 使用 `scripts/rag-to-requirement.py` 脚本转换检索结果为结构化 requirement(见 Phase 1.1) +3. **PDF 文件**: 用户提供 PDF 文件路径,先解析再调用生成 API + → 提取 PDF 文本后构建 requirement,**无需调用脚本** + +### Phase 1.1: RAG 结果 → Requirement 转换(仅适用于场景 2) + +当场景 2 有 RAG 检索结果时,调用 `scripts/rag-to-requirement.py` 将 chunks 转换为 requirement: + +``` +execute_skill_script( + skill_name: "openmaic-classroom", + script_path: "scripts/rag-to-requirement.py", + input: '{"chunks": [...检索结果...], "query": "用户查询", "audience": "目标受众"}' +) +``` + +**input 参数格式(JSON 字符串,必须通过 `input` 参数传入,不可用 `--file`):** +- `chunks`(必填): RAG 检索结果数组,每项包含 `document_name`、`content`、`metadata` +- `query`(可选): 用户原始查询 +- `audience`(可选): 目标受众描述,默认"相关领域的学习者" +- `depth`(可选): 教学深度 `beginner|intermediate|advanced`,默认 `intermediate` +- `language`(可选): `zh-CN|en-US`,默认 `zh-CN` +- `focus_areas`(可选): 重点领域数组 + +**注意:** +- 必须将 chunks 数据作为 `input` 参数传入(等价于 `echo '{"chunks":...}' | python script.py`) +- **不要**在没有任何参数的情况下调用此脚本,否则会报错退出 +- 如果脚本执行失败,可直接根据检索结果手动构建 requirement + +### Phase 2: 构建 Generation Request + +根据输入源构建请求体,**字段说明**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `requirement` | string | 是 | 教学主题描述,1-2 句话 | +| `pdfContent` | object | 否 | PDF 解析后的文本和图片 | +| `language` | string | 否 | `"zh-CN"` 或 `"en-US"`,默认 `"zh-CN"` | +| `enableWebSearch` | bool | 否 | 是否启用网络搜索,默认 false | +| `enableImageGeneration` | bool | 否 | 是否生成配图,默认 false | +| `enableVideoGeneration` | bool | 否 | 是否生成视频,默认 false | +| `enableTTS` | bool | 否 | 是否生成语音朗读,默认 false | +| `agentMode` | string | 否 | `"default"` 或 `"generate"`,默认 `"default"` | + +场景适配: +- **场景 1(纯需求)**: `requirement` 直接使用用户描述 +- **场景 2(RAG 结果)**: `requirement` 使用 Phase 1.1 脚本输出中的 `requirement` 字段 +- **场景 3(PDF)**: `requirement` 根据 PDF 提取的文本构建,`pdfContent` 填入解析结果 + +### Phase 3: 调用 OpenMAIC API + +**优先方式**:通过 WeKnora 注册的 MCP 工具直接调用 API。 + +**第一步:识别 HTTP 请求工具** +- 在你可用的 MCP 工具中,找到用于 HTTP 请求的工具 +- 工具名称格式为 `mcp_{service_name}_{tool_name}`(如 `mcp_mcp_api_requester_make_request`) +- 通过工具描述(description)识别:寻找包含 "HTTP request"、"API"、"GET/POST" 等关键词的工具 +- 如果找不到 HTTP 请求类 MCP 工具,则降级为 curl 方案 + +**第二步:确定 Base URL 和认证 Header** + +| 模式 | Base URL | 认证 Header | +|------|----------|-------------| +| 托管模式 | `https://open.maic.chat` | `Authorization: Bearer ` | +| 本地模式 | `http://host.docker.internal:3000` 或 `http://localhost:3000` | 无 | + +**第三步:Feature Detection(发送可选功能前)** + +在发送生成请求前,先查询 `GET /api/health`(托管模式需带 auth header),检查返回的 `capabilities` 对象: + +```json +{ + "status": "ok", + "version": "...", + "capabilities": { + "webSearch": true, + "imageGeneration": false, + "videoGeneration": false, + "tts": true + } +} +``` + +- 只有当 `capabilities` 中某项为 `true` 时,才能在生成请求中将对应 feature flag 设为 `true` +- 如果服务器未返回 `capabilities`(旧版本),不要发送任何可选 feature flags + +**第四步:发送 POST 请求** + +使用识别到的 HTTP 请求工具发送请求。根据上面确定的模式和 URL 构造请求: + +**托管模式**: +```json +{ + "url": "https://open.maic.chat/api/generate-classroom", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer " + }, + "body": { + "requirement": "..." + } +} +``` + +**本地模式**: +```json +{ + "url": "/api/generate-classroom", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "requirement": "..." + } +} +``` + +**降级方式**:如果 MCP 工具不可用,向用户展示 curl 命令: + +**托管模式**: +```bash +curl -X POST https://open.maic.chat/api/generate-classroom \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{"requirement": "..."}' +``` + +**本地模式**: +```bash +curl -X POST /api/generate-classroom \ + -H "Content-Type: application/json" \ + -d '{"requirement": "..."}' +``` + +### Phase 4: 查询任务进度 + +API 返回 `jobId` 和 `pollUrl` 后,执行以下流程: + +**第 1 次查询(提交后立即执行)**: +1. 调用 HTTP 请求工具 `GET {pollUrl}` 获取当前状态 +2. 检查 `status`: + - 如果 `succeeded` → 进入 Phase 5 + - 如果 `failed` → 报告错误并停止 + - 如果 `queued` 或 `running` → **停止查询,告知用户**: + + > 课程正在生成中,预计需要 2-10 分钟。请稍后询问我查询进度。 + > Job ID: {jobId} + +**用户询问进度时(第 2 次查询)**: +1. 再次调用 `GET {pollUrl}` +2. 检查 `status`: + - 如果 `succeeded` → 进入 Phase 5 + - 如果 `failed` → 报告错误并停止 + - 如果仍在 `queued` 或 `running` → **停止查询,告知用户继续等待**: + + > 课程仍在生成中,请稍后再试。 + > Job ID: {jobId} + +**重要规则**: +- 提交后只查询 **1 次**,不要连续轮询 +- 用户询问进度时只查询 **1 次**,不要连续轮询 +- 仅在 `status` 为 `succeeded` 或 `failed` 时才继续下一步——否则必须停止并告知用户等待 +- 不要尝试重新提交 job——保持查询同一个 `pollUrl` + +### Phase 5: 返回结果 + +生成成功后,返回: + +``` +Classroom ID: +Classroom URL: +/classroom/ +``` + +托管模式的 URL 格式:`https://open.maic.chat/classroom/` + +> URL 必须以纯文本独占一行输出,不加粗、不加代码格式、不加 Markdown 链接。 + +## 错误处理 + +| 错误 | 含义 | 处理方式 | +|------|------|----------| +| 连接失败 | 网络不通或服务未启动 | 检查 Base URL 是否正确,服务是否启动 | +| 401 | 访问码无效(托管模式) | 告知用户到 open.maic.chat 检查或重新生成访问码 | +| 403 | 每日配额用尽(托管模式) | 告知每日 10 次限制,次日零点重置 | +| 500 | 服务器错误 | 建议稍后重试或切换到本地模式 | +| Provider 配置错误 | 模型/Provider/认证问题 | 引导用户检查 `.env.local` 或 `server-providers.yml` | + +## 多文档 → 课程集 + +当用户需要将多个文档/知识片段生成课程集时: + +1. 收集所有文档内容 +2. 为每个文档/主题分别生成 requirement +3. 通过 MCP 工具依次调用生成 API(不可并行,避免配额冲突) +4. 如果 MCP 工具不可用,依次提供对应的 curl 命令 +5. 汇总返回所有 Classroom URL + +## 注意事项 + +- 脚本在 Docker 沙箱中执行,**沙箱默认禁用网络访问** +- **通过 WeKnora MCP 工具,你可以直接调用 OpenMAIC API**——不再需要用户提供 curl 命令 +- MCP 工具名称格式为 `mcp_{service_name}_{tool_name}`,根据描述识别 HTTP 请求工具 +- 如果 MCP 工具未启用或不可用,降级为展示 curl 命令让用户手动执行 +- 单次生成任务预计 2-10 分钟,取决于内容复杂度和可选功能 +- 托管模式(open.maic.chat)每天最多 10 次生成配额,独立于 Web UI 配额 +- 如果用户在同一个 job 仍在运行时要求生成新课程,不要重复提交——先检查已有 job 状态 diff --git a/skills/openmaic-classroom/references/api-reference.md b/skills/openmaic-classroom/references/api-reference.md new file mode 100644 index 00000000..4693b267 --- /dev/null +++ b/skills/openmaic-classroom/references/api-reference.md @@ -0,0 +1,297 @@ +# OpenMAIC API Reference + +OpenMAIC 服务 API 接口规范。所有 API 基础路径为 `{base_url}/api`,默认 `http://localhost:3000/api`。 + +## 认证机制 + +- 如果 OpenMAIC 未设置 `ACCESS_CODE` 环境变量:所有接口开放 +- 如果设置了 `ACCESS_CODE`:需先通过 `/api/access-code/verify` 获取 cookie +- `/api/health` 始终无需认证 + +### 获取认证 Cookie + +``` +POST {base_url}/api/access-code/verify +Content-Type: application/json + +{ "code": "" } +``` + +成功返回 `{ "success": true, "valid": true }` 并设置 `openmaic_access` cookie(7 天有效)。 + +## 标准响应格式 + +**成功**: +```json +{ "success": true, ...additionalFields } +``` + +**错误**: +```json +{ "success": false, "errorCode": "", "error": "", "details?": "" } +``` + +--- + +## 核心端点 + +### 1. 健康检查 + +``` +GET {base_url}/api/health +``` + +响应: +```json +{ + "success": true, + "status": "ok", + "version": "0.1.0", + "capabilities": { + "webSearch": true, + "imageGeneration": false, + "videoGeneration": false, + "tts": true + } +} +``` + +> `capabilities` 用于功能检测,只在返回此字段时才启用对应的可选功能。 + +--- + +### 2. 生成课程(异步任务) + +#### 2a. 创建生成任务 + +``` +POST {base_url}/api/generate-classroom +Content-Type: application/json +``` + +请求体: +```json +{ + "requirement": "教学主题描述", + "pdfContent": { "text": "PDF文本内容", "images": [] }, + "enableWebSearch": false, + "enableImageGeneration": false, + "enableVideoGeneration": false, + "enableTTS": false, + "agentMode": "default" +} +``` + +成功响应 (202): +```json +{ + "success": true, + "jobId": "abc123xyz", + "status": "queued", + "step": "queued", + "message": "Classroom generation job queued", + "pollUrl": "{base_url}/api/generate-classroom/abc123xyz", + "pollIntervalMs": 5000 +} +``` + +错误: +- `400`: `requirement` 字段缺失 +- `500`: 内部错误 + +#### 2b. 轮询任务状态 + +``` +GET {pollUrl} +``` + +响应: +```json +{ + "success": true, + "jobId": "abc123xyz", + "status": "running", + "step": "generating_outlines", + "progress": 35, + "message": "Generating scene outlines...", + "pollUrl": "...", + "pollIntervalMs": 5000, + "scenesGenerated": 2, + "totalScenes": 6, + "result": null, + "error": null, + "done": false +} +``` + +最终成功响应: +```json +{ + "success": true, + "status": "succeeded", + "result": { + "classroomId": "Uyh82Y32ZK", + "url": "{base_url}/classroom/Uyh82Y32ZK", + "scenesCount": 6 + }, + "done": true +} +``` + +最终失败响应: +```json +{ + "success": true, + "status": "failed", + "error": "具体错误信息", + "done": true +} +``` + +--- + +### 3. 解析 PDF + +``` +POST {base_url}/api/parse-pdf +Content-Type: multipart/form-data +``` + +表单字段: +- `pdf`(文件,必填): PDF 文件 +- `providerId`(可选): PDF 提供商,默认 `"unpdf"` +- `apiKey`(可选): 提供商 API Key +- `baseUrl`(可选): 提供商基础 URL + +响应: +```json +{ + "success": true, + "data": { + "text": "提取的文本内容", + "images": [], + "metadata": { + "pageCount": 10, + "fileName": "document.pdf", + "fileSize": 1024000 + } + } +} +``` + +--- + +### 4. 课程存储 + +#### 4a. 获取课程 + +``` +GET {base_url}/api/classroom?id= +``` + +#### 4b. 持久化课程 + +``` +POST {base_url}/api/classroom +Content-Type: application/json + +{ "stage": {...}, "scenes": [...] } +``` + +--- + +### 5. Web 搜索 + +``` +POST {base_url}/api/web-search +Content-Type: application/json + +{ "query": "搜索关键词", "pdfText": "PDF上下文", "apiKey": "Tavily Key" } +``` + +--- + +### 6. 验证 LLM 连接 + +``` +POST {base_url}/api/verify-model +Content-Type: application/json + +{ "model": "openai:gpt-4o", "apiKey": "...", "baseUrl": "..." } +``` + +--- + +## 生成管线(逐步调用) + +如果需要更细粒度的控制,可以使用以下逐步生成端点代替 `/api/generate-classroom`: + +### A. 生成场景大纲(SSE 流) + +``` +POST {base_url}/api/generate/scene-outlines-stream +``` + +请求体: +```json +{ + "requirements": { "requirement": "主题", "userNickname": "用户昵称" }, + "pdfText": "PDF文本", + "pdfImages": [], + "researchContext": "网络搜索结果", + "agents": [] +} +``` + +Headers: +- `x-image-generation-enabled`: `"true"` 或 `"false"` +- `x-video-generation-enabled`: `"true"` 或 `"false"` + +SSE 事件类型:`languageDirective`、`outline`、`done`、`error`、`retry` + +### B. 生成场景内容 + +``` +POST {base_url}/api/generate/scene-content +Content-Type: application/json + +{ + "outline": {...}, + "allOutlines": [...], + "stageId": "stage-1", + "pdfImages": [], + "agents": [], + "languageDirective": "使用简体中文" +} +``` + +### C. 生成场景动作 + +``` +POST {base_url}/api/generate/scene-actions +Content-Type: application/json + +{ + "outline": {...}, + "allOutlines": [...], + "content": {...}, + "stageId": "stage-1", + "agents": [], + "languageDirective": "使用简体中文" +} +``` + +### D. 生成 Agent 画像 + +``` +POST {base_url}/api/generate/agent-profiles +Content-Type: application/json + +{ + "stageInfo": { "name": "入门阶段", "description": "..." }, + "sceneOutlines": [{ "title": "...", "description": "..." }], + "languageDirective": "使用简体中文", + "availableAvatars": [], + "avatarDescriptions": [] +} +``` diff --git a/skills/openmaic-classroom/references/requirement-builder.md b/skills/openmaic-classroom/references/requirement-builder.md new file mode 100644 index 00000000..c023e6d5 --- /dev/null +++ b/skills/openmaic-classroom/references/requirement-builder.md @@ -0,0 +1,135 @@ +# 需求构建指南 + +将 WeKnora RAG 检索结果转换为 OpenMAIC 课程生成所需的 `requirement` 格式。 + +## 核心原则 + +OpenMAIC 的 `requirement` 字段需要是**结构化的教学需求描述**,而不是原始文档片段。构建时需考虑: + +1. **教学主题**: 明确要教什么 +2. **目标受众**: 面向谁教学(如:初学者、专业人士、学生等) +3. **教学深度**: 入门级、中级、高级 +4. **内容范围**: 基于哪些知识源构建 + +## 转换模板 + +### 模板 1: 纯需求(无检索结果) + +用户直接描述需求时,直接使用用户描述作为 requirement: + +``` +用户: "帮我创建一个关于量子力学的入门课程" +→ requirement: "Create an introductory classroom on quantum mechanics for beginners" +``` + +### 模板 2: 基于 RAG 检索结果 + +``` +步骤: +1. 使用 knowledge_search 检索相关知识 +2. 从检索结果中提取: + - 核心主题/概念 + - 关键知识点 + - 文档来源信息 +3. 构建结构化 requirement +``` + +构建格式: +``` +基于以下知识内容,创建一个面向[目标受众]的[深度级别]课程: + +核心主题:[从检索结果提取的主要概念] +关键知识点: +- [知识点1] +- [知识点2] +- ... +内容来源:[文档名称列表] +``` + +### 模板 3: 基于单个文档 + +``` +基于文档《[文档名称]》的内容,创建一个面向[目标受众]的课程, +重点讲解以下方面: +- [用户指定的重点1] +- [用户指定的重点2] +``` + +### 模板 4: 基于多个文档/知识块 + +``` +综合以下文档内容,创建一个系统的课程: + +文档1《[名称1]》: [简要内容摘要] +文档2《[名称2]》: [简要内容摘要] +文档3《[名称3]》: [简要内容摘要] + +要求: +- 教学深度:[级别] +- 目标受众:[描述] +- 重点覆盖:[关键主题列表] +``` + +## 示例 + +### 示例 1: 技术文档 → 课程 + +``` +检索结果: +- 文档: "Kubernetes 部署指南.pdf" +- 关键内容: Pod 管理、Service 配置、Ingress 路由、存储卷 + +构建的 requirement: +"基于 Kubernetes 部署指南,创建一个面向 DevOps 工程师的中级课程。 +重点涵盖:Pod 生命周期管理、Service 和 Ingress 网络配置、持久化存储卷管理。 +课程应包含实践操作环节。" +``` + +### 示例 2: 产品手册 → 入门课程 + +``` +检索结果: +- 文档: "产品使用手册 v2.0.pdf" +- 关键内容: 产品概述、快速开始、核心功能、常见问题 + +构建的 requirement: +"基于产品使用手册 v2.0,为新用户创建一个入门课程。 +帮助用户快速了解产品核心功能,掌握基本操作方法, +并能够独立完成常见任务。课程语言为中文。" +``` + +### 示例 3: 研究论文 → 高级课程 + +``` +检索结果: +- 文档: "Transformer 架构研究综述.pdf" +- 关键内容: Attention 机制、位置编码、多头注意力、训练技巧 + +构建的 requirement: +"基于 Transformer 架构研究综述,为具有深度学习基础的研究人员 +创建高级课程。深入讲解 Attention 机制的数学原理、位置编码的 +各种变体、多头注意力的设计动机,以及训练大模型时的实践技巧。" +``` + +## 可选功能配置建议 + +在构建 request 时,根据用户需求推荐可选功能: + +| 场景 | 推荐功能 | +|------|----------| +| 技术培训 | `enableWebSearch: true`(补充最新技术动态) | +| 产品介绍 | `enableImageGeneration: true`(生成产品截图/界面图) | +| 市场营销 | `enableImageGeneration: true, enableVideoGeneration: true` | +| 语言教学 | `enableTTS: true`(语音朗读) | +| 学术研究 | 默认配置即可(不需要多媒体) | + +## 多文档处理 + +当需要将多个独立文档分别生成课程时: + +1. 为每个文档单独构建 requirement +2. 依次调用生成 API(不可并行) +3. 每完成一个即返回 URL,继续下一个 +4. 最终汇总所有 Classroom URL + +> 注意:OpenMAIC 托管模式每天最多 10 次生成配额,本地模式取决于 LLM Provider 配额。 diff --git a/skills/openmaic-classroom/scripts/rag-to-requirement.py b/skills/openmaic-classroom/scripts/rag-to-requirement.py new file mode 100644 index 00000000..f37290a7 --- /dev/null +++ b/skills/openmaic-classroom/scripts/rag-to-requirement.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +RAG 检索结果 → OpenMAIC Requirement 转换器 + +将 WeKnora RAG 检索结果转换为结构化的 OpenMAIC 课程生成需求描述。 +此脚本仅做数据转换,不涉及网络调用。 + +用法: + echo '{"chunks": [...], "audience": "初学者"}' | python scripts/rag-to-requirement.py + python scripts/rag-to-requirement.py --file results.json + +输入格式 (JSON): +{ + "chunks": [ + { + "document_name": "文档A.pdf", + "content": "文档内容片段...", + "metadata": {"page": 5, "section": "第三章"} + } + ], + "query": "用户原始查询(可选)", + "audience": "目标受众描述(可选,默认'相关领域的学习者')", + "depth": "教学深度: beginner|intermediate|advanced(可选,默认'intermediate')", + "language": "zh-CN|en-US(可选,默认'zh-CN')", + "focus_areas": ["重点领域1", "重点领域2"] # 可选 +} + +输出格式 (JSON): +{ + "requirement": "结构化的教学需求描述", + "metadata": { + "source_documents": ["文档A.pdf", "文档B.pdf"], + "total_chunks": 5, + "audience": "初学者", + "depth": "intermediate", + "language": "zh-CN" + } +} +""" + +import json +import sys +from typing import Any + + +def extract_key_topics(chunks: list[dict], max_topics: int = 5) -> list[str]: + """从文档块中提取关键主题(基于内容摘要和 section 信息)。""" + topics = [] + seen = set() + + for chunk in chunks: + metadata = chunk.get("metadata", {}) + # 优先使用 section/chapter 信息 + for key in ("section", "chapter", "heading", "title"): + if key in metadata and metadata[key] not in seen: + topics.append(metadata[key]) + seen.add(metadata[key]) + if len(topics) >= max_topics: + return topics + + # 如果 section 信息不足,从内容前 50 字提取 + for chunk in chunks: + content = chunk.get("content", "")[:50].strip() + if content and content not in seen: + topics.append(content + "...") + seen.add(content) + if len(topics) >= max_topics: + break + + return topics + + +def build_requirement(data: dict[str, Any]) -> str: + """将 RAG 结果构建为 OpenMAIC requirement 字符串。""" + chunks = data.get("chunks", []) + query = data.get("query", "") + audience = data.get("audience", "相关领域的学习者") + depth = data.get("depth", "intermediate") + language = data.get("language", "zh-CN") + focus_areas = data.get("focus_areas", []) + + depth_map = { + "beginner": "入门", + "intermediate": "中级", + "advanced": "高级", + } + depth_cn = depth_map.get(depth, "中级") + + # 提取文档名称 + source_docs = list({c.get("document_name", "未知文档") for c in chunks}) + + # 提取关键主题 + key_topics = extract_key_topics(chunks) + + # 构建 requirement + parts = [] + + # 开头:基于什么内容,为谁创建什么课程 + if query: + parts.append(f"基于以下知识库内容,为{audience}创建一个{depth_cn}课程。") + parts.append(f"用户原始需求:{query}") + else: + parts.append(f"基于以下知识库内容,为{audience}创建一个{depth_cn}课程。") + + # 内容来源 + if source_docs: + docs_str = "、".join(source_docs[:5]) + if len(source_docs) > 5: + docs_str += f" 等{len(source_docs)}个文档" + parts.append(f"\n内容来源:{docs_str}") + + # 关键主题 + if key_topics: + parts.append("\n核心主题:") + for i, topic in enumerate(key_topics, 1): + parts.append(f" {i}. {topic}") + + # 重点领域 + if focus_areas: + parts.append("\n重点覆盖:") + for area in focus_areas: + parts.append(f" - {area}") + + # 语言要求 + if language == "zh-CN": + parts.append("\n请使用中文生成课程内容。") + + return "\n".join(parts) + + +def process(input_data: dict[str, Any]) -> dict[str, Any]: + """主处理函数。""" + chunks = input_data.get("chunks", []) + if not chunks: + return { + "requirement": input_data.get("query", ""), + "metadata": { + "source_documents": [], + "total_chunks": 0, + "error": "未提供检索结果,使用原始查询作为 requirement", + }, + } + + requirement = build_requirement(input_data) + + source_docs = list({c.get("document_name", "未知文档") for c in chunks}) + + return { + "requirement": requirement, + "metadata": { + "source_documents": source_docs, + "total_chunks": len(chunks), + "audience": input_data.get("audience", "相关领域的学习者"), + "depth": input_data.get("depth", "intermediate"), + "language": input_data.get("language", "zh-CN"), + }, + } + + +def main() -> None: + """入口:从 stdin 或文件读取输入,输出 JSON 结果。""" + import argparse + + parser = argparse.ArgumentParser(description="RAG 结果 → OpenMAIC Requirement 转换器") + parser.add_argument("--file", "-f", help="输入 JSON 文件路径") + args = parser.parse_args() + + # 读取输入 + if args.file: + with open(args.file, "r", encoding="utf-8") as f: + input_data = json.load(f) + else: + input_text = sys.stdin.read() + if not input_text.strip(): + print( + "错误: 未提供输入数据。用法:\n" + " echo '{\"chunks\": [...]}' | python rag-to-requirement.py\n" + " python rag-to-requirement.py --file input.json", + file=sys.stderr, + ) + sys.exit(1) + try: + input_data = json.loads(input_text) + except json.JSONDecodeError as e: + print(f"错误: 输入 JSON 解析失败: {e}", file=sys.stderr) + sys.exit(1) + + # 处理并输出 + result = process(input_data) + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main()