From 6e82493c20def129f06f26bde88ba228ec645b21 Mon Sep 17 00:00:00 2001 From: ymq Date: Sat, 22 Aug 2026 01:19:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=83=BD=E5=8A=9B=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=8C=89=20capability=20=E5=8A=A8=E6=80=81=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=EF=BC=88=E6=A0=B9=E6=B2=BB=E7=BA=B8=E9=9D=A2=E8=90=BD=E5=BA=93?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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)的问题 --- pipeline_service/agent_loop.py | 64 ++++++- pipeline_service/capability_tools.py | 247 +++++++++++++++++++++++++++ 2 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 pipeline_service/capability_tools.py diff --git a/pipeline_service/agent_loop.py b/pipeline_service/agent_loop.py index d9b1612..1e34f90 100644 --- a/pipeline_service/agent_loop.py +++ b/pipeline_service/agent_loop.py @@ -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}) diff --git a/pipeline_service/capability_tools.py b/pipeline_service/capability_tools.py new file mode 100644 index 0000000..028b3a8 --- /dev/null +++ b/pipeline_service/capability_tools.py @@ -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]}"