pipeline-service/pipeline_service/capability_tools.py
ymq 6e82493c20 feat: 能力工具按 capability 动态注入(根治纸面落库)
- 新增 capability_tools.py:TOOL_SCHEMAS 工具 schema + 从产线状态机规范 frontmatter
  读取 capability→tools 映射(可配置)+ exec_capability_tool 分发
- agent_loop 加 _collect_capability_tools:角色技能(scope=role)声明 capability,
  匹配产线状态机规范(scope=pipeline)的 tools 声明,注入对应能力工具
- role_agent_run 注入能力工具(AGENT_TOOLS+capability_tools) + capability_ctx
  (project_id/iteration_id/who/agent_id 自动注入,LLM 不可见)
- _exec_agent_tool 分发能力工具到对应 capability 函数
- 根治:requirement 无法 propose_feature、test 无法 create_case/report_bug
  只能写文档假装落库(sd_features 表=0)的问题
2026-08-22 01:19:27 +08:00

248 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""能力工具注入:capability → 工具列表 从产线状态机规范 frontmatter 读取(可配置)。
三层:
1. 产线状态机规范(common/feature 等)frontmatter 声明 `capability: feature_capability` + `tools: [propose_feature, ...]`
—— 这是「配置」:哪个 capability 含哪些工具,改 frontmatter 即可增删,不用改代码。
2. 角色技能(roles/{role}/role)frontmatter 声明 `capability: feature_capability`
—— 角色声明「我需要这个能力」,系统据此注入该能力下的全部工具。
3. 本模块:工具 schema(分发模块/描述/参数)硬编码在此(代码不可避免),
但 capability→工具列表 由第 1 层 frontmatter 决定(可配置)。
系统上下文(project_id/iteration_id/who/agent_id)由分发函数自动注入,不暴露给 LLM。
"""
import inspect
import importlib
import logging
logger = logging.getLogger("pipeline")
# 工具 schema(分发模块 + 描述 + 参数)——代码层,不可避免。
# 每个工具名对应某个 capability 模块里的同名函数。参数只暴露 LLM 可见的业务参数,
# project_id/iteration_id/who/agent_id 由 exec 自动注入。
TOOL_SCHEMAS = {
# ── feature_capability ──
"propose_feature": {
"module": "feature_capability",
"description": "提出功能点(拆解需求提功能),落库 sd_features,status=proposed。每个业务域的功能都要提",
"params": {"feature_name": "功能名称", "description": "功能描述(输入/处理/输出/验收标准)",
"priority": "优先级 P0/P1/P2(默认 P2)", "feature_type": "功能类型(默认 new_feature)"},
"required": ["feature_name"],
},
"list_features": {
"module": "feature_capability",
"description": "列出功能清单(sd_features),可按状态/迭代过滤",
"params": {"status": "状态(可选 proposed/approved/...)", "iteration_id": "迭代ID(可选)"},
"required": [],
},
"approve_feature": {
"module": "feature_capability",
"description": "审批功能:proposed → approved",
"params": {"feature_id": "功能ID", "comment": "审批意见(可选)"},
"required": ["feature_id"],
},
"reject_feature": {
"module": "feature_capability",
"description": "驳回功能:proposed/approved → rejected",
"params": {"feature_id": "功能ID", "comment": "驳回原因"},
"required": ["feature_id"],
},
"start_feature": {
"module": "feature_capability",
"description": "开始开发功能:approved → in_progress",
"params": {"feature_id": "功能ID"},
"required": ["feature_id"],
},
"deliver_feature": {
"module": "feature_capability",
"description": "交付功能:in_progress → delivered",
"params": {"feature_id": "功能ID"},
"required": ["feature_id"],
},
"verify_feature": {
"module": "feature_capability",
"description": "验收功能:delivered → verified",
"params": {"feature_id": "功能ID"},
"required": ["feature_id"],
},
# ── test_plan_capability ──
"create_test_plan": {
"module": "test_plan_capability",
"description": "创建测试计划,落库 sd_test_plans,status=draft",
"params": {"plan_name": "计划名称", "plan_type": "类型(默认 functional)", "scope": "测试范围",
"environment": "测试环境", "entry_criteria": "准入标准", "exit_criteria": "准出标准"},
"required": ["plan_name"],
},
"list_plans": {
"module": "test_plan_capability",
"description": "列出测试计划(sd_test_plans)",
"params": {"status": "状态(可选)"},
"required": [],
},
# ── test_case_capability ──
"create_case": {
"module": "test_case_capability",
"description": "创建测试用例,落库 sd_test_cases,status=pending",
"params": {"plan_id": "测试计划ID", "case_name": "用例名称", "case_type": "类型(默认 functional)",
"priority": "优先级(默认 P2)", "precondition": "前置条件", "steps": "执行步骤",
"expected_result": "预期结果"},
"required": ["plan_id", "case_name"],
},
"pass_case": {
"module": "test_case_capability",
"description": "用例通过:pending/fail → pass",
"params": {"case_id": "用例ID", "plan_id": "计划ID", "actual_result": "实际结果"},
"required": ["case_id", "plan_id"],
},
"fail_case": {
"module": "test_case_capability",
"description": "用例失败:pending/pass → fail(失败后应 report_bug 建 Bug)",
"params": {"case_id": "用例ID", "plan_id": "计划ID", "actual_result": "实际结果(与预期不符处)"},
"required": ["case_id", "plan_id"],
},
"skip_case": {
"module": "test_case_capability",
"description": "用例跳过:pending → skipped(注明原因)",
"params": {"case_id": "用例ID", "plan_id": "计划ID", "actual_result": "跳过原因"},
"required": ["case_id", "plan_id"],
},
"block_case": {
"module": "test_case_capability",
"description": "用例阻塞:pending → blocked(环境/依赖无法执行)",
"params": {"case_id": "用例ID", "plan_id": "计划ID", "actual_result": "阻塞原因"},
"required": ["case_id", "plan_id"],
},
"list_cases": {
"module": "test_case_capability",
"description": "列出测试用例(sd_test_cases)",
"params": {"plan_id": "计划ID", "status": "状态(可选)"},
"required": ["plan_id"],
},
# ── bug_capability ──
"report_bug": {
"module": "bug_capability",
"description": "上报 Bug,落库 sd_bugs,status=open(用例 fail 后应调用)",
"params": {"title": "Bug 标题", "description": "Bug 描述", "severity": "严重度 critical/major/minor(默认 major)",
"priority": "优先级(默认 P1)", "case_id": "关联用例ID(可选)", "step_name": "发现步骤(可选)"},
"required": ["title"],
},
"list_bugs": {
"module": "bug_capability",
"description": "列出 Bug(sd_bugs)",
"params": {"status": "状态(可选)", "severity": "严重度(可选)"},
"required": [],
},
# ── deploy_capability ──
"configure_env": {
"module": "deploy_capability",
"description": "配置部署环境,落库 sd_deploy_envs,status=configured",
"params": {"env_type": "环境类型 test/prod", "host": "目标机主机", "user": "SSH 用户",
"deploy_path": "部署路径", "port": "SSH 端口(默认 22)", "ssh_key_path": "SSH 密钥路径(可选)",
"sudo_enabled": "是否 sudo(可选)", "python_path": "Python 路径(可选)", "db_host": "DB 主机(可选)",
"db_port": "DB 端口(可选)", "db_name": "DB 名(可选)", "db_user": "DB 用户(可选)",
"db_password": "DB 密码(可选)"},
"required": ["env_type", "host", "user", "deploy_path"],
},
"verify_env": {
"module": "deploy_capability",
"description": "验证部署环境可用:configured → verified",
"params": {"env_id": "环境ID"},
"required": ["env_id"],
},
"fail_env": {
"module": "deploy_capability",
"description": "标记部署环境不可用(部署前检查失败时调用)",
"params": {"env_id": "环境ID", "reason": "不可用原因"},
"required": ["env_id"],
},
"list_envs": {
"module": "deploy_capability",
"description": "列出部署环境(sd_deploy_envs)",
"params": {"env_type": "类型(可选 test/prod)", "status": "状态(可选)"},
"required": [],
},
}
def _capability_to_tools_map(merged_skills):
"""从技能 frontmatter 的 capability + tools 声明,建立 capability → 工具名列表。
「可配置」的核心:产线状态机规范(common/feature 等)frontmatter 写
`capability: feature_capability` + `tools: [propose_feature, ...]`,
这里读取该声明。改 frontmatter 即可增删 capability 下的工具,不用改代码。
"""
m = {}
for skill in merged_skills.values():
cap = getattr(skill, "capability", "") or ""
tools = getattr(skill, "tools", []) or []
if not cap:
continue
m.setdefault(cap, [])
for t in tools:
if t not in m[cap]:
m[cap].append(t)
return m
def resolve_capability_tools(capabilities, merged_skills):
"""根据角色声明的 capability 集合 + 技能 frontmatter 映射,返回工具定义列表。
capabilities: 角色技能 frontmatter 声明的 capability 集合
merged_skills: 角色可见技能(get_merged 结果)
返回 [{name, description, params, required}, ...](去重)
"""
cap_map = _capability_to_tools_map(merged_skills)
tools = []
seen = set()
for cap in (capabilities or []):
for tool_name in cap_map.get(cap, []):
schema = TOOL_SCHEMAS.get(tool_name)
if not schema or tool_name in seen:
continue
seen.add(tool_name)
tools.append({
"name": tool_name,
"description": schema["description"],
"params": schema["params"],
"required": schema["required"],
})
return tools
async def exec_capability_tool(tool_name, params, ctx):
"""分发能力工具。ctx: {project_id, iteration_id, who, agent_id}。"""
schema = TOOL_SCHEMAS.get(tool_name)
if not schema:
return f"FAIL: 未注册的能力工具 {tool_name}"
p = dict(params or {})
p.setdefault("project_id", ctx.get("project_id", ""))
p.setdefault("iteration_id", ctx.get("iteration_id", ""))
p.setdefault("who", ctx.get("who", ""))
p.setdefault("agent_id", ctx.get("agent_id", ""))
try:
mod = importlib.import_module(f"pipeline_service.{schema['module']}")
fn = getattr(mod, tool_name)
sig = inspect.signature(fn)
kwargs = {k: v for k, v in p.items() if k in sig.parameters}
result = await fn(**kwargs)
if isinstance(result, tuple) and len(result) == 2:
ok, msg = result
return f"{'OK' if ok else 'FAIL'}: {msg}"
if isinstance(result, list):
if not result:
return "(空)"
lines = []
for item in result[:50]:
if hasattr(item, "keys"):
lines.append(str({k: getattr(item, k, "") for k in item.keys()}))
else:
lines.append(str(item))
return "\n".join(lines)
return str(result)
except Exception as e:
logger.exception(f"exec_capability_tool {tool_name} failed")
return f"ERROR: {str(e)[:300]}"