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)的问题
This commit is contained in:
parent
b31cb6c816
commit
6e82493c20
@ -904,7 +904,7 @@ def _agent_tools_to_openai_schema(agent_tools):
|
||||
k: {"type": "string", "description": v}
|
||||
for k, v in (t.get("params") or {}).items()
|
||||
},
|
||||
"required": list((t.get("params") or {}).keys()),
|
||||
"required": list((t.get("required")) or (t.get("params") or {}).keys()),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -912,7 +912,7 @@ def _agent_tools_to_openai_schema(agent_tools):
|
||||
]
|
||||
|
||||
|
||||
async def _exec_agent_tool(tool, params, workspace_dir):
|
||||
async def _exec_agent_tool(tool, params, workspace_dir, ctx=None):
|
||||
p = params or {}
|
||||
try:
|
||||
if tool == 'read_file':
|
||||
@ -968,6 +968,10 @@ async def _exec_agent_tool(tool, params, workspace_dir):
|
||||
target = _resolve_repo_target(workspace_dir, p.get('repo_dir', ''))
|
||||
r = await _git_commit_push(target, msg)
|
||||
return f"rc={r['rc']} {r['message']}"
|
||||
# 能力工具(propose_feature/create_case/report_bug 等,按角色 capability 注入)
|
||||
from .capability_tools import exec_capability_tool, TOOL_SCHEMAS
|
||||
if tool in TOOL_SCHEMAS:
|
||||
return await exec_capability_tool(tool, p, ctx or {})
|
||||
return f'未实现: {tool}'
|
||||
except Exception as e:
|
||||
return f'ERROR: {str(e)[:300]}'
|
||||
@ -1054,6 +1058,38 @@ async def _build_role_skills_block(sor, project_id, role, org_id=""):
|
||||
return ""
|
||||
|
||||
|
||||
async def _collect_capability_tools(sor, project_id, role, org_id=""):
|
||||
"""收集角色声明的 capability 对应的能力工具。
|
||||
|
||||
角色技能(scope=role)frontmatter 声明 `capability: feature_capability`(角色需要哪些能力),
|
||||
产线状态机规范(scope=pipeline)frontmatter 声明 `capability + tools`(能力含哪些工具),
|
||||
这里匹配两者,返回要注入的工具定义列表(name/description/params/required)。
|
||||
"""
|
||||
try:
|
||||
pid = await _resolve_pipeline_id(project_id)
|
||||
from pipeline_core.skill_loader import get_skill_loader
|
||||
from pipeline_core.skill_pack import get_skills_base
|
||||
from .capability_tools import resolve_capability_tools
|
||||
skills_dir = get_skills_base()
|
||||
loader = get_skill_loader(skills_dir)
|
||||
merged = loader.get_merged(pipeline_id=pid, role=role,
|
||||
project_id=project_id, org_id=org_id or '0')
|
||||
if not merged:
|
||||
return []
|
||||
role_caps = set()
|
||||
for skill in merged.values():
|
||||
if getattr(skill, 'scope', '') == 'role':
|
||||
cap = getattr(skill, 'capability', '') or ''
|
||||
for c in str(cap).split(','):
|
||||
c = c.strip()
|
||||
if c:
|
||||
role_caps.add(c)
|
||||
return resolve_capability_tools(role_caps, merged)
|
||||
except Exception as e:
|
||||
logger.warning(f"collect capability tools failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def _load_skill_by_name(sor, project_id, role, org_id, name, file_path=None):
|
||||
"""按需加载技能全文或子文件(分层导入第二层,对应 load_skill 工具)。
|
||||
|
||||
@ -1121,12 +1157,28 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
|
||||
role_specific = _role_specific
|
||||
qna_section = await _build_qna_section(sor, task_id, role, agent_id)
|
||||
tools_text = json.dumps(AGENT_TOOLS, ensure_ascii=False)
|
||||
# 能力工具:角色技能 capability 声明 → 产线状态机规范 tools 声明 → 注入对应工具
|
||||
capability_tools = await _collect_capability_tools(sor, project_id, role, org_id)
|
||||
all_tools = AGENT_TOOLS + capability_tools
|
||||
tools_text = json.dumps(all_tools, ensure_ascii=False)
|
||||
|
||||
# 注入角色技能(角色专属技能全量 + 其余 scope 目录层,优先级 角色>项目>产线>组织>通用)
|
||||
role_skills = await _build_role_skills_block(sor, project_id, role, org_id)
|
||||
# 项目名(用于 prompt 里 {项目名}_pc 占位符替换,角色产出路径以 project-directory-spec 为准)
|
||||
project_name = await _get_project_name(sor, project_id)
|
||||
# 能力工具上下文(project_id/iteration_id/who/agent_id 自动注入,LLM 不可见)
|
||||
_iter = None
|
||||
try:
|
||||
from .iteration_capability import get_current_iteration
|
||||
_iter = await get_current_iteration(sor, project_id)
|
||||
except Exception:
|
||||
_iter = None
|
||||
capability_ctx = {
|
||||
"project_id": project_id,
|
||||
"iteration_id": getattr(_iter, 'id', '') if _iter else '',
|
||||
"who": role,
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
system = (AGENT_SYSTEM_PROMPT
|
||||
.replace('__ROLE__', role)
|
||||
@ -1142,7 +1194,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
msgs.append({"role": "user", "content": f"执行任务:{title}\n参数:{params_str}"})
|
||||
|
||||
from .llm_bridge import llm_call_msgs, llm_call_msgs_native
|
||||
tools_schema = _agent_tools_to_openai_schema(AGENT_TOOLS)
|
||||
tools_schema = _agent_tools_to_openai_schema(all_tools)
|
||||
|
||||
deliverable = None
|
||||
ask_question = None
|
||||
@ -1199,7 +1251,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
if tool == "load_skill":
|
||||
result = await _load_skill_by_name(sor, project_id, role, org_id, params.get("name", ""), params.get("file_path") or None)
|
||||
else:
|
||||
result = await _exec_agent_tool(tool, params, workspace_dir)
|
||||
result = await _exec_agent_tool(tool, params, workspace_dir, capability_ctx)
|
||||
if tool == "write_file" and params.get("path"):
|
||||
written_files.append(os.path.join(workspace_dir, params["path"]))
|
||||
msgs.append({"role": "tool", "tool_call_id": tc.get("id", ""), "content": str(result)})
|
||||
@ -1223,7 +1275,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
||||
if tool == 'load_skill':
|
||||
result = await _load_skill_by_name(sor, project_id, role, org_id, params.get('name', ''), params.get('file_path') or None)
|
||||
else:
|
||||
result = await _exec_agent_tool(tool, params, workspace_dir)
|
||||
result = await _exec_agent_tool(tool, params, workspace_dir, capability_ctx)
|
||||
if tool == 'write_file' and params.get('path'):
|
||||
written_files.append(os.path.join(workspace_dir, params['path']))
|
||||
msgs.append({"role": "assistant", "content": raw})
|
||||
|
||||
247
pipeline_service/capability_tools.py
Normal file
247
pipeline_service/capability_tools.py
Normal file
@ -0,0 +1,247 @@
|
||||
# -*- 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]}"
|
||||
Loading…
x
Reference in New Issue
Block a user