WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
@@ -0,0 +1,58 @@
---
name: 引用生成器
description: 自动生成规范引用格式。当用户需要生成参考文献、引用来源、标注知识库内容出处、或要求提供引用信息时使用此技能。
---
# Citation Generator
为知识库检索结果生成规范的引用格式。
## 核心能力
1. **来源标注**: 为回答中使用的每个知识点标注来源
2. **格式化引用**: 支持多种引用格式(APA、MLA、Chicago、简化格式)
3. **参考文献列表**: 在回答末尾生成完整的参考文献列表
## 引用格式
### 简化格式(默认)
对于知识库内容,使用以下格式:
```
[文档名称, 第X页/段落X]
```
示例:
```
根据公司政策[员工手册2024.pdf, 第15页],年假申请需提前...
```
### APA 格式
```
作者. (年份). 标题. 来源.
```
### 参考文献列表格式
在回答末尾,使用以下格式列出所有引用:
```
---
**参考文献**
1. [1] 文档A - 第X章/第Y页
2. [2] 文档B - 第Z段
```
## 使用指南
1. **检索内容时**: 记录每个检索结果的来源信息(文档名、页码、分块ID)
2. **引用时**: 在使用知识点后立即标注来源
3. **汇总时**: 在回答末尾列出完整参考文献
## 注意事项
- 如果检索结果未提供页码,使用分块或段落编号
- 对于同一文档的多次引用,可合并为一条
- 引用应准确对应原文内容,不可虚构来源
+175
View File
@@ -0,0 +1,175 @@
---
name: 数据处理器
description: 数据处理与分析技能。当用户需要对知识库检索结果进行数据分析、统计计算、格式转换、数据提取或生成报告时使用此技能。支持 Python 脚本执行进行高级数据处理。
---
# Data Processor
企业级知识库数据处理与分析技能,用于处理 RAG 检索结果和执行数据分析任务。
## 核心能力
1. **数据分析**: 对检索到的文档数据进行统计分析
2. **格式转换**: JSON/CSV/Markdown 等格式相互转换
3. **数据提取**: 从非结构化文本中提取结构化信息
4. **报告生成**: 生成数据分析报告和摘要
## 使用场景
当用户请求涉及以下内容时,使用此技能:
- "分析这些数据"、"统计一下"、"计算总数/平均值"
- "转换为 JSON/CSV 格式"
- "提取关键信息"、"整理成表格"
- "生成报告"、"数据汇总"
## 可用脚本
### 1. analyze.py - 数据分析脚本
分析输入的 JSON 数据,生成统计报告。
**命令行用法** (仅供参考):
```bash
# 通过 stdin 传入 JSON 数据
echo '{"items": [1, 2, 3, 4, 5]}' | python scripts/analyze.py
# 或传入文件路径(需要文件实际存在)
python scripts/analyze.py --file data.json
```
**使用 execute_skill_script 工具时**:
- 如果你有内存中的数据(如 JSON 字符串),使用 `input` 参数传入,不要使用 `args`
- `--file` 参数仅用于读取技能目录中已存在的文件,不适用于传递内存数据
```json
// ✅ 正确:通过 input 传入数据
{
"skill_name": "数据处理器",
"script_path": "scripts/analyze.py",
"input": "{\"items\": [1, 2, 3], \"query\": \"统计分析\"}"
}
// ❌ 错误:--file 需要文件路径,不能单独使用
{
"skill_name": "数据处理器",
"script_path": "scripts/analyze.py",
"args": ["--file"],
"input": "{...}"
}
```
**输入格式**:
```json
{
"items": [数据项数组],
"query": "可选的查询描述"
}
```
**输出**: JSON 格式的统计结果,包含计数、求和、平均值等。
### 2. format_converter.py - 格式转换脚本
在 JSON、CSV、Markdown 表格之间转换数据。
**用法**:
```bash
# JSON 转 CSV
echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to csv
# JSON 转 Markdown 表格
echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to markdown
# CSV 转 JSON
echo 'name,value\nA,1' | python scripts/format_converter.py --from csv --to json
```
### 3. extract_info.py - 信息提取脚本
从文本中提取结构化信息(数字、日期、关键词等)。
**用法**:
```bash
echo "2024年销售额为100万元,同比增长15%" | python scripts/extract_info.py
```
**输出**:
```json
{
"numbers": ["100", "15"],
"dates": ["2024年"],
"percentages": ["15%"],
"amounts": ["100万元"]
}
```
## 处理流程
### 分析 RAG 检索结果
当需要分析知识库检索结果时:
1. 收集检索到的文档片段
2. 提取关键数据点
3. 使用 `analyze.py` 进行统计
4. 整理并呈现分析结果
**示例**:
```
用户: "帮我统计知识库中提到的所有产品销售数据"
步骤:
1. 使用 knowledge_search 检索相关文档
2. 整理数据为 JSON 格式
3. 调用 execute_skill_script:
- skill_name: "data-processor"
- script_path: "scripts/analyze.py"
- 通过 stdin 传入数据
4. 解析输出并生成报告
```
### 数据格式转换
当用户需要特定格式输出时:
1. 整理数据为标准 JSON 格式
2. 使用 `format_converter.py` 转换
3. 返回目标格式结果
## 最佳实践
1. **数据预处理**: 调用脚本前,确保数据格式正确
2. **错误处理**: 检查脚本执行结果,处理异常情况
3. **结果验证**: 验证输出结果的合理性
4. **渐进处理**: 大数据量时分批处理
## 输出格式
分析结果示例:
```markdown
## 数据分析报告
### 基本统计
- 数据条数: 50
- 数值总和: 1,234,567
- 平均值: 24,691.34
- 最大值: 99,999
- 最小值: 100
### 分布情况
| 区间 | 数量 | 占比 |
|------|------|------|
| 0-1000 | 10 | 20% |
| 1000-10000 | 25 | 50% |
| >10000 | 15 | 30% |
### 结论
根据数据分析,XXX...
```
## 注意事项
- 脚本在 Docker 沙箱中执行,确保安全隔离
- 执行超时默认为 60 秒
- 输入数据大小有限制,大文件请分批处理
- 脚本输出为 JSON 格式,便于后续处理
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""
数据分析脚本 - 用于分析 RAG 检索结果和知识库数据
支持功能:
- 基本统计(计数、求和、平均值、最大/最小值)
- 数值分布分析
- 文本统计(词频、字符数)
用法:
# 通过 stdin 传入 JSON 数据
echo '{"items": [1, 2, 3, 4, 5]}' | python analyze.py
# 通过参数传入文件
python analyze.py --file data.json
# 指定分析类型
echo '{"items": [1, 2, 3]}' | python analyze.py --type numeric
"""
import sys
import json
import argparse
from collections import Counter
def analyze_numeric(data: list) -> dict:
"""分析数值数据"""
if not data:
return {"error": "空数据集"}
# 过滤出数值
numbers = [x for x in data if isinstance(x, (int, float))]
if not numbers:
return {"error": "无有效数值数据"}
numbers.sort()
n = len(numbers)
result = {
"count": n,
"sum": sum(numbers),
"mean": sum(numbers) / n,
"min": min(numbers),
"max": max(numbers),
"median": numbers[n // 2] if n % 2 == 1 else (numbers[n // 2 - 1] + numbers[n // 2]) / 2,
}
# 计算标准差
mean = result["mean"]
variance = sum((x - mean) ** 2 for x in numbers) / n
result["std_dev"] = variance ** 0.5
# 分布统计
if n >= 5:
result["quartiles"] = {
"q1": numbers[n // 4],
"q2": result["median"],
"q3": numbers[3 * n // 4]
}
return result
def analyze_text(data: list) -> dict:
"""分析文本数据"""
if not data:
return {"error": "空数据集"}
texts = [str(x) for x in data if x]
# 基本统计
total_chars = sum(len(t) for t in texts)
total_words = sum(len(t.split()) for t in texts)
# 词频统计(简单分词)
all_words = []
for text in texts:
words = text.split()
all_words.extend(w.strip('.,!?;:""\'()[]{}') for w in words if w.strip())
word_freq = Counter(all_words)
result = {
"count": len(texts),
"total_chars": total_chars,
"total_words": total_words,
"avg_chars_per_item": total_chars / len(texts) if texts else 0,
"avg_words_per_item": total_words / len(texts) if texts else 0,
"top_words": dict(word_freq.most_common(10)),
"unique_words": len(word_freq)
}
return result
def analyze_mixed(data: list) -> dict:
"""分析混合数据"""
if not data:
return {"error": "空数据集"}
# 类型统计
type_counts = Counter(type(x).__name__ for x in data)
result = {
"total_items": len(data),
"type_distribution": dict(type_counts),
}
# 分别分析数值和文本
numbers = [x for x in data if isinstance(x, (int, float))]
texts = [x for x in data if isinstance(x, str)]
if numbers:
result["numeric_analysis"] = analyze_numeric(numbers)
if texts:
result["text_analysis"] = analyze_text(texts)
return result
def analyze_dict_list(data: list) -> dict:
"""分析字典列表(如数据库查询结果)"""
if not data:
return {"error": "空数据集"}
if not all(isinstance(x, dict) for x in data):
return {"error": "数据格式不正确,需要字典列表"}
result = {
"record_count": len(data),
"fields": {},
}
# 获取所有字段
all_keys = set()
for item in data:
all_keys.update(item.keys())
# 分析每个字段
for key in all_keys:
values = [item.get(key) for item in data if key in item]
# 判断字段类型
non_null_values = [v for v in values if v is not None]
if not non_null_values:
result["fields"][key] = {"type": "all_null", "null_count": len(values)}
continue
sample = non_null_values[0]
if isinstance(sample, (int, float)):
field_analysis = analyze_numeric(non_null_values)
field_analysis["type"] = "numeric"
elif isinstance(sample, str):
field_analysis = analyze_text(non_null_values)
field_analysis["type"] = "text"
else:
field_analysis = {"type": type(sample).__name__, "count": len(non_null_values)}
field_analysis["null_count"] = len(values) - len(non_null_values)
result["fields"][key] = field_analysis
return result
def main():
parser = argparse.ArgumentParser(description="数据分析工具")
parser.add_argument("--file", "-f", help="输入文件路径")
parser.add_argument("--type", "-t", choices=["numeric", "text", "mixed", "auto"],
default="auto", help="分析类型")
parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出")
args = parser.parse_args()
# 读取输入
try:
if args.file:
with open(args.file, 'r', encoding='utf-8') as f:
raw_data = f.read()
else:
raw_data = sys.stdin.read()
if not raw_data.strip():
print(json.dumps({"error": "空输入"}))
return
data = json.loads(raw_data)
except json.JSONDecodeError as e:
print(json.dumps({"error": f"JSON 解析错误: {str(e)}"}))
return
except FileNotFoundError:
print(json.dumps({"error": f"文件未找到: {args.file}"}))
return
except Exception as e:
print(json.dumps({"error": f"读取错误: {str(e)}"}))
return
# 提取数据
items = None
if isinstance(data, dict):
if "items" in data:
items = data["items"]
elif "data" in data:
items = data["data"]
elif "results" in data:
items = data["results"]
else:
# 假设整个 dict 是单条记录,包装成列表
items = [data]
elif isinstance(data, list):
items = data
else:
print(json.dumps({"error": "不支持的数据格式,需要列表或包含 items/data/results 的字典"}))
return
# 根据类型分析
if args.type == "auto":
# 自动检测
if items and all(isinstance(x, dict) for x in items):
result = analyze_dict_list(items)
elif items and all(isinstance(x, (int, float)) for x in items):
result = analyze_numeric(items)
elif items and all(isinstance(x, str) for x in items):
result = analyze_text(items)
else:
result = analyze_mixed(items)
elif args.type == "numeric":
result = analyze_numeric(items)
elif args.type == "text":
result = analyze_text(items)
else:
result = analyze_mixed(items)
# 添加元信息
output = {
"success": True,
"analysis": result,
"metadata": {
"input_type": type(data).__name__,
"item_count": len(items) if items else 0,
"analysis_type": args.type
}
}
# 输出
indent = 2 if args.pretty else None
print(json.dumps(output, ensure_ascii=False, indent=indent))
if __name__ == "__main__":
main()
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""
信息提取脚本 - 从文本中提取结构化信息
提取内容:
- 数字
- 日期
- 百分比
- 金额
- 邮箱
- URL
- 电话号码
用法:
echo "2024年销售额为100万元,同比增长15%" | python extract_info.py
# 指定提取类型
echo "联系我: test@example.com 或 13800138000" | python extract_info.py --types email,phone
"""
import sys
import json
import argparse
import re
def extract_numbers(text: str) -> list:
"""提取数字"""
# 匹配整数和小数
pattern = r'-?\d+(?:\.\d+)?'
numbers = re.findall(pattern, text)
# 转换为数值
result = []
for n in numbers:
try:
if '.' in n:
result.append(float(n))
else:
result.append(int(n))
except ValueError:
result.append(n)
return result
def extract_dates(text: str) -> list:
"""提取日期"""
patterns = [
r'\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?', # 2024-01-01 或 2024年1月1日
r'\d{4}[-/年]\d{1,2}[月]?', # 2024-01 或 2024年1月
r'\d{4}年', # 2024年
r'\d{1,2}[-/月]\d{1,2}[日]?', # 01-01 或 1月1日
]
dates = []
for pattern in patterns:
matches = re.findall(pattern, text)
dates.extend(matches)
return list(set(dates))
def extract_percentages(text: str) -> list:
"""提取百分比"""
pattern = r'-?\d+(?:\.\d+)?%'
return re.findall(pattern, text)
def extract_amounts(text: str) -> list:
"""提取金额"""
patterns = [
r'[¥$€£]\s*\d+(?:,\d{3})*(?:\.\d+)?', # ¥100.00
r'\d+(?:,\d{3})*(?:\.\d+)?\s*[元万亿美金]', # 100万元
r'\d+(?:\.\d+)?[百千万亿]+[元]?', # 100万
]
amounts = []
for pattern in patterns:
matches = re.findall(pattern, text)
amounts.extend(matches)
return list(set(amounts))
def extract_emails(text: str) -> list:
"""提取邮箱"""
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
return re.findall(pattern, text)
def extract_urls(text: str) -> list:
"""提取 URL"""
pattern = r'https?://[^\s<>"{}|\\^`\[\]]+'
return re.findall(pattern, text)
def extract_phones(text: str) -> list:
"""提取电话号码"""
patterns = [
r'1[3-9]\d{9}', # 手机号
r'\d{3,4}[-\s]?\d{7,8}', # 固话
r'\+\d{1,3}[-\s]?\d{10,12}', # 国际号码
]
phones = []
for pattern in patterns:
matches = re.findall(pattern, text)
phones.extend(matches)
return list(set(phones))
def extract_keywords(text: str, min_len: int = 2) -> list:
"""提取关键词(中文和英文)"""
# 中文关键词
chinese_pattern = r'[\u4e00-\u9fa5]{2,}'
chinese_words = re.findall(chinese_pattern, text)
# 英文关键词
english_pattern = r'[a-zA-Z]{3,}'
english_words = re.findall(english_pattern, text)
# 统计词频
from collections import Counter
words = chinese_words + [w.lower() for w in english_words]
# 过滤停用词
stopwords = {'的', '是', '在', '了', '和', '与', '或', '为', '有', '这', '那', '等',
'the', 'is', 'are', 'was', 'were', 'and', 'or', 'for', 'with', 'this'}
words = [w for w in words if w not in stopwords and len(w) >= min_len]
word_freq = Counter(words)
return [{"word": w, "count": c} for w, c in word_freq.most_common(20)]
def main():
parser = argparse.ArgumentParser(description="信息提取工具")
parser.add_argument("--types", "-t",
help="要提取的类型,逗号分隔 (numbers,dates,percentages,amounts,emails,urls,phones,keywords)")
parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出")
args = parser.parse_args()
# 读取输入
try:
text = sys.stdin.read()
if not text.strip():
print(json.dumps({"error": "空输入"}))
return
except Exception as e:
print(json.dumps({"error": f"读取错误: {str(e)}"}))
return
# 确定要提取的类型
all_types = ["numbers", "dates", "percentages", "amounts", "emails", "urls", "phones", "keywords"]
if args.types:
extract_types = [t.strip().lower() for t in args.types.split(",")]
else:
extract_types = all_types
# 提取信息
result = {
"success": True,
"text_length": len(text),
"extracted": {}
}
extractors = {
"numbers": extract_numbers,
"dates": extract_dates,
"percentages": extract_percentages,
"amounts": extract_amounts,
"emails": extract_emails,
"urls": extract_urls,
"phones": extract_phones,
"keywords": extract_keywords,
}
for ext_type in extract_types:
if ext_type in extractors:
try:
extracted = extractors[ext_type](text)
if extracted:
result["extracted"][ext_type] = extracted
except Exception as e:
result["extracted"][ext_type] = {"error": str(e)}
# 统计
result["summary"] = {
"total_extractions": sum(len(v) if isinstance(v, list) else 0
for v in result["extracted"].values()),
"types_found": list(result["extracted"].keys())
}
# 输出
indent = 2 if args.pretty else None
print(json.dumps(result, ensure_ascii=False, indent=indent))
if __name__ == "__main__":
main()
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""
格式转换脚本 - JSON/CSV/Markdown 相互转换
用法:
# JSON 转 CSV
echo '[{"name": "A", "value": 1}]' | python format_converter.py --to csv
# JSON 转 Markdown 表格
echo '[{"name": "A", "value": 1}]' | python format_converter.py --to markdown
# CSV 转 JSON
cat data.csv | python format_converter.py --from csv --to json
"""
import sys
import json
import argparse
import csv
import io
def json_to_csv(data: list) -> str:
"""将 JSON 列表转换为 CSV"""
if not data:
return ""
if not all(isinstance(x, dict) for x in data):
raise ValueError("JSON 数据必须是字典列表")
# 获取所有字段
fieldnames = []
for item in data:
for key in item.keys():
if key not in fieldnames:
fieldnames.append(key)
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(data)
return output.getvalue()
def csv_to_json(csv_text: str) -> list:
"""将 CSV 转换为 JSON 列表"""
reader = csv.DictReader(io.StringIO(csv_text))
return list(reader)
def json_to_markdown(data: list) -> str:
"""将 JSON 列表转换为 Markdown 表格"""
if not data:
return ""
if not all(isinstance(x, dict) for x in data):
raise ValueError("JSON 数据必须是字典列表")
# 获取所有字段
fieldnames = []
for item in data:
for key in item.keys():
if key not in fieldnames:
fieldnames.append(key)
# 构建表头
lines = []
lines.append("| " + " | ".join(fieldnames) + " |")
lines.append("| " + " | ".join(["---"] * len(fieldnames)) + " |")
# 构建数据行
for item in data:
row = []
for field in fieldnames:
value = item.get(field, "")
# 转义 Markdown 特殊字符
str_value = str(value) if value is not None else ""
str_value = str_value.replace("|", "\\|")
row.append(str_value)
lines.append("| " + " | ".join(row) + " |")
return "\n".join(lines)
def markdown_to_json(md_text: str) -> list:
"""将 Markdown 表格转换为 JSON 列表"""
lines = [line.strip() for line in md_text.strip().split("\n") if line.strip()]
if len(lines) < 2:
raise ValueError("无效的 Markdown 表格")
# 解析表头
header_line = lines[0]
if not header_line.startswith("|"):
raise ValueError("无效的 Markdown 表格格式")
headers = [h.strip() for h in header_line.strip("|").split("|")]
# 跳过分隔行
data_lines = lines[2:] if len(lines) > 2 else []
# 解析数据
result = []
for line in data_lines:
if not line.startswith("|"):
continue
values = [v.strip() for v in line.strip("|").split("|")]
item = {}
for i, header in enumerate(headers):
if i < len(values):
item[header] = values[i]
result.append(item)
return result
def detect_format(text: str) -> str:
"""自动检测输入格式"""
text = text.strip()
if text.startswith("[") or text.startswith("{"):
return "json"
elif text.startswith("|"):
return "markdown"
elif "," in text.split("\n")[0]:
return "csv"
else:
return "unknown"
def main():
parser = argparse.ArgumentParser(description="数据格式转换工具")
parser.add_argument("--from", "-f", dest="from_format",
choices=["json", "csv", "markdown", "auto"],
default="auto", help="输入格式")
parser.add_argument("--to", "-t", dest="to_format",
choices=["json", "csv", "markdown"],
required=True, help="输出格式")
parser.add_argument("--pretty", "-p", action="store_true", help="格式化输出")
args = parser.parse_args()
# 读取输入
try:
raw_input = sys.stdin.read()
if not raw_input.strip():
print(json.dumps({"error": "空输入"}))
return
except Exception as e:
print(json.dumps({"error": f"读取错误: {str(e)}"}))
return
# 检测输入格式
from_format = args.from_format
if from_format == "auto":
from_format = detect_format(raw_input)
if from_format == "unknown":
print(json.dumps({"error": "无法自动检测输入格式"}))
return
# 转换为中间格式(JSON 列表)
try:
if from_format == "json":
data = json.loads(raw_input)
if isinstance(data, dict):
# 尝试提取列表
if "items" in data:
data = data["items"]
elif "data" in data:
data = data["data"]
else:
data = [data]
if not isinstance(data, list):
data = [data]
elif from_format == "csv":
data = csv_to_json(raw_input)
elif from_format == "markdown":
data = markdown_to_json(raw_input)
else:
print(json.dumps({"error": f"不支持的输入格式: {from_format}"}))
return
except Exception as e:
print(json.dumps({"error": f"解析输入失败: {str(e)}"}))
return
# 转换为目标格式
try:
if args.to_format == "json":
indent = 2 if args.pretty else None
output = json.dumps(data, ensure_ascii=False, indent=indent)
elif args.to_format == "csv":
output = json_to_csv(data)
elif args.to_format == "markdown":
output = json_to_markdown(data)
else:
print(json.dumps({"error": f"不支持的输出格式: {args.to_format}"}))
return
print(output)
except Exception as e:
print(json.dumps({"error": f"转换失败: {str(e)}"}))
return
if __name__ == "__main__":
main()
+375
View File
@@ -0,0 +1,375 @@
---
name: 文档协作
description: 引导用户通过结构化的文档共同编写工作流程。当用户想撰写文档、提案、技术规范、决策文档或类似结构化内容时使用。该工作流程帮助用户高效传递上下文,通过迭代优化内容,并验证文档对读者有效。当用户提到写文档、创建提案、起草规范或类似文档任务时触发。
---
# Doc Co-Authoring Workflow
This skill provides a structured workflow for guiding users through collaborative document creation. Act as an active guide, walking users through three stages: Context Gathering, Refinement & Structure, and Reader Testing.
## When to Offer This Workflow
**Trigger conditions:**
- User mentions writing documentation: "write a doc", "draft a proposal", "create a spec", "write up"
- User mentions specific doc types: "PRD", "design doc", "decision doc", "RFC"
- User seems to be starting a substantial writing task
**Initial offer:**
Offer the user a structured workflow for co-authoring the document. Explain the three stages:
1. **Context Gathering**: User provides all relevant context while Claude asks clarifying questions
2. **Refinement & Structure**: Iteratively build each section through brainstorming and editing
3. **Reader Testing**: Test the doc with a fresh Claude (no context) to catch blind spots before others read it
Explain that this approach helps ensure the doc works well when others read it (including when they paste it into Claude). Ask if they want to try this workflow or prefer to work freeform.
If user declines, work freeform. If user accepts, proceed to Stage 1.
## Stage 1: Context Gathering
**Goal:** Close the gap between what the user knows and what Claude knows, enabling smart guidance later.
### Initial Questions
Start by asking the user for meta-context about the document:
1. What type of document is this? (e.g., technical spec, decision doc, proposal)
2. Who's the primary audience?
3. What's the desired impact when someone reads this?
4. Is there a template or specific format to follow?
5. Any other constraints or context to know?
Inform them they can answer in shorthand or dump information however works best for them.
**If user provides a template or mentions a doc type:**
- Ask if they have a template document to share
- If they provide a link to a shared document, use the appropriate integration to fetch it
- If they provide a file, read it
**If user mentions editing an existing shared document:**
- Use the appropriate integration to read the current state
- Check for images without alt-text
- If images exist without alt-text, explain that when others use Claude to understand the doc, Claude won't be able to see them. Ask if they want alt-text generated. If so, request they paste each image into chat for descriptive alt-text generation.
### Info Dumping
Once initial questions are answered, encourage the user to dump all the context they have. Request information such as:
- Background on the project/problem
- Related team discussions or shared documents
- Why alternative solutions aren't being used
- Organizational context (team dynamics, past incidents, politics)
- Timeline pressures or constraints
- Technical architecture or dependencies
- Stakeholder concerns
Advise them not to worry about organizing it - just get it all out. Offer multiple ways to provide context:
- Info dump stream-of-consciousness
- Point to team channels or threads to read
- Link to shared documents
**If integrations are available** (e.g., Slack, Teams, Google Drive, SharePoint, or other MCP servers), mention that these can be used to pull in context directly.
**If no integrations are detected and in Claude.ai or Claude app:** Suggest they can enable connectors in their Claude settings to allow pulling context from messaging apps and document storage directly.
Inform them clarifying questions will be asked once they've done their initial dump.
**During context gathering:**
- If user mentions team channels or shared documents:
- If integrations available: Inform them the content will be read now, then use the appropriate integration
- If integrations not available: Explain lack of access. Suggest they enable connectors in Claude settings, or paste the relevant content directly.
- If user mentions entities/projects that are unknown:
- Ask if connected tools should be searched to learn more
- Wait for user confirmation before searching
- As user provides context, track what's being learned and what's still unclear
**Asking clarifying questions:**
When user signals they've done their initial dump (or after substantial context provided), ask clarifying questions to ensure understanding:
Generate 5-10 numbered questions based on gaps in the context.
Inform them they can use shorthand to answer (e.g., "1: yes, 2: see #channel, 3: no because backwards compat"), link to more docs, point to channels to read, or just keep info-dumping. Whatever's most efficient for them.
**Exit condition:**
Sufficient context has been gathered when questions show understanding - when edge cases and trade-offs can be asked about without needing basics explained.
**Transition:**
Ask if there's any more context they want to provide at this stage, or if it's time to move on to drafting the document.
If user wants to add more, let them. When ready, proceed to Stage 2.
## Stage 2: Refinement & Structure
**Goal:** Build the document section by section through brainstorming, curation, and iterative refinement.
**Instructions to user:**
Explain that the document will be built section by section. For each section:
1. Clarifying questions will be asked about what to include
2. 5-20 options will be brainstormed
3. User will indicate what to keep/remove/combine
4. The section will be drafted
5. It will be refined through surgical edits
Start with whichever section has the most unknowns (usually the core decision/proposal), then work through the rest.
**Section ordering:**
If the document structure is clear:
Ask which section they'd like to start with.
Suggest starting with whichever section has the most unknowns. For decision docs, that's usually the core proposal. For specs, it's typically the technical approach. Summary sections are best left for last.
If user doesn't know what sections they need:
Based on the type of document and template, suggest 3-5 sections appropriate for the doc type.
Ask if this structure works, or if they want to adjust it.
**Once structure is agreed:**
Create the initial document structure with placeholder text for all sections.
**If access to artifacts is available:**
Use `create_file` to create an artifact. This gives both Claude and the user a scaffold to work from.
Inform them that the initial structure with placeholders for all sections will be created.
Create artifact with all section headers and brief placeholder text like "[To be written]" or "[Content here]".
Provide the scaffold link and indicate it's time to fill in each section.
**If no access to artifacts:**
Create a markdown file in the working directory. Name it appropriately (e.g., `decision-doc.md`, `technical-spec.md`).
Inform them that the initial structure with placeholders for all sections will be created.
Create file with all section headers and placeholder text.
Confirm the filename has been created and indicate it's time to fill in each section.
**For each section:**
### Step 1: Clarifying Questions
Announce work will begin on the [SECTION NAME] section. Ask 5-10 clarifying questions about what should be included:
Generate 5-10 specific questions based on context and section purpose.
Inform them they can answer in shorthand or just indicate what's important to cover.
### Step 2: Brainstorming
For the [SECTION NAME] section, brainstorm [5-20] things that might be included, depending on the section's complexity. Look for:
- Context shared that might have been forgotten
- Angles or considerations not yet mentioned
Generate 5-20 numbered options based on section complexity. At the end, offer to brainstorm more if they want additional options.
### Step 3: Curation
Ask which points should be kept, removed, or combined. Request brief justifications to help learn priorities for the next sections.
Provide examples:
- "Keep 1,4,7,9"
- "Remove 3 (duplicates 1)"
- "Remove 6 (audience already knows this)"
- "Combine 11 and 12"
**If user gives freeform feedback** (e.g., "looks good" or "I like most of it but...") instead of numbered selections, extract their preferences and proceed. Parse what they want kept/removed/changed and apply it.
### Step 4: Gap Check
Based on what they've selected, ask if there's anything important missing for the [SECTION NAME] section.
### Step 5: Drafting
Use `str_replace` to replace the placeholder text for this section with the actual drafted content.
Announce the [SECTION NAME] section will be drafted now based on what they've selected.
**If using artifacts:**
After drafting, provide a link to the artifact.
Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections.
**If using a file (no artifacts):**
After drafting, confirm completion.
Inform them the [SECTION NAME] section has been drafted in [filename]. Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections.
**Key instruction for user (include when drafting the first section):**
Provide a note: Instead of editing the doc directly, ask them to indicate what to change. This helps learning of their style for future sections. For example: "Remove the X bullet - already covered by Y" or "Make the third paragraph more concise".
### Step 6: Iterative Refinement
As user provides feedback:
- Use `str_replace` to make edits (never reprint the whole doc)
- **If using artifacts:** Provide link to artifact after each edit
- **If using files:** Just confirm edits are complete
- If user edits doc directly and asks to read it: mentally note the changes they made and keep them in mind for future sections (this shows their preferences)
**Continue iterating** until user is satisfied with the section.
### Quality Checking
After 3 consecutive iterations with no substantial changes, ask if anything can be removed without losing important information.
When section is done, confirm [SECTION NAME] is complete. Ask if ready to move to the next section.
**Repeat for all sections.**
### Near Completion
As approaching completion (80%+ of sections done), announce intention to re-read the entire document and check for:
- Flow and consistency across sections
- Redundancy or contradictions
- Anything that feels like "slop" or generic filler
- Whether every sentence carries weight
Read entire document and provide feedback.
**When all sections are drafted and refined:**
Announce all sections are drafted. Indicate intention to review the complete document one more time.
Review for overall coherence, flow, completeness.
Provide any final suggestions.
Ask if ready to move to Reader Testing, or if they want to refine anything else.
## Stage 3: Reader Testing
**Goal:** Test the document with a fresh Claude (no context bleed) to verify it works for readers.
**Instructions to user:**
Explain that testing will now occur to see if the document actually works for readers. This catches blind spots - things that make sense to the authors but might confuse others.
### Testing Approach
**If access to sub-agents is available (e.g., in Claude Code):**
Perform the testing directly without user involvement.
### Step 1: Predict Reader Questions
Announce intention to predict what questions readers might ask when trying to discover this document.
Generate 5-10 questions that readers would realistically ask.
### Step 2: Test with Sub-Agent
Announce that these questions will be tested with a fresh Claude instance (no context from this conversation).
For each question, invoke a sub-agent with just the document content and the question.
Summarize what Reader Claude got right/wrong for each question.
### Step 3: Run Additional Checks
Announce additional checks will be performed.
Invoke sub-agent to check for ambiguity, false assumptions, contradictions.
Summarize any issues found.
### Step 4: Report and Fix
If issues found:
Report that Reader Claude struggled with specific issues.
List the specific issues.
Indicate intention to fix these gaps.
Loop back to refinement for problematic sections.
---
**If no access to sub-agents (e.g., claude.ai web interface):**
The user will need to do the testing manually.
### Step 1: Predict Reader Questions
Ask what questions people might ask when trying to discover this document. What would they type into Claude.ai?
Generate 5-10 questions that readers would realistically ask.
### Step 2: Setup Testing
Provide testing instructions:
1. Open a fresh Claude conversation: https://claude.ai
2. Paste or share the document content (if using a shared doc platform with connectors enabled, provide the link)
3. Ask Reader Claude the generated questions
For each question, instruct Reader Claude to provide:
- The answer
- Whether anything was ambiguous or unclear
- What knowledge/context the doc assumes is already known
Check if Reader Claude gives correct answers or misinterprets anything.
### Step 3: Additional Checks
Also ask Reader Claude:
- "What in this doc might be ambiguous or unclear to readers?"
- "What knowledge or context does this doc assume readers already have?"
- "Are there any internal contradictions or inconsistencies?"
### Step 4: Iterate Based on Results
Ask what Reader Claude got wrong or struggled with. Indicate intention to fix those gaps.
Loop back to refinement for any problematic sections.
---
### Exit Condition (Both Approaches)
When Reader Claude consistently answers questions correctly and doesn't surface new gaps or ambiguities, the doc is ready.
## Final Review
When Reader Testing passes:
Announce the doc has passed Reader Claude testing. Before completion:
1. Recommend they do a final read-through themselves - they own this document and are responsible for its quality
2. Suggest double-checking any facts, links, or technical details
3. Ask them to verify it achieves the impact they wanted
Ask if they want one more review, or if the work is done.
**If user wants final review, provide it. Otherwise:**
Announce document completion. Provide a few final tips:
- Consider linking this conversation in an appendix so readers can see how the doc was developed
- Use appendices to provide depth without bloating the main doc
- Update the doc as feedback is received from real readers
## Tips for Effective Guidance
**Tone:**
- Be direct and procedural
- Explain rationale briefly when it affects user behavior
- Don't try to "sell" the approach - just execute it
**Handling Deviations:**
- If user wants to skip a stage: Ask if they want to skip this and write freeform
- If user seems frustrated: Acknowledge this is taking longer than expected. Suggest ways to move faster
- Always give user agency to adjust the process
**Context Management:**
- Throughout, if context is missing on something mentioned, proactively ask
- Don't let gaps accumulate - address them as they come up
**Artifact Management:**
- Use `create_file` for drafting full sections
- Use `str_replace` for all edits
- Provide artifact link after every change
- Never use artifacts for brainstorming lists - that's just conversation
**Quality over Speed:**
- Don't rush through stages
- Each iteration should make meaningful improvements
- The goal is a document that actually works for readers
@@ -0,0 +1,80 @@
---
name: 文档分析器
description: 深度分析文档结构和内容。当用户需要分析文档结构、提取关键信息、识别文档类型、进行内容质量评估、或理解文档组织方式时使用此技能。
---
# Document Analyzer
对知识库中的文档进行深度结构分析和内容理解。
## 核心能力
1. **结构分析**: 识别文档的章节层级、组织架构
2. **关键信息提取**: 提取核心论点、关键数据、重要结论
3. **文档类型识别**: 判断文档类型(报告、手册、论文、合同等)
4. **内容质量评估**: 评估文档的完整性、一致性、可读性
## 分析流程
### 1. 文档概览
首先获取文档的整体信息:
- 文档名称和类型
- 总页数/分块数
- 创建/更新时间
- 主要章节/标题
### 2. 结构分析
识别并描述:
- 标题层级结构
- 章节组织方式
- 逻辑流程(时间顺序、因果关系、并列结构)
### 3. 内容提取
重点关注:
- **核心主题**: 文档的中心议题
- **关键论点**: 主要观点和论述
- **支撑数据**: 重要的数据、统计、事实
- **结论建议**: 文档的结论或建议
### 4. 质量评估
评估维度:
- 完整性:是否涵盖必要内容
- 一致性:前后是否逻辑一致
- 清晰度:表达是否清晰易懂
## 输出格式
```markdown
## 文档分析报告
### 基本信息
- 文档名称:XXX
- 文档类型:XXX
- 结构层级:X级
### 文档结构
1. 第一章:XXX
1.1 ...
1.2 ...
2. 第二章:XXX
### 核心内容
- 主题:XXX
- 关键论点:
1. ...
2. ...
- 重要数据:XXX
### 分析结论
XXX
```
## 注意事项
- 保持客观中立,忠于原文
- 区分事实陈述和观点表达
- 标注信息来源位置
@@ -0,0 +1,370 @@
---
name: openmaic-classroom
description: 将 RAG 检索结果、文档块或知识图谱概念转换为 OpenMAIC 互动课程。当用户要求将知识库内容、检索到的文档片段、上传的文档、或知识图谱中的概念批量转换为教学课件/互动课堂时使用此技能。支持纯需求生成、基于 PDF 内容的课程生成、和基于概念图遍历的批量课堂生成。
---
# 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`、`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 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`、`content`
- `entities`(可选): 关联 entity 数组,每项包含 `slug`、`title`、`summary`、`link_type`
- `language`(可选): `zh-CN|en-US`,默认 `zh-CN`
- `depth`(可选): `beginner|intermediate|advanced`,默认 `intermediate`
- `audience`(可选): 目标受众描述,默认"相关领域的学习者"
**步骤 4:顺序调用 OpenMAIC API**
对每个 concept 的 requirement,**顺序**调用 OpenMAIC 生成 API(concurrency=1):
- 每个 concept → 一个 micro-classroom
- requirement 中标注 `micro-classroom`
- 不可并行,避免配额冲突
**步骤 5:生成 manifest(可恢复性)**
生成 manifest JSON,记录每个 concept 的生成状态:
```json
{
"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` 对象:
```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 <access-code>"
},
"body": {
"requirement": "..."
}
}
```
**本地模式**:
```json
{
"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 状态
@@ -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": "<ACCESS_CODE>" }
```
成功返回 `{ "success": true, "valid": true }` 并设置 `openmaic_access` cookie(7 天有效)。
## 标准响应格式
**成功**:
```json
{ "success": true, ...additionalFields }
```
**错误**:
```json
{ "success": false, "errorCode": "<Code>", "error": "<message>", "details?": "<detail>" }
```
---
## 核心端点
### 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=<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
```
请求体:
```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": []
}
```
@@ -0,0 +1,195 @@
# 需求构建指南
将 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` 自动生成。
**输入结构**:
```json
{
"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 配额。
@@ -0,0 +1,412 @@
#!/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()
@@ -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()