pipeline-service/pipeline_service/capability_tools.py

278 lines
12 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"],
},
"reopen_feature": {
"module": "feature_capability",
"description": "重新打开功能:rejected/verified → proposed(回炉)",
"params": {"feature_id": "功能ID", "comment": "重开原因(可选)"},
"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": [],
},
"start_fix": {
"module": "bug_capability",
"description": "开始修复 Bug:confirmed → fixing(记录处理人)",
"params": {"bug_id": "Bug ID", "assignee_id": "处理人(可选)"},
"required": ["bug_id"],
},
"fix_bug": {
"module": "bug_capability",
"description": "修复完成 Bug:fixing → fixed(附修复说明+提交)",
"params": {"bug_id": "Bug ID", "fix_description": "修复说明", "fix_commit": "修复提交(可选)"},
"required": ["bug_id"],
},
"verify_bug": {
"module": "bug_capability",
"description": "验证 Bug 修复:fixed → verified(记录验证人)",
"params": {"bug_id": "Bug ID", "verified_by": "验证人(可选)"},
"required": ["bug_id"],
},
"close_bug": {
"module": "bug_capability",
"description": "关闭 Bug:verified → closed",
"params": {"bug_id": "Bug ID"},
"required": ["bug_id"],
},
# ── 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]}"