
Openmaic Classroom
- 6 installs
- 19.4k repo stars
- Updated August 4, 2026
- tencent/weknora
Helps with ai & agent building tasks during AI-assisted development.
About
openmaic-classroom is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- openmaic-classroom
- AI & Agent Building
- AI-coding skill
Openmaic Classroom by the numbers
- 6 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #12,825 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tencent/weknora --skill openmaic-classroomAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 19.4k |
| Last updated | August 4, 2026 |
| Repository | tencent/weknora ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
OpenMAIC Classroom Generator
将 WeKnora 知识库中的 RAG 检索结果或文档内容转换为 OpenMAIC 互动课程。
核心能力
1. RAG → 课程: 将知识检索结果提炼为教学需求(requirement),通过 OpenMAIC API 生成互动课程 2. PDF → 课程: 解析用户上传的 PDF,结合内容生成课程 3. 文档块 → 课程集: 将多个文档块/知识片段组织为多阶段课程集 4. 概念图遍历 → 批量微课堂: 遍历知识图谱中所有 concept 页面,每个 concept 生成一个 micro-classroom
能力边界
通过 WeKnora 注册的 mcp_api_requester MCP 工具,你可以直接调用 OpenMAIC API(HTTP POST/GET 请求)。
该工具在 WeKnora agent 中注册为mcp_{service_name}_{tool_name}格式(如mcp_mcp_api_requester_make_request)。
必须使用该 MCP 工具调用 API。如果 MCP 工具未配置或不可用,引导用户先部署 mcp-api-requester,不提供 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 <access-code> | 用户有 open.maic.chat 访问码,无需本地部署 |
| 本地模式 | 用户提供(见本地模式 Base URL 处理) | 无认证(本地自部署) | 用户自行部署了 OpenMAIC 实例 |
判断规则:
- 用户提到"在线服务"、"open.maic.chat"、"访问码" → 使用托管模式
- 用户提到"本地部署"、"自建" → 使用本地模式
- 用户未明确说明时,优先询问用户使用哪个模式
本地模式 Base URL 处理: 1. 用户选择本地模式后,必须询问用户:"请输入你的 OpenMAIC 本地部署地址(例如 http://localhost:3000 或 http://192.168.1.100:3000)" 2. 收到用户提供的地址后,进行如下处理:
- 将地址中的
127.0.0.1替换为host.docker.internal - 将地址中的
localhost替换为host.docker.internal - 其他地址保持不变
⚠️ WeKnora 运行在 Docker 容器内,localhost和127.0.0.1指向容器自身,无法访问宿主机服务。必须使用host.docker.internal作为容器访问宿主机的桥接地址。
前置条件
| 配置项 | 说明 |
|---|---|
| 模式 | 托管模式 或 本地模式(见上方判断规则) |
accessCode | 托管模式必需——访问码(以 sk- 开头),由用户在 open.maic.chat 获取 |
| 健康检查 | 调用前验证服务可用:GET <BASE_URL>/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,无需调用脚本 4. 概念图遍历批量生成: 遍历知识图谱中所有 concept 页面,每个 concept 生成一个 micro-classroom → 使用 scripts/concept-to-requirement.py 脚本转换 concept + 关联 entity 为结构化 requirement(见 Phase 1.2)
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、metadataquery(可选): 用户原始查询audience(可选): 目标受众描述,默认"相关领域的学习者"depth(可选): 教学深度beginner|intermediate|advanced,默认intermediatelanguage(可选):zh-CN|en-US,默认zh-CNfocus_areas(可选): 重点领域数组
注意:
- 必须将 chunks 数据作为
input参数传入(等价于echo '{"chunks":...}' | python script.py) - 不要在没有任何参数的情况下调用此脚本,否则会报错退出
- 如果脚本执行失败,可直接根据检索结果手动构建 requirement
Phase 1.2: Concept Graph → Requirement 转换(仅适用于场景 4)
当场景 4 需要基于知识图谱概念批量生成课堂时,执行以下步骤:
步骤 1:列出所有 concept 页面
使用 wiki_search 工具搜索所有 concept 类型的页面:
wiki_search("^concept/", limit=50)如果 concept 数量超过 50 个,多次调用翻页直到获取全部。
步骤 2:对每个 concept 获取详情和关联 entity
对每个 concept 页面:
1. 调用 wiki_read_page([concept_slug]) 获取页面详情(含 OutLinks 和 InLinks) 2. 从 OutLinks 和 InLinks 中筛选出 entity/* 开头的 slug 3. 确定每个 entity 的 link_type:
- 同时出现在 OutLinks 和 InLinks 中 →
bidirectional - 仅出现在 OutLinks 中 →
outlink - 仅出现在 InLinks 中 →
inlink
4. 调用 wiki_read_page([entity_slugs]) 批量读取关联 entity(只取 title + summary,不取完整 content)
步骤 3:转换为 requirement
对每个 concept,调用 scripts/concept-to-requirement.py 将 concept + 关联 entity 转换为 requirement:
execute_skill_script(
skill_name: "openmaic-classroom",
script_path: "scripts/concept-to-requirement.py",
input: '{"concept": {"slug": "...", "title": "...", "summary": "...", "content": "..."}, "entities": [{"slug": "...", "title": "...", "summary": "...", "link_type": "..."}], "language": "zh-CN", "depth": "intermediate"}'
)input 参数格式(JSON 字符串,必须通过 `input` 参数传入):
concept(必填): concept 页面对象,包含slug、title、summary、contententities(可选): 关联 entity 数组,每项包含slug、title、summary、link_typelanguage(可选):zh-CN|en-US,默认zh-CNdepth(可选):beginner|intermediate|advanced,默认intermediateaudience(可选): 目标受众描述,默认"相关领域的学习者"
步骤 4:顺序调用 OpenMAIC API
对每个 concept 的 requirement,顺序调用 OpenMAIC 生成 API(concurrency=1):
- 每个 concept → 一个 micro-classroom
- requirement 中标注
micro-classroom - 不可并行,避免配额冲突
步骤 5:生成 manifest(可恢复性)
生成 manifest JSON,记录每个 concept 的生成状态:
{
"kb_id": "...",
"total_concepts": 10,
"generated": ["concept/rag", "concept/llm"],
"failed": [{"slug": "concept/embedding", "error": "..."}],
"pending": ["concept/vector-db"]
}失败时从断点继续:跳过 generated 中的 concept,从 pending 的第一个开始。
关键约束:
- wiki 读取和脚本转换允许 batching
- OpenMAIC API 生成 concurrency=1
- concept.Summary 作为 requirement 核心锚定
- entity 只取 title + summary,不取完整 content
- 无关联 entity 的 concept 仍可生成课堂(缺少实践环节)
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填入解析结果 - 场景 4(概念图遍历):
requirement使用 Phase 1.2 脚本输出中的requirement字段,每个 concept 单独调用 API
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 工具,则引导用户部署 mcp_api_requester(见 MCP 可用性检查)
第二步:确定 Base URL 和认证 Header
| 模式 | Base URL | 认证 Header |
|---|---|---|
| 托管模式 | https://open.maic.chat | Authorization: Bearer <access-code> |
| 本地模式 | 用户提供的地址(已将 localhost/127.0.0.1 替换为 host.docker.internal) | 无 |
第三步:Feature Detection(发送可选功能前)
在发送生成请求前,先查询 GET <BASE_URL>/api/health(托管模式需带 auth header),检查返回的 capabilities 对象:
{
"status": "ok",
"version": "...",
"capabilities": {
"webSearch": true,
"imageGeneration": false,
"videoGeneration": false,
"tts": true
}
}- 只有当
capabilities中某项为true时,才能在生成请求中将对应 feature flag 设为true - 如果服务器未返回
capabilities(旧版本),不要发送任何可选 feature flags
第四步:发送 POST 请求
使用识别到的 HTTP 请求工具发送请求。根据上面确定的模式和 URL 构造请求:
托管模式:
{
"url": "https://open.maic.chat/api/generate-classroom",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer <access-code>"
},
"body": {
"requirement": "..."
}
}本地模式:
{
"url": "<BASE_URL>/api/generate-classroom",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"requirement": "..."
}
}MCP 工具不可用的处理:
告知用户:
未检测到 mcp_api_requester MCP 服务。请先从 https://github.com/yryuu/mcp-api-requester 下载代码并部署,然后在 WeKnora 中注册该 MCP 服务。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: <classroomId>
Classroom URL:
<BASE_URL>/classroom/<classroomId>托管模式的 URL 格式:https://open.maic.chat/classroom/<classroomId>
URL 必须以纯文本独占一行输出,不加粗、不加代码格式、不加 Markdown 链接。
错误处理
| 错误 | 含义 | 处理方式 |
|---|---|---|
| 连接失败 | 网络不通或服务未启动 | 检查 Base URL 是否正确,服务是否启动 |
| 401 | 访问码无效(托管模式) | 告知用户到 open.maic.chat 检查或重新生成访问码 |
| 403 | 每日配额用尽(托管模式) | 告知每日 10 次限制,次日零点重置 |
| 500 | 服务器错误 | 建议稍后重试或切换到本地模式 |
| Provider 配置错误 | 模型/Provider/认证问题 | 引导用户检查 配置或联系管理员 |
多文档 → 课程集
当用户需要将多个文档/知识片段生成课程集时:
1. 收集所有文档内容 2. 为每个文档/主题分别生成 requirement 3. 通过 MCP 工具依次调用生成 API(不可并行,避免配额冲突) 4. 如果 MCP 工具不可用,告知用户先部署 mcp_api_requester(见 MCP 可用性检查) 5. 汇总返回所有 Classroom URL
概念图遍历 → 批量微课堂
当用户需要基于知识图谱概念批量生成课堂时(场景 4),遵循 Phase 1.2 的完整流程。
MVP 课程编排策略:one concept → one micro-classroom
课程类型标注:requirement 中标注 micro-classroom
批处理可恢复性:生成 manifest JSON,记录每个 concept 的生成状态,失败时可从断点继续。
关键约束:
- wiki 读取和脚本转换允许 batching
- OpenMAIC API 生成 concurrency=1
- concept.Summary 作为 requirement 核心锚定
- entity 只取 title + summary,不取完整 content
- 无关联 entity 的 concept 仍可生成课堂(缺少实践环节)
注意事项
- 脚本在 Docker 沙箱中执行,沙箱默认禁用网络访问
- 必须通过 WeKnora MCP 工具调用 OpenMAIC API——不提供 curl 命令作为降级方案
- MCP 工具名称格式为
mcp_{service_name}_{tool_name},根据描述识别 HTTP 请求工具 - 如果 MCP 工具未启用或不可用,告知用户先从 https://github.com/yryuu/mcp-api-requester 下载代码并部署,然后在 WeKnora 中注册该 MCP 服务
- 单次生成任务预计 2-10 分钟,取决于内容复杂度和可选功能
- 托管模式(open.maic.chat)每天最多 10 次生成配额,独立于 Web UI 配额
- 如果用户在同一个 job 仍在运行时要求生成新课程,不要重复提交——先检查已有 job 状态
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": "<ACCESS_CODE>" }成功返回 { "success": true, "valid": true } 并设置 openmaic_access cookie(7 天有效)。
标准响应格式
成功:
{ "success": true, ...additionalFields }错误:
{ "success": false, "errorCode": "<Code>", "error": "<message>", "details?": "<detail>" }---
核心端点
1. 健康检查
GET {base_url}/api/health响应:
{
"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请求体:
{
"requirement": "教学主题描述",
"pdfContent": { "text": "PDF文本内容", "images": [] },
"enableWebSearch": false,
"enableImageGeneration": false,
"enableVideoGeneration": false,
"enableTTS": false,
"agentMode": "default"
}成功响应 (202):
{
"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}响应:
{
"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
}最终成功响应:
{
"success": true,
"status": "succeeded",
"result": {
"classroomId": "Uyh82Y32ZK",
"url": "{base_url}/classroom/Uyh82Y32ZK",
"scenesCount": 6
},
"done": true
}最终失败响应:
{
"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 KeybaseUrl(可选): 提供商基础 URL
响应:
{
"success": true,
"data": {
"text": "提取的文本内容",
"images": [],
"metadata": {
"pageCount": 10,
"fileName": "document.pdf",
"fileSize": 1024000
}
}
}---
4. 课程存储
4a. 获取课程
GET {base_url}/api/classroom?id=<classroomId>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请求体:
{
"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": []
}需求构建指南
将 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]》: [简要内容摘要]
要求:
- 教学深度:[级别]
- 目标受众:[描述]
- 重点覆盖:[关键主题列表]模板 5: 基于概念图遍历(Concept Graph)
当从知识图谱 concept 页面及其关联 entity 生成微课堂时使用此模板。由 scripts/concept-to-requirement.py 自动生成。
输入结构:
{
"concept": { "slug": "concept/rag", "title": "RAG 检索增强生成", "summary": "...", "content": "..." },
"entities": [
{ "slug": "entity/vector-db", "title": "向量数据库", "summary": "...", "link_type": "outlink" },
{ "slug": "entity/embedding", "title": "Embedding 模型", "summary": "...", "link_type": "bidirectional" }
],
"language": "zh-CN",
"depth": "intermediate",
"audience": "相关领域的学习者"
}输出 requirement 结构:
基于知识图谱概念「[concept.title]」,为[audience]创建一个[depth]微课堂(micro-classroom)。
教学锚点:[concept.summary]
学习目标:
- 理解[concept.summary 中的关键句]
核心知识点:
- [从 concept.content 解析的定义/机制]
关联实体(实践环节):
- 案例:[entity.title]:[entity.summary]
- 工具:[entity.title]:[entity.summary]
- 应用场景:[entity.title]:[entity.summary]
- 前置知识:[entity.title]
实践任务:
- 通过 [entity.title] 实践 [concept.title] 的应用
常见误区检查:
- [从 concept.content 解析的误区]
评估提示:
- 请解释 [concept.title] 的核心定义
请使用中文生成课程内容。entity 分类排序规则(纯文本操作,无 LLM/embedding):
- link_type 权重:bidirectional (+3) > outlink (+2) > inlink (+1)
- title token overlap:concept title 分词后与 entity title 的交集数 (+1 per hit, cap +2)
- summary keyword hit:concept summary 关键词在 entity summary 中出现 (+1 per hit, cap +2)
- slug token hit:concept slug token 在 entity slug 中出现 (+1)
- summary 为空扣分 (-2)
- 取 top 3-5 entities,分为 Examples / Tools / Application Scenarios / Prerequisites
概念内容解析逻辑:
- 优先解析 markdown 结构:标题列表(定义段、机制段、案例段、误区段)
- fallback 到前 N 字
示例
示例 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 配额。
#!/usr/bin/env python3
"""
Concept Graph Material → OpenMAIC Requirement 转换器
将 WeKnora wiki 知识图谱中的 concept 页面及其关联 entity 转换为
结构化的 OpenMAIC 课程生成需求描述。两阶段转换:
1. Concept Graph Material → Pedagogical Design JSON
2. Pedagogical Design JSON → requirement string
此脚本仅做数据转换,不涉及网络调用。
用法:
echo '{"concept": {...}, "entities": [...]}' | python scripts/concept-to-requirement.py
python scripts/concept-to-requirement.py --file input.json
"""
import json
import re
import sys
from typing import Any
def _tokenize(text: str) -> list[str]:
"""Simple whitespace + punctuation tokenizer for title/slug overlap."""
return [t.lower() for t in re.split(r"[\s_\-/]+", text) if t]
def _score_entity(
entity: dict[str, Any],
concept_title_tokens: list[str],
concept_summary_keywords: list[str],
concept_slug_tokens: list[str],
) -> int:
"""Score an entity by relevance to the concept (pure text heuristics)."""
score = 0
# link_type scoring
link_type = entity.get("link_type", "")
if link_type == "bidirectional":
score += 3
elif link_type == "outlink":
score += 2
elif link_type == "inlink":
score += 1
# title token overlap
entity_title_tokens = set(_tokenize(entity.get("title", "")))
overlap = entity_title_tokens & set(concept_title_tokens)
score += min(len(overlap), 2)
# summary keyword hit
entity_summary = (entity.get("summary") or "").lower()
keyword_hits = sum(1 for kw in concept_summary_keywords if kw in entity_summary)
score += min(keyword_hits, 2)
# slug token hit
entity_slug_tokens = set(_tokenize(entity.get("slug", "")))
slug_overlap = entity_slug_tokens & set(concept_slug_tokens)
score += min(len(slug_overlap), 1)
# penalty for empty summary
if not entity.get("summary"):
score -= 2
return score
def _classify_entity(
entity: dict[str, Any], concept_title: str
) -> str:
"""Classify an entity into a pedagogical role."""
title = (entity.get("title") or "").lower()
summary = (entity.get("summary") or "").lower()
text = f"{title} {summary}"
tool_keywords = ["工具", "平台", "框架", "库", "sdk", "api", "tool", "platform", "framework", "library"]
example_keywords = ["案例", "实例", "示例", "应用", "case", "example", "application", "demo"]
prereq_keywords = ["前提", "基础", "前置", "先决", "prerequisite", "foundation", "basic"]
if any(kw in text for kw in tool_keywords):
return "Tools"
if any(kw in text for kw in example_keywords):
return "Examples"
if any(kw in text for kw in prereq_keywords):
return "Prerequisites"
return "Application Scenarios"
def _parse_markdown_sections(content: str) -> dict[str, str]:
"""Parse markdown content into sections keyed by heading."""
sections: dict[str, str] = {}
current_heading = ""
current_lines: list[str] = []
for line in content.split("\n"):
heading_match = re.match(r"^(#{1,4})\s+(.+)$", line)
if heading_match:
if current_heading:
sections[current_heading] = "\n".join(current_lines).strip()
current_heading = heading_match.group(2).strip()
current_lines = []
else:
current_lines.append(line)
if current_heading:
sections[current_heading] = "\n".join(current_lines).strip()
return sections
def _extract_key_points(sections: dict[str, str]) -> list[str]:
"""Extract key points from markdown sections (definitions, mechanisms)."""
points: list[str] = []
definition_headings = {"定义", "概念", "概述", "简介", "Definition", "Overview", "Introduction"}
mechanism_headings = {"机制", "原理", "工作原理", "Mechanism", "How it works", "Principle"}
for heading, body in sections.items():
if any(d in heading for d in definition_headings):
first_para = body.split("\n\n")[0].strip()
if first_para:
points.append(first_para[:200])
elif any(m in heading for m in mechanism_headings):
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
points.extend(bullets[:3])
return points[:5]
def _extract_examples(sections: dict[str, str]) -> list[str]:
"""Extract examples from markdown sections."""
example_headings = {"案例", "示例", "实例", "应用场景", "Example", "Use Case", "Application"}
examples: list[str] = []
for heading, body in sections.items():
if any(e in heading for e in example_headings):
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
examples.extend(bullets[:3])
return examples[:3]
def _extract_misconceptions(sections: dict[str, str]) -> list[str]:
"""Extract common misconceptions from markdown sections."""
misconception_headings = {"误区", "常见错误", "误解", "Misconception", "Common mistake", "Pitfall"}
misconceptions: list[str] = []
for heading, body in sections.items():
if any(m in heading for m in misconception_headings):
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
misconceptions.extend(bullets[:3])
return misconceptions[:3]
def build_pedagogical_design(data: dict[str, Any]) -> dict[str, Any]:
"""Stage 1: Concept Graph Material → Pedagogical Design JSON."""
concept = data["concept"]
entities = data.get("entities", [])
language = data.get("language", "zh-CN")
depth = data.get("depth", "intermediate")
audience = data.get("audience", "相关领域的学习者")
concept_title = concept.get("title", "")
concept_summary = concept.get("summary", "")
concept_content = concept.get("content", "")
concept_slug = concept.get("slug", "")
# Parse markdown sections from content
sections = _parse_markdown_sections(concept_content) if concept_content else {}
# Extract pedagogical elements from content
key_points = _extract_key_points(sections)
examples_from_content = _extract_examples(sections)
misconceptions = _extract_misconceptions(sections)
# Score and rank entities
concept_title_tokens = _tokenize(concept_title)
concept_summary_keywords = [w.lower() for w in re.findall(r"\w+", concept_summary) if len(w) > 1]
concept_slug_tokens = _tokenize(concept_slug)
scored_entities = []
for entity in entities:
score = _score_entity(entity, concept_title_tokens, concept_summary_keywords, concept_slug_tokens)
scored_entities.append((score, entity))
scored_entities.sort(key=lambda x: x[0], reverse=True)
# Select top entities (3-5)
top_count = min(max(3, len(scored_entities)), 5)
top_entities = scored_entities[:top_count]
# Classify entities into pedagogical roles
classified: dict[str, list[dict[str, Any]]] = {
"Examples": [],
"Tools": [],
"Application Scenarios": [],
"Prerequisites": [],
}
for score, entity in top_entities:
role = _classify_entity(entity, concept_title)
classified[role].append({
"slug": entity.get("slug", ""),
"title": entity.get("title", ""),
"summary": entity.get("summary", ""),
"link_type": entity.get("link_type", ""),
"relevance_score": score,
})
# Build learning objectives from concept summary
learning_objectives: list[str] = []
if concept_summary:
sentences = re.split(r"[。!?.!?]", concept_summary)
learning_objectives = [f"理解{s.strip()}" for s in sentences if s.strip()][:3]
if not learning_objectives:
learning_objectives = [f"掌握 {concept_title} 的核心概念"]
# Build practice tasks from entity examples
practice_tasks: list[str] = []
for ent in classified.get("Examples", []):
practice_tasks.append(f"通过 {ent['title']} 实践 {concept_title} 的应用")
for ent in classified.get("Application Scenarios", []):
practice_tasks.append(f"分析 {ent['title']} 在 {concept_title} 中的作用")
if not practice_tasks and top_entities:
_, first_ent = top_entities[0]
practice_tasks.append(f"结合 {first_ent.get('title', '相关实体')} 理解 {concept_title} 的实际应用")
# Build prerequisites from entity prerequisites
prerequisites: list[str] = []
for ent in classified.get("Prerequisites", []):
prerequisites.append(ent["title"])
# Build assessment prompts
assessment_prompts: list[str] = []
if concept_summary:
assessment_prompts.append(f"请解释 {concept_title} 的核心定义")
if key_points:
assessment_prompts.append(f"请描述 {concept_title} 的工作机制")
if classified.get("Examples"):
assessment_prompts.append(f"请举例说明 {concept_title} 的实际应用")
# Build warnings from misconceptions
warnings: list[str] = []
for m in misconceptions:
warnings.append(f"常见误区:{m}")
return {
"concept_slug": concept_slug,
"title": concept_title,
"teaching_anchor": concept_summary or concept_title,
"learning_objectives": learning_objectives,
"key_points": key_points,
"examples": examples_from_content,
"practice_tasks": practice_tasks,
"prerequisites": prerequisites,
"misconception_checks": misconceptions,
"assessment_prompts": assessment_prompts,
"warnings": warnings,
"classified_entities": classified,
}
def build_requirement(design: dict[str, Any], data: dict[str, Any]) -> str:
"""Stage 2: Pedagogical Design JSON → requirement string."""
concept = data["concept"]
depth = data.get("depth", "intermediate")
audience = data.get("audience", "相关领域的学习者")
language = data.get("language", "zh-CN")
depth_map = {"beginner": "入门", "intermediate": "中级", "advanced": "高级"}
depth_cn = depth_map.get(depth, "中级")
parts: list[str] = []
# Header
parts.append(f"基于知识图谱概念「{design['title']}」,为{audience}创建一个{depth_cn}微课堂(micro-classroom)。")
parts.append("")
# Teaching anchor
parts.append(f"教学锚点:{design['teaching_anchor']}")
parts.append("")
# Learning objectives
if design["learning_objectives"]:
parts.append("学习目标:")
for obj in design["learning_objectives"]:
parts.append(f" - {obj}")
parts.append("")
# Key points
if design["key_points"]:
parts.append("核心知识点:")
for kp in design["key_points"]:
parts.append(f" - {kp}")
parts.append("")
# Classified entities as practice context
classified = design.get("classified_entities", {})
entity_sections = []
for role in ("Examples", "Tools", "Application Scenarios", "Prerequisites"):
ents = classified.get(role, [])
if ents:
role_cn = {
"Examples": "案例",
"Tools": "工具",
"Application Scenarios": "应用场景",
"Prerequisites": "前置知识",
}[role]
ent_descs = [f"{e['title']}" + (f":{e['summary'][:80]}" if e.get("summary") else "") for e in ents]
entity_sections.append(f"{role_cn}:{';'.join(ent_descs)}")
if entity_sections:
parts.append("关联实体(实践环节):")
for section in entity_sections:
parts.append(f" - {section}")
parts.append("")
# Practice tasks
if design["practice_tasks"]:
parts.append("实践任务:")
for task in design["practice_tasks"]:
parts.append(f" - {task}")
parts.append("")
# Misconception checks
if design["misconception_checks"]:
parts.append("常见误区检查:")
for mc in design["misconception_checks"]:
parts.append(f" - {mc}")
parts.append("")
# Assessment
if design["assessment_prompts"]:
parts.append("评估提示:")
for ap in design["assessment_prompts"]:
parts.append(f" - {ap}")
parts.append("")
# Language directive
if language == "zh-CN":
parts.append("请使用中文生成课程内容。")
# Concept content fallback
concept_content = concept.get("content", "")
if concept_content and len(concept_content) > 200:
parts.append("")
parts.append(f"参考内容(前500字):{concept_content[:500]}")
return "\n".join(parts)
def process(input_data: dict[str, Any]) -> dict[str, Any]:
"""Main processing: two-stage conversion."""
concept = input_data.get("concept")
if not concept:
return {
"requirement": "",
"pedagogical_design": {},
"metadata": {"error": "Missing 'concept' in input"},
}
entities = input_data.get("entities", [])
# Stage 1: Build pedagogical design
design = build_pedagogical_design(input_data)
# Stage 2: Build requirement string
requirement = build_requirement(design, input_data)
return {
"requirement": requirement,
"pedagogical_design": design,
"metadata": {
"concept_slug": concept.get("slug", ""),
"entity_count": len(entities),
"depth": input_data.get("depth", "intermediate"),
"language": input_data.get("language", "zh-CN"),
},
}
def main() -> None:
"""Entry point: read from stdin or file, output JSON."""
import argparse
parser = argparse.ArgumentParser(description="Concept Graph → 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 \'{"concept": {...}, "entities": [...]}\' | python concept-to-requirement.py\n'
" python concept-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()
#!/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()