task.py
1 # Copyright (c) 2024-2026 Tencent Zhuque Lab. All rights reserved. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # 15 # Requirement: Any integration or derivative work must explicitly attribute 16 # Tencent Zhuque Lab (https://github.com/Tencent/AI-Infra-Guard) in its 17 # documentation or user interface, as detailed in the NOTICE file. 18 19 """ 20 Task 工具 - 子 Agent 任务执行工具 21 遵循与 Skill 相同的标准: 22 - Agent 模板存储在 prompt/agents/ 下 23 - 支持 YAML Frontmatter 元数据 24 """ 25 import os 26 import re 27 import uuid 28 import yaml 29 from typing import Any, Optional, Dict, List 30 from tools.registry import register_tool 31 from utils.logging import logger 32 from utils.tool_context import ToolContext 33 from utils.config import base_dir 34 35 AGENTS_DIR = os.path.join(base_dir, "prompt", "agents") 36 37 38 def parse_agent_file(file_path: str) -> Dict[str, Any]: 39 """解析带有 YAML Frontmatter 的 Agent 文件""" 40 if not os.path.exists(file_path): 41 return {} 42 43 with open(file_path, 'r', encoding='utf-8') as f: 44 content = f.read() 45 46 meta = {} 47 body = content 48 49 # 使用正则匹配 YAML Frontmatter 50 frontmatter_pattern = re.compile(r'^---\s*\n(.*?)\n---\s*\n', re.DOTALL) 51 match = frontmatter_pattern.match(content) 52 53 if match: 54 yaml_content = match.group(1) 55 meta = yaml.safe_load(yaml_content) or {} 56 body = content[match.end():].strip() 57 58 # 确保 meta 中有基本信息 59 if 'description' not in meta: 60 lines = body.split('\n') 61 desc_lines = [] 62 for line in lines[:5]: 63 line = line.strip() 64 if line and not line.startswith('#'): 65 desc_lines.append(line) 66 meta['description'] = ' '.join(desc_lines)[:200] 67 68 return { 69 "meta": meta, 70 "content": body, 71 "raw": content 72 } 73 74 75 def get_all_agents() -> List[Dict[str, Any]]: 76 """扫描目录获取所有 Agent""" 77 agents = [] 78 79 if not os.path.exists(AGENTS_DIR): 80 return agents 81 82 for name in os.listdir(AGENTS_DIR): 83 if name.startswith('.'): 84 continue 85 86 item_path = os.path.join(AGENTS_DIR, name) 87 88 # 情况1: 直接是 .md 文件 89 if os.path.isfile(item_path) and name.endswith('.md'): 90 agent_name = name[:-3] # 移除 .md 91 data = parse_agent_file(item_path) 92 meta = data.get('meta', {}) 93 94 agents.append({ 95 "name": agent_name, 96 "title": meta.get('name', agent_name), 97 "description": meta.get('description', ''), 98 "meta": meta, 99 "path": item_path 100 }) 101 102 # 情况2: 是目录,查找 index.md 或同名 .md 103 elif os.path.isdir(item_path): 104 agent_file = None 105 # 优先查找 index.md 106 index_path = os.path.join(item_path, 'index.md') 107 main_path = os.path.join(item_path, f'{name}.md') 108 109 if os.path.isfile(index_path): 110 agent_file = index_path 111 elif os.path.isfile(main_path): 112 agent_file = main_path 113 114 if agent_file: 115 data = parse_agent_file(agent_file) 116 meta = data.get('meta', {}) 117 118 agents.append({ 119 "name": name, 120 "title": meta.get('name', name), 121 "description": meta.get('description', ''), 122 "path": agent_file 123 }) 124 125 return sorted(agents, key=lambda x: x['name']) 126 127 128 def load_agent_prompt(agent_name: str) -> Optional[Dict[str, Any]]: 129 """ 130 加载 Agent 提示词 131 132 Args: 133 agent_name: Agent 名称 134 135 Returns: 136 包含 meta 和 content 的字典,如果不存在返回 None 137 """ 138 # 直接检查 .md 文件 139 direct_path = os.path.join(AGENTS_DIR, f"{agent_name}.md") 140 if os.path.isfile(direct_path): 141 return parse_agent_file(direct_path) 142 143 # 检查目录 144 agent_dir = os.path.join(AGENTS_DIR, agent_name) 145 if os.path.isdir(agent_dir): 146 index_path = os.path.join(agent_dir, 'index.md') 147 main_path = os.path.join(agent_dir, f'{agent_name}.md') 148 149 if os.path.isfile(index_path): 150 return parse_agent_file(index_path) 151 elif os.path.isfile(main_path): 152 return parse_agent_file(main_path) 153 154 # 尝试模糊匹配 155 all_agents = get_all_agents() 156 for agent in all_agents: 157 if agent['name'] == agent_name: 158 return parse_agent_file(agent['path']) 159 160 return None 161 162 163 @register_tool 164 async def task( 165 prompt: str, 166 subagent_type: str, 167 description: str = "", 168 context: ToolContext = None 169 ) -> dict[str, Any]: 170 """ 171 执行子 Agent 任务 172 173 Args: 174 prompt: 任务提示词 175 subagent_type: 子 Agent 类型 176 description: 任务描述(3-5 个词) 177 context: 工具上下文 178 179 Returns: 180 包含执行结果的字典 181 """ 182 # 加载 Agent 数据 183 agent_instruction = load_agent_prompt(subagent_type) 184 185 if agent_instruction is None: 186 available = get_all_agents() 187 available_names = [a['name'] for a in available] 188 189 return { 190 "success": False, 191 "error": f"Unknown agent type: {subagent_type}. Available agents: {', '.join(available_names) if available_names else 'none'}" 192 } 193 194 # 构建任务提示词 195 task_prompt = f""" 196 Task: {description or 'Execute the following'} 197 198 {prompt} 199 200 Please complete this task and provide a summary of your actions and results. 201 """ 202 203 logger.info(f"Executing task with agent '{subagent_type}': {description or prompt[:50]}") 204 205 result = await context.call_subagent( 206 description, subagent_type, task_prompt, uuid.uuid4().__str__(), context.language, "", {} 207 ) 208 209 return { 210 "success": True, 211 "title": description or f"Task: {subagent_type}", 212 "output": result, 213 "agent": subagent_type, 214 } 215 216 217 @register_tool 218 def list_agents(context: ToolContext = None) -> dict[str, Any]: 219 """ 220 列出可用的子 Agent 221 222 Returns: 223 包含 Agent 列表的字典 224 """ 225 agents = get_all_agents() 226 227 if not agents: 228 return { 229 "success": True, 230 "output": "No agents available.", 231 "agents": [] 232 } 233 234 output_lines = ["Available agents:"] 235 for agent in agents: 236 output_lines.append(f"- {agent['name']}: {agent['description']}") 237 238 return { 239 "success": True, 240 "output": '\n'.join(output_lines), 241 "agents": agents 242 }