Add label master skill

This commit is contained in:
wuyang6
2026-05-09 17:49:53 +08:00
parent 84b715a293
commit 010dc1a88d
166 changed files with 13099 additions and 3 deletions
+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""从 Markdown 标签知识库生成机器可读索引。
这个脚本只做确定性整理,不做语义判断:
- 人继续维护 knowledge/ 下的中文 Markdown。
- 脚本把标签、intent、function、object、边界文件整理成 JSON manifest。
- Agent 和校验脚本优先读取 manifest,避免每次全量扫描知识库。
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
SCRIPT_PATH = Path(__file__).resolve()
SKILL_ROOT = SCRIPT_PATH.parents[1]
KNOWLEDGE_ROOT = SKILL_ROOT / "knowledge"
DEFAULT_OUTPUT = KNOWLEDGE_ROOT / "索引" / "label_manifest.json"
def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def rel(path: Path) -> str:
return path.relative_to(SKILL_ROOT).as_posix()
def first_heading(text: str, fallback: str) -> str:
match = re.search(r"^#\s+(.+?)\s*$", text, re.MULTILINE)
return match.group(1).strip() if match else fallback
def extract_section(text: str, heading: str) -> str:
pattern = rf"^##\s+{re.escape(heading)}\s*$"
match = re.search(pattern, text, re.MULTILINE)
if not match:
return ""
start = match.end()
next_match = re.search(r"^##\s+", text[start:], re.MULTILINE)
end = start + next_match.start() if next_match else len(text)
return text[start:end].strip()
def extract_bullet_value(text: str, label: str) -> str:
match = re.search(rf"^-\s*{re.escape(label)}[:]\s*(.+?)\s*$", text, re.MULTILINE)
return match.group(1).strip() if match else ""
def parse_markdown_table(text: str) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
table_lines = [line.strip() for line in text.splitlines() if line.strip().startswith("|")]
if len(table_lines) < 2:
return rows
header = [cell.strip() for cell in table_lines[0].strip("|").split("|")]
for line in table_lines[2:]:
cells = [cell.strip() for cell in line.strip("|").split("|")]
if len(cells) != len(header):
continue
rows.append(dict(zip(header, cells)))
return rows
def parse_enum_values(description: str) -> list[str]:
if "枚举" not in description:
return []
desc = re.sub(r"[,。;;].*$", "", description.split("枚举", 1)[-1])
desc = desc.replace("", ":")
if ":" in desc:
desc = desc.split(":", 1)[1]
parts = re.split(r"[、,/| 或 和]+", desc)
values: list[str] = []
for part in parts:
cleaned = part.strip().strip("`").strip()
cleaned = re.sub(r"[(].*?[)]", "", cleaned).strip()
if cleaned and re.fullmatch(r"[A-Za-z0-9_]+", cleaned):
values.append(cleaned)
return values
def parse_label_cards() -> list[dict[str, Any]]:
labels_root = KNOWLEDGE_ROOT / "标签"
labels: list[dict[str, Any]] = []
if not labels_root.exists():
return labels
for path in sorted(labels_root.rglob("*.md")):
text = read_text(path)
name = first_heading(text, path.stem)
domain = path.parent.name
label = {
"name": name,
"domain": domain,
"path": rel(path),
"old_tag": extract_bullet_value(text, "旧 tag 标签"),
"agent_candidate": extract_bullet_value(text, "Agent 包装候选(不代表最终)"),
"function_candidate": extract_bullet_value(text, "Function 输出候选"),
"recommended_output_shape": extract_bullet_value(text, "推荐输出形态"),
"scope": extract_section(text, "适用范围"),
"examples": extract_section(text, "典型 Query"),
"confusing_labels": extract_section(text, "易混淆标签"),
"principles": extract_section(text, "划分原则"),
}
labels.append(label)
return labels
def parse_function_catalog() -> list[dict[str, Any]]:
path = KNOWLEDGE_ROOT / "输出能力" / "函数目录.md"
if not path.exists():
return []
text = read_text(path)
functions: list[dict[str, Any]] = []
sections = list(re.finditer(r"^##\s+(.+?)\s*$", text, re.MULTILINE))
for index, match in enumerate(sections):
name = match.group(1).strip()
start = match.end()
end = sections[index + 1].start() if index + 1 < len(sections) else len(text)
body = text[start:end].strip()
params: dict[str, dict[str, Any]] = {}
param_block = ""
if "参数:" in body:
param_block = body.split("参数:", 1)[1]
if "示例:" in param_block:
param_block = param_block.split("示例:", 1)[0]
for row in parse_markdown_table(param_block):
param_name = row.get("参数", "").strip()
description = row.get("说明", "").strip()
if not param_name:
continue
params[param_name] = {
"description": description,
"enum_values": parse_enum_values(description),
}
functions.append(
{
"name": name,
"path": rel(path),
"params": params,
"allow_unknown_params": False,
"summary": re.sub(r"\s+", " ", extract_summary(body)).strip(),
}
)
return functions
def parse_object_catalog() -> list[dict[str, Any]]:
path = KNOWLEDGE_ROOT / "输出能力" / "对象目录.md"
if not path.exists():
return []
text = read_text(path)
objects: list[dict[str, Any]] = []
sections = list(re.finditer(r"^##\s+(.+?)\s*$", text, re.MULTILINE))
for index, match in enumerate(sections):
name = match.group(1).strip()
start = match.end()
end = sections[index + 1].start() if index + 1 < len(sections) else len(text)
body = text[start:end].strip()
if name in {"VisionQA 辅助对象", "生成类辅助对象"}:
for row in parse_markdown_table(body):
object_name = row.get("对象", "").strip()
if object_name:
objects.append(
{
"name": object_name,
"path": rel(path),
"params": {},
"allow_unknown_params": True,
"summary": row.get("说明", "").strip(),
}
)
continue
params: dict[str, dict[str, Any]] = {}
param_block = body.split("参数:", 1)[1] if "参数:" in body else ""
if "示例:" in param_block:
param_block = param_block.split("示例:", 1)[0]
for row in parse_markdown_table(param_block):
param_name = row.get("参数", "").strip()
description = row.get("说明", "").strip()
if not param_name:
continue
params[param_name] = {
"description": description,
"enum_values": parse_enum_values(description),
}
objects.append(
{
"name": name,
"path": rel(path),
"params": params,
"allow_unknown_params": False,
"summary": re.sub(r"\s+", " ", extract_summary(body)).strip(),
}
)
return objects
def parse_intent_catalog() -> list[dict[str, Any]]:
path = KNOWLEDGE_ROOT / "输出能力" / "意图目录.md"
if not path.exists():
return []
text = read_text(path)
intents: list[dict[str, Any]] = []
for row in parse_markdown_table(text):
name = row.get("intent", "").replace("\\|", "|").strip()
if not name:
continue
intents.append(
{
"name": name,
"agent": row.get("Agent", "").strip(),
"instruction_type": row.get("指令类型", "").strip(),
"scope": row.get("简要范围", "").strip(),
"path": rel(path),
}
)
return intents
def extract_summary(body: str) -> str:
match = re.search(r"功能范围[:]\s*(.+?)(?:\n\n|$)", body, re.DOTALL)
return match.group(1).strip() if match else ""
def parse_boundary_files() -> list[dict[str, str]]:
boundary_root = KNOWLEDGE_ROOT / "边界"
if not boundary_root.exists():
return []
files: list[dict[str, str]] = []
for path in sorted(boundary_root.rglob("*.md")):
if path.name == "README.md":
continue
text = read_text(path)
parts = path.relative_to(boundary_root).parts
category = parts[0] if len(parts) > 1 else "root"
files.append({"name": first_heading(text, path.stem), "path": rel(path), "category": category})
return files
def parse_dimension_files() -> list[dict[str, str]]:
root = KNOWLEDGE_ROOT / "判断维度"
if not root.exists():
return []
files: list[dict[str, str]] = []
for path in sorted(root.glob("*.md")):
if path.name == "README.md":
continue
text = read_text(path)
files.append({"name": first_heading(text, path.stem), "path": rel(path)})
return files
def build_manifest() -> dict[str, Any]:
labels = parse_label_cards()
functions = parse_function_catalog()
objects = parse_object_catalog()
intents = parse_intent_catalog()
old_tags = sorted({item["old_tag"] for item in labels if item.get("old_tag") and item["old_tag"] != "待确认。"})
label_names = sorted({item["name"] for item in labels})
agent_tags = sorted(set(old_tags) | set(label_names))
return {
"schema_version": 1,
"skill": "label-master",
"display_name": "标签大师",
"description": "标签大师的中控标签知识库机器索引;由 scripts/build_label_manifest.py 从 Markdown 生成。",
"counts": {
"labels": len(labels),
"agent_tags": len(agent_tags),
"functions": len(functions),
"objects": len(objects),
"intents": len(intents),
},
"agent_tags": agent_tags,
"labels": labels,
"functions": functions,
"objects": objects,
"intents": intents,
"boundaries": parse_boundary_files(),
"dimensions": parse_dimension_files(),
}
def write_manifest(path: Path, manifest: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
text = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
path.write_text(text, encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="生成 label-master 标签大师机器索引")
parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="输出 JSON 路径")
parser.add_argument("--check", action="store_true", help="只检查输出文件是否为最新,不写入")
args = parser.parse_args(argv)
output = Path(args.output)
if not output.is_absolute():
output = SKILL_ROOT / output
manifest = build_manifest()
new_text = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
if args.check:
if not output.exists():
print(f"ERROR: manifest 不存在:{output}", file=sys.stderr)
return 1
old_text = output.read_text(encoding="utf-8")
if old_text != new_text:
print(f"ERROR: manifest 不是最新:{output}", file=sys.stderr)
return 1
print(f"OK: manifest 已是最新:{output}")
return 0
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(new_text, encoding="utf-8")
print(f"Wrote {output}")
print(json.dumps(manifest["counts"], ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""校验标签输出格式是否合法。
这个脚本只做“格式、存在性、引用关系”的确定性校验,不判断 query 语义。
适合在 Agent 给出 target 后、写入训练/评测数据前调用。
"""
from __future__ import annotations
import argparse
import ast
import json
import re
import sys
from pathlib import Path
from typing import Any
try:
from build_label_manifest import DEFAULT_OUTPUT, SKILL_ROOT, build_manifest
except ImportError: # pragma: no cover - 兼容从其他 cwd 直接执行
sys.path.insert(0, str(Path(__file__).resolve().parent))
from build_label_manifest import DEFAULT_OUTPUT, SKILL_ROOT, build_manifest
AGENT_RE = re.compile(r'^Agent\(\s*tag\s*=\s*["\'](.+?)["\']\s*\)$')
ASSIGNMENT_RE = re.compile(r"^(x\d+)\s*=\s*(.+)$")
def normalize_quotes(text: str) -> str:
return (
text.replace("", '"')
.replace("", '"')
.replace("", "'")
.replace("", "'")
.strip()
)
def load_manifest(path: Path) -> dict[str, Any]:
if path.exists():
return json.loads(path.read_text(encoding="utf-8"))
return build_manifest()
def names_by_key(items: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
return {item["name"]: item for item in items if item.get("name")}
class LabelOutputValidator:
def __init__(self, manifest: dict[str, Any]):
self.manifest = manifest
self.agent_tags = set(manifest.get("agent_tags", []))
self.functions = names_by_key(manifest.get("functions", []))
self.objects = names_by_key(manifest.get("objects", []))
self.intents = names_by_key(manifest.get("intents", []))
def validate(self, raw_target: str) -> dict[str, Any]:
target = normalize_quotes(raw_target)
result: dict[str, Any] = {
"valid": True,
"detected_type": "unknown",
"normalized_output": target,
"errors": [],
"warnings": [],
"references": [],
}
if not target:
self.error(result, "输出为空")
return result
dimensions, target_body = self.split_dimension_prefix(target)
if dimensions:
result["dimensions"] = dimensions
target_body = target_body.strip()
if target_body.startswith("["):
self.validate_group_json(target_body, result)
elif AGENT_RE.match(target_body):
self.validate_agent_target(target_body, result)
elif self.looks_like_call_program(target_body):
self.validate_call_program(target_body, result)
elif target_body in self.intents:
result["detected_type"] = "intent"
result["references"].append({"type": "intent", "name": target_body})
elif target_body in self.agent_tags:
result["detected_type"] = "bare_label"
result["warnings"].append("只输出了标签名;如果当前任务需要可消费 target,建议明确 intent、function 或 Agent 包装形式")
result["references"].append({"type": "label", "name": target_body})
else:
self.error(result, f"无法识别的输出形式:{target_body}")
if dimensions:
result["normalized_output"] = "\n".join([*dimensions, result["normalized_output"]])
result["valid"] = not result["errors"]
return result
def split_dimension_prefix(self, target: str) -> tuple[list[str], str]:
dimensions: list[str] = []
lines = [line.strip() for line in target.splitlines() if line.strip()]
while lines and re.fullmatch(r"(complex|multi_instruction|auto_task)\s*=\s*(true|false)", lines[0], re.I):
key, value = [part.strip().lower() for part in lines.pop(0).split("=", 1)]
dimensions.append(f"{key}={value}")
return dimensions, "\n".join(lines)
def validate_agent_target(self, target: str, result: dict[str, Any]) -> None:
result["detected_type"] = "agent"
match = AGENT_RE.match(target)
assert match is not None
tag = match.group(1)
result["normalized_output"] = f'Agent(tag="{tag}")'
if tag not in self.agent_tags:
self.error(result, f'Agent tag 不在标签知识库中:{tag}')
return
result["references"].append({"type": "agent_tag", "name": tag})
def looks_like_call_program(self, target: str) -> bool:
lines = [line.strip() for line in target.splitlines() if line.strip()]
return bool(lines) and all("(" in line and line.endswith(")") for line in lines)
def validate_call_program(self, target: str, result: dict[str, Any]) -> None:
result["detected_type"] = "function_program"
variables: dict[str, str] = {}
lines = [line.strip() for line in target.splitlines() if line.strip()]
for index, line in enumerate(lines):
assignment = ASSIGNMENT_RE.match(line)
if assignment:
var_name, expr = assignment.group(1), assignment.group(2).strip()
call_info = self.parse_call(expr, result, line)
if not call_info:
continue
object_name = call_info["name"]
if object_name not in self.objects:
self.error(result, f"变量 {var_name} 只能绑定 object,但 {object_name} 不在对象目录中")
continue
self.validate_call_kwargs(call_info, self.objects[object_name], result)
variables[var_name] = object_name
result["references"].append({"type": "object", "name": object_name, "variable": var_name})
continue
call_info = self.parse_call(line, result, line)
if not call_info:
continue
function_name = call_info["name"]
if function_name not in self.functions:
self.error(result, f"最终调用必须是已定义 function,但 {function_name} 不在函数目录中")
continue
self.validate_call_kwargs(call_info, self.functions[function_name], result)
self.validate_variable_refs(call_info, variables, result)
result["references"].append({"type": "function", "name": function_name})
if index != len(lines) - 1:
self.error(result, f"非最后一行不能直接调用 function:{line}")
def parse_call(self, expr: str, result: dict[str, Any], raw_line: str) -> dict[str, Any] | None:
try:
node = ast.parse(expr, mode="eval").body
except SyntaxError as exc:
self.error(result, f"调用语法无法解析:{raw_line} ({exc.msg})")
return None
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name):
self.error(result, f"不是合法的函数调用:{raw_line}")
return None
return {
"name": node.func.id,
"keywords": node.keywords,
"positional_count": len(node.args),
"raw": raw_line,
}
def validate_call_kwargs(self, call_info: dict[str, Any], spec: dict[str, Any], result: dict[str, Any]) -> None:
if call_info["positional_count"]:
self.error(result, f"暂不支持位置参数,请使用命名参数:{call_info['raw']}")
params = spec.get("params", {})
allow_unknown = bool(spec.get("allow_unknown_params"))
for keyword in call_info["keywords"]:
if keyword.arg is None:
self.error(result, f"暂不支持 **kwargs{call_info['raw']}")
continue
if not allow_unknown and keyword.arg not in params:
self.error(result, f"{call_info['name']} 未定义参数:{keyword.arg}")
continue
enum_values = params.get(keyword.arg, {}).get("enum_values", [])
if enum_values:
self.validate_enum(keyword, enum_values, call_info, result)
def validate_enum(self, keyword: ast.keyword, enum_values: list[str], call_info: dict[str, Any], result: dict[str, Any]) -> None:
values = self.literal_string_values(keyword.value)
for value in values:
if value not in enum_values:
self.error(result, f"{call_info['name']}.{keyword.arg} 枚举值不合法:{value},可选:{', '.join(enum_values)}")
def literal_string_values(self, node: ast.AST) -> list[str]:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return [node.value]
if isinstance(node, ast.List):
values: list[str] = []
for item in node.elts:
values.extend(self.literal_string_values(item))
return values
return []
def validate_variable_refs(self, call_info: dict[str, Any], variables: dict[str, str], result: dict[str, Any]) -> None:
referenced = sorted({node.id for keyword in call_info["keywords"] for node in ast.walk(keyword.value) if isinstance(node, ast.Name)})
for name in referenced:
if name not in variables:
self.error(result, f"引用了未定义 object 变量:{name}")
def validate_group_json(self, target: str, result: dict[str, Any]) -> None:
result["detected_type"] = "group_json"
try:
groups = json.loads(target)
except json.JSONDecodeError as exc:
self.error(result, f"多指令/自动任务 JSON 解析失败:{exc.msg}")
return
if not isinstance(groups, list):
self.error(result, "多指令/自动任务输出必须是 JSON list")
return
for group_index, group in enumerate(groups):
if not isinstance(group, dict):
self.error(result, f"{group_index} 个 group 不是 object")
continue
if "condition" not in group:
self.error(result, f"{group_index} 个 group 缺少 condition")
querys = group.get("querys")
if not isinstance(querys, list):
self.error(result, f"{group_index} 个 group 的 querys 必须是 list")
continue
for query_index, item in enumerate(querys):
self.validate_group_item(item, group_index, query_index, result)
def validate_group_item(self, item: Any, group_index: int, query_index: int, result: dict[str, Any]) -> None:
prefix = f"{group_index} 个 group 的第 {query_index} 条 query"
if not isinstance(item, dict):
self.error(result, f"{prefix} 不是 object")
return
if not item.get("subquery"):
self.error(result, f"{prefix} 缺少 subquery")
value_fields = [field for field in ("function", "intent", "target", "label") if item.get(field)]
if not value_fields:
self.error(result, f"{prefix} 缺少 function、intent、target 或 label")
return
if len(value_fields) > 1:
self.error(result, f"{prefix} 同时包含多个输出字段:{', '.join(value_fields)}")
return
field = value_fields[0]
value = str(item[field])
if field == "function":
nested = self.validate(value)
self.merge_nested(result, nested, prefix)
elif field == "intent":
if value not in self.intents and value not in self.agent_tags:
self.error(result, f"{prefix} 的 intent 不在知识库中:{value}")
else:
result["references"].append({"type": "intent", "name": value})
else:
nested = self.validate(value)
self.merge_nested(result, nested, prefix)
def merge_nested(self, result: dict[str, Any], nested: dict[str, Any], prefix: str) -> None:
for error in nested.get("errors", []):
self.error(result, f"{prefix}: {error}")
for warning in nested.get("warnings", []):
result["warnings"].append(f"{prefix}: {warning}")
result["references"].extend(nested.get("references", []))
def error(self, result: dict[str, Any], message: str) -> None:
result["errors"].append(message)
def load_targets_from_file(path: Path, field: str) -> list[str]:
text = path.read_text(encoding="utf-8").strip()
if not text:
return []
if path.suffix == ".jsonl":
targets: list[str] = []
for line_no, line in enumerate(text.splitlines(), start=1):
if not line.strip():
continue
item = json.loads(line)
if field not in item:
raise ValueError(f"{path}:{line_no} 缺少字段 {field}")
targets.append(str(item[field]))
return targets
data = json.loads(text)
if isinstance(data, dict):
if field in data:
return [str(data[field])]
if "records" in data and isinstance(data["records"], list):
return [str(item[field]) for item in data["records"] if isinstance(item, dict) and field in item]
if isinstance(data, list):
return [str(item[field]) for item in data if isinstance(item, dict) and field in item]
raise ValueError(f"无法从 {path} 读取字段 {field}")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="校验标签输出格式")
parser.add_argument("--target", help="直接校验一个 target 字符串")
parser.add_argument("--file", help="从 JSON/JSONL 文件中批量读取 target")
parser.add_argument("--field", default="target", help="批量文件中的字段名,默认 target")
parser.add_argument("--manifest", default=str(DEFAULT_OUTPUT), help="manifest JSON 路径")
args = parser.parse_args(argv)
if not args.target and not args.file:
parser.error("必须提供 --target 或 --file")
manifest_path = Path(args.manifest)
if not manifest_path.is_absolute():
manifest_path = SKILL_ROOT / manifest_path
validator = LabelOutputValidator(load_manifest(manifest_path))
targets = [args.target] if args.target else load_targets_from_file(Path(args.file), args.field)
results = [validator.validate(target or "") for target in targets]
payload: dict[str, Any]
if len(results) == 1:
payload = results[0]
else:
payload = {
"valid": all(item["valid"] for item in results),
"total": len(results),
"invalid": sum(1 for item in results if not item["valid"]),
"results": results,
}
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
return 0 if payload["valid"] else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""把旧 docx 标签表迁移为中文 Markdown 标签知识卡片。
这个脚本是一次性/阶段性迁移辅助工具,不是运行时分类器。
长期维护入口应该是 skills/label-master/knowledge 下的 Markdown 文件。
"""
from __future__ import annotations
import argparse
import re
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
from xml.etree import ElementTree as ET
NS = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
@dataclass
class LabelRow:
domain: str
name: str
raw_tag: str
agent_candidate: str
abstraction: str
scope: str
examples: str
semantic_points: str
function_def: str
boundary: str
confusing: str
principle: str
notes: str
def _cell_text(cell: ET.Element) -> str:
parts: list[str] = []
for text_node in cell.findall('.//w:t', NS):
if text_node.text:
parts.append(text_node.text)
return re.sub(r'\s+', ' ', ''.join(parts)).strip()
def _docx_tables(path: Path) -> list[list[list[str]]]:
with zipfile.ZipFile(path) as zf:
xml = zf.read('word/document.xml')
root = ET.fromstring(xml)
tables: list[list[list[str]]] = []
for table in root.findall('.//w:tbl', NS):
rows: list[list[str]] = []
for tr in table.findall('./w:tr', NS):
row = [_cell_text(tc) for tc in tr.findall('./w:tc', NS)]
if any(cell.strip() for cell in row):
rows.append(row)
if rows:
tables.append(rows)
return tables
def _norm_header(value: str) -> str:
return re.sub(r'[\s/()()]+', '', value)
def _find_index(headers: list[str], candidates: Iterable[str]) -> int | None:
normalized = [_norm_header(h) for h in headers]
for candidate in candidates:
needle = _norm_header(candidate)
for idx, header in enumerate(normalized):
if needle and needle in header:
return idx
return None
def _safe_filename(value: str) -> str:
value = re.sub(r'[a-f0-9]{24,}$', '', value, flags=re.IGNORECASE)
value = re.sub(r'[\\/:*?"<>|]+', '', value).strip()
value = re.sub(r'\s+', '', value)
return value[:48] or '未命名标签'
def _domain_name(path: Path) -> str:
stem = path.stem
return stem.removeprefix('Label定义-')
def _read_cell(row: list[str], idx: int | None) -> str:
if idx is None or idx >= len(row):
return ''
return row[idx].strip()
def _compact_label_name(tag: str, abstraction: str) -> str:
"""从旧表格的 tag 列中提取更适合人工维护的中文标签名。
部分旧表格把标签名和长描述写进了同一个单元格,例如
“闹钟闹钟/小憩定时闹钟的增删改查...”。这里尽量保留短标签,
不把整段说明变成文件名或 Agent(tag=...)。
"""
tag = re.sub(r'[a-f0-9]{24,}$', '', tag.strip(), flags=re.IGNORECASE)
abstraction = abstraction.strip()
if tag.startswith('应该属于'):
return abstraction or tag.removeprefix('应该属于').strip()
if not tag:
stop_match = re.search(r'(关于|回答|打开|查询|增|删|改|查|的|功能|垂域|/|、|,|。)', abstraction)
if stop_match and stop_match.start() >= 2:
return abstraction[:stop_match.start()]
return abstraction
if abstraction and tag.startswith(abstraction) and len(abstraction) <= 12:
return abstraction
repeated = re.match(r'^(.{2,8})\1', tag)
if repeated:
return repeated.group(1)
if len(tag) <= 24:
return tag
stop_match = re.search(r'(关于|回答|打开|查询|增|删|改|查|的|功能|垂域|/|、|,|。)', tag)
if stop_match and stop_match.start() >= 2:
return tag[:stop_match.start()]
return abstraction or tag[:12]
def _agent_candidate_for_label(name: str, raw_tag: str) -> str:
if not name:
return ''
if raw_tag.startswith('应该属于'):
target = raw_tag.removeprefix('应该属于').strip()
if target:
return f'Agent(tag="{target}")'
if raw_tag and len(raw_tag) <= 24:
return f'Agent(tag="{name}")'
if raw_tag.startswith(name):
return f'Agent(tag="{name}")'
return ''
def _function_candidate(value: str) -> str:
value = value.strip()
if not value:
return '待确认。'
# 保留旧表中的函数/参数定义原文,避免在迁移阶段误判最终输出格式。
return value
def _looks_like_header(row: list[str]) -> bool:
joined = ''.join(row)
return 'tag标签' in joined or ('功能抽象' in joined and '功能和范围定义' in joined)
def extract_label_rows(source_dir: Path) -> list[LabelRow]:
rows: list[LabelRow] = []
for docx_path in sorted(source_dir.glob('Label定义-*.docx')):
domain = _domain_name(docx_path)
for table in _docx_tables(docx_path):
if not table:
continue
header_pos = next((idx for idx, row in enumerate(table) if _looks_like_header(row)), None)
if header_pos is None:
continue
headers = table[header_pos]
idx_abstraction = _find_index(headers, ['功能抽象'])
idx_tag = _find_index(headers, ['tag标签'])
idx_scope = _find_index(headers, ['功能和范围定义'])
idx_examples = _find_index(headers, ['示例query'])
idx_semantic = _find_index(headers, ['三级语义功能点举例'])
idx_function = _find_index(headers, ['Function及参数定义', 'Function及参数定义飘黄部分未共识'])
idx_boundary = _find_index(headers, ['满足边界问题'])
idx_confusing = _find_index(headers, ['易混淆tagfunctionAgent', '易混淆tagfunction', '易混淆'])
idx_principle = _find_index(headers, ['划分原则'])
idx_notes = _find_index(headers, ['问题备注', '未解决'])
for row in table[header_pos + 1:]:
abstraction = _read_cell(row, idx_abstraction)
tag = _read_cell(row, idx_tag)
name = _compact_label_name(tag, abstraction)
if not name or name in {'/', '-', '待定'}:
continue
if len(name) > 80 and not tag:
continue
agent_candidate = _agent_candidate_for_label(name, tag)
rows.append(
LabelRow(
domain=domain,
name=name,
raw_tag=tag,
agent_candidate=agent_candidate,
abstraction=abstraction,
scope=_read_cell(row, idx_scope),
examples=_read_cell(row, idx_examples),
semantic_points=_read_cell(row, idx_semantic),
function_def=_read_cell(row, idx_function),
boundary=_read_cell(row, idx_boundary),
confusing=_read_cell(row, idx_confusing),
principle=_read_cell(row, idx_principle),
notes=_read_cell(row, idx_notes),
)
)
return rows
def _section(title: str, body: str) -> str:
body = body.strip()
if not body:
body = '待补充。'
return f'## {title}\n\n{body}\n'
def _table_cell(value: str, *, limit: int = 80) -> str:
value = re.sub(r'\s+', ' ', value.strip()).replace('|', '')
if not value:
return '待确认'
return value[:limit] + ('...' if len(value) > limit else '')
def _output_section(row: LabelRow) -> str:
raw_tag = row.raw_tag or '待确认。'
agent_candidate = row.agent_candidate or '待确认。'
function_candidate = _function_candidate(row.function_def)
return '\n'.join(
[
'## 标注输出',
'',
'> 注意:旧标签资料中同时存在 tag 标签和 Function 定义。本卡片不直接声明最终训练标签;生成数据前必须结合 `判断维度/标注输出形态.md` 确认当前任务使用 Agent 形式还是 Function 形式。',
'',
f'- 旧 tag 标签:{raw_tag}',
f'- Agent 包装候选(不代表最终):{agent_candidate}',
f'- Function 输出候选:{function_candidate}',
'- 推荐输出形态:待确认。',
'',
]
)
def _card_text(row: LabelRow) -> str:
title = row.name
return '\n'.join(
[
f'# {title}',
'',
_output_section(row),
_section('功能抽象', row.abstraction),
_section('适用范围', row.scope),
_section('典型 Query', row.examples),
_section('三级语义功能点', row.semantic_points),
_section('Function / Agent 说明', row.function_def),
_section('满足边界问题', row.boundary),
_section('易混淆标签', row.confusing),
_section('划分原则', row.principle),
_section('未解决问题', row.notes),
]
).rstrip() + '\n'
def write_knowledge(rows: list[LabelRow], output_dir: Path) -> None:
label_root = output_dir / '标签'
boundary_root = output_dir / '边界'
index_root = output_dir / '索引'
label_root.mkdir(parents=True, exist_ok=True)
boundary_root.mkdir(parents=True, exist_ok=True)
index_root.mkdir(parents=True, exist_ok=True)
domains: dict[str, list[LabelRow]] = {}
for row in rows:
domains.setdefault(row.domain, []).append(row)
domain_dir = label_root / _safe_filename(row.domain)
domain_dir.mkdir(parents=True, exist_ok=True)
path = domain_dir / f'{_safe_filename(row.name)}.md'
path.write_text(_card_text(row), encoding='utf-8')
for domain, domain_rows in sorted(domains.items()):
lines = [f'# {domain}边界', '']
for row in domain_rows:
if not row.confusing and not row.principle and not row.boundary:
continue
lines.append(f'## {row.name}')
if row.confusing:
lines.extend(['', f'- 易混淆:{row.confusing}'])
if row.principle:
lines.extend(['', f'- 划分原则:{row.principle}'])
if row.boundary:
lines.extend(['', f'- 满足边界问题:{row.boundary}'])
lines.append('')
if len(lines) > 2:
(boundary_root / f'{_safe_filename(domain)}边界.md').write_text('\n'.join(lines).rstrip() + '\n', encoding='utf-8')
overview_lines = ['# 标签迁移索引', '']
label_index_lines = [
'# 标签索引',
'',
'这个索引用于 Agent 第一阶段快速判断候选标签和输出形态。详细边界仍需读取具体标签卡片和边界文件。',
'',
'| 领域 | 标签 | 旧 tag 标签 | Agent 形式候选 | Function 形式候选 | 知识卡片 |',
'| --- | --- | --- | --- | --- | --- |',
]
for domain, domain_rows in sorted(domains.items()):
overview_lines.append(f'## {domain}')
overview_lines.append('')
seen_links: set[str] = set()
for row in sorted(domain_rows, key=lambda item: item.name):
rel = Path('标签') / _safe_filename(domain) / f'{_safe_filename(row.name)}.md'
rel_text = rel.as_posix()
if rel_text in seen_links:
continue
seen_links.add(rel_text)
overview_lines.append(f'- [{row.name}]({rel.as_posix()})')
rel_from_index = Path('..') / rel
function_hint = _table_cell(row.function_def, limit=60) if row.function_def else '待确认'
label_index_lines.append(
'| {domain} | {name} | {raw_tag} | {agent} | {function} | [{name}]({path}) |'.format(
domain=_table_cell(domain, limit=20),
name=_table_cell(row.name, limit=24),
raw_tag=_table_cell(row.raw_tag, limit=32),
agent=_table_cell(row.agent_candidate, limit=36),
function=function_hint,
path=rel_from_index.as_posix(),
)
)
overview_lines.append('')
(output_dir / '标签迁移索引.md').write_text('\n'.join(overview_lines).rstrip() + '\n', encoding='utf-8')
(index_root / '标签索引.md').write_text('\n'.join(label_index_lines).rstrip() + '\n', encoding='utf-8')
def main() -> int:
parser = argparse.ArgumentParser(description='迁移 docx 标签定义表为 Markdown 知识卡片')
parser.add_argument('--source-dir', default='标签定义', help='旧标签定义目录')
parser.add_argument('--output-dir', default='skills/label-master/knowledge', help='知识库输出目录')
args = parser.parse_args()
source_dir = Path(args.source_dir)
output_dir = Path(args.output_dir)
rows = extract_label_rows(source_dir)
write_knowledge(rows, output_dir)
print(f'已迁移 {len(rows)} 条标签知识到 {output_dir}')
return 0
if __name__ == '__main__':
raise SystemExit(main())