# GitHub Copilot SDK 自定义工具快速入门 ## 🎯 目标 在 OpenWebUI Pipe 中直接使用 GitHub Copilot SDK 的自定义工具功能,无需集成 OpenWebUI Function 系统。 --- ## 📖 基础概念 ### Copilot SDK Tool 的三要素 ```python from copilot.types import Tool, ToolInvocation, ToolResult # 1. Tool Definition(工具定义) tool = Tool( name="tool_name", # 工具名称 description="What it does", # 描述(给 AI 看的) parameters={...}, # JSON Schema 参数定义 handler=handler_function # 处理函数 ) # 2. Tool Handler(处理函数) async def handler_function(invocation: ToolInvocation) -> ToolResult: # invocation 包含: # - session_id: 会话 ID # - tool_call_id: 调用 ID # - tool_name: 工具名称 # - arguments: dict(实际参数) result = do_something(invocation["arguments"]) return ToolResult( textResultForLlm="结果文本", resultType="success", # 或 "failure" error=None, toolTelemetry={} ) # 3. Session Configuration(会话配置) session_config = SessionConfig( model="claude-sonnet-4.5", tools=[tool1, tool2, tool3], # ✅ 传入工具列表 streaming=True ) ``` --- ## 💻 完整实现示例 ### 示例 1:获取当前时间 ```python from datetime import datetime from copilot.types import Tool, ToolInvocation, ToolResult def create_time_tool(): """创建获取时间的工具""" async def get_time_handler(invocation: ToolInvocation) -> ToolResult: """工具处理函数""" try: # 获取参数 timezone = invocation["arguments"].get("timezone", "UTC") format_str = invocation["arguments"].get("format", "%Y-%m-%d %H:%M:%S") # 执行逻辑 current_time = datetime.now().strftime(format_str) result_text = f"Current time: {current_time}" # 返回结果 return ToolResult( textResultForLlm=result_text, resultType="success", error=None, toolTelemetry={"execution_time": "fast"} ) except Exception as e: return ToolResult( textResultForLlm=f"Error getting time: {str(e)}", resultType="failure", error=str(e), toolTelemetry={} ) # 创建工具定义 return Tool( name="get_current_time", description="Get the current date and time. Useful when user asks 'what time is it' or needs to know the current date.", parameters={ "type": "object", "properties": { "timezone": { "type": "string", "description": "Timezone name (e.g., 'UTC', 'Asia/Shanghai')", "default": "UTC" }, "format": { "type": "string", "description": "Time format string", "default": "%Y-%m-%d %H:%M:%S" } } }, handler=get_time_handler ) ``` ### 示例 2:数学计算器 ```python def create_calculator_tool(): """创建计算器工具""" async def calculate_handler(invocation: ToolInvocation) -> ToolResult: try: expression = invocation["arguments"].get("expression", "") # 安全检查 allowed_chars = set("0123456789+-*/()., ") if not all(c in allowed_chars for c in expression): raise ValueError("Expression contains invalid characters") # 计算(安全的 eval) result = eval(expression, {"__builtins__": {}}) return ToolResult( textResultForLlm=f"The result of {expression} is {result}", resultType="success", error=None, toolTelemetry={} ) except Exception as e: return ToolResult( textResultForLlm=f"Calculation error: {str(e)}", resultType="failure", error=str(e), toolTelemetry={} ) return Tool( name="calculate", description="Perform mathematical calculations. Supports basic arithmetic operations (+, -, *, /).", parameters={ "type": "object", "properties": { "expression": { "type": "string", "description": "Mathematical expression to evaluate (e.g., '2 + 2 * 3')" } }, "required": ["expression"] }, handler=calculate_handler ) ``` ### 示例 3:随机数生成器 ```python import random def create_random_number_tool(): """创建随机数生成工具""" async def random_handler(invocation: ToolInvocation) -> ToolResult: try: min_val = invocation["arguments"].get("min", 1) max_val = invocation["arguments"].get("max", 100) if min_val >= max_val: raise ValueError("min must be less than max") number = random.randint(min_val, max_val) return ToolResult( textResultForLlm=f"Generated random number: {number}", resultType="success", error=None, toolTelemetry={} ) except Exception as e: return ToolResult( textResultForLlm=f"Error: {str(e)}", resultType="failure", error=str(e), toolTelemetry={} ) return Tool( name="generate_random_number", description="Generate a random integer within a specified range.", parameters={ "type": "object", "properties": { "min": { "type": "integer", "description": "Minimum value (inclusive)", "default": 1 }, "max": { "type": "integer", "description": "Maximum value (inclusive)", "default": 100 } } }, handler=random_handler ) ``` --- ## 🔧 集成到 Pipe ### 完整的 Pipe 实现 ```python class Pipe: class Valves(BaseModel): # ... 现有 Valves ... ENABLE_TOOLS: bool = Field( default=False, description="Enable custom tools (time, calculator, random)" ) AVAILABLE_TOOLS: str = Field( default="all", description="Available tools: 'all' or comma-separated list (e.g., 'get_current_time,calculate')" ) def __init__(self): # ... 现有初始化 ... self._custom_tools = [] def _initialize_custom_tools(self): """初始化自定义工具""" if not self.valves.ENABLE_TOOLS: return [] # 定义所有可用工具 all_tools = { "get_current_time": create_time_tool(), "calculate": create_calculator_tool(), "generate_random_number": create_random_number_tool(), } # 根据配置过滤工具 if self.valves.AVAILABLE_TOOLS == "all": return list(all_tools.values()) # 只启用指定的工具 enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")] return [all_tools[name] for name in enabled if name in all_tools] async def pipe( self, body: dict, __metadata__: Optional[dict] = None, __event_emitter__=None, __event_call__=None, ) -> Union[str, AsyncGenerator]: # ... 现有代码 ... # ✅ 初始化工具 custom_tools = self._initialize_custom_tools() if custom_tools: await self._emit_debug_log( f"Enabled {len(custom_tools)} custom tools: {[t.name for t in custom_tools]}", __event_call__ ) # ✅ 创建会话配置(传入工具) from copilot.types import SessionConfig, InfiniteSessionConfig session_config = SessionConfig( session_id=chat_id if chat_id else None, model=real_model_id, streaming=body.get("stream", False), tools=custom_tools, # ✅✅✅ 关键:传入工具列表 infinite_sessions=infinite_session_config if self.valves.INFINITE_SESSION else None, ) session = await client.create_session(config=session_config) # ... 其余代码保持不变 ... ``` --- ## 📊 处理工具调用事件 ### 在 stream_response 中显示工具调用 ```python async def stream_response( self, client, session, send_payload, init_message: str = "", __event_call__=None ) -> AsyncGenerator: # ... 现有代码 ... def handler(event): event_type = str(getattr(event.type, "value", event.type)) # ✅ 工具调用开始 if "tool_invocation_started" in event_type or "tool_call_started" in event_type: tool_name = get_event_data(event, "tool_name", "") if tool_name: queue.put_nowait(f"\n\n🔧 **Calling tool**: `{tool_name}`\n") # ✅ 工具调用完成 elif "tool_invocation_completed" in event_type or "tool_call_completed" in event_type: tool_name = get_event_data(event, "tool_name", "") result = get_event_data(event, "result", "") if tool_name: queue.put_nowait(f"\n✅ **Tool `{tool_name}` completed**\n") # ✅ 工具调用失败 elif "tool_invocation_failed" in event_type or "tool_call_failed" in event_type: tool_name = get_event_data(event, "tool_name", "") error = get_event_data(event, "error", "") if tool_name: queue.put_nowait(f"\n❌ **Tool `{tool_name}` failed**: {error}\n") # ... 其他事件处理 ... # ... 其余代码 ... ``` --- ## 🧪 测试示例 ### 测试 1:询问时间 ``` User: "What time is it now?" Expected Flow: 1. Copilot 识别需要调用 get_current_time 工具 2. 调用工具(无参数或默认参数) 3. 工具返回: "Current time: 2026-01-26 15:30:00" 4. Copilot 回答: "The current time is 2026-01-26 15:30:00" Pipe Output: --- 🔧 **Calling tool**: `get_current_time` ✅ **Tool `get_current_time` completed** The current time is 2026-01-26 15:30:00 --- ``` ### 测试 2:数学计算 ``` User: "Calculate 123 * 456" Expected Flow: 1. Copilot 调用 calculate 工具 2. 参数: {"expression": "123 * 456"} 3. 工具返回: "The result of 123 * 456 is 56088" 4. Copilot 回答: "123 multiplied by 456 equals 56,088" Pipe Output: --- 🔧 **Calling tool**: `calculate` ✅ **Tool `calculate` completed** 123 multiplied by 456 equals 56,088 --- ``` ### 测试 3:生成随机数 ``` User: "Give me a random number between 1 and 10" Expected Flow: 1. Copilot 调用 generate_random_number 工具 2. 参数: {"min": 1, "max": 10} 3. 工具返回: "Generated random number: 7" 4. Copilot 回答: "I generated a random number for you: 7" ``` --- ## 🔍 调试技巧 ### 1. 记录所有工具事件 ```python def handler(event): event_type = str(getattr(event.type, "value", event.type)) # 记录所有包含 "tool" 的事件 if "tool" in event_type.lower(): event_data = {} if hasattr(event, "data"): try: event_data = { "type": event_type, "data": str(event.data)[:200] # 截断长数据 } except: pass self._emit_debug_log_sync( f"Tool Event: {json.dumps(event_data)}", __event_call__ ) ``` ### 2. 验证工具注册 ```python async def pipe(...): # ... custom_tools = self._initialize_custom_tools() # 调试:打印工具信息 if self.valves.DEBUG: tool_info = [ { "name": t.name, "description": t.description[:50], "has_handler": t.handler is not None } for t in custom_tools ] await self._emit_debug_log( f"Registered tools: {json.dumps(tool_info, indent=2)}", __event_call__ ) ``` ### 3. 测试工具处理函数 ```python # 单独测试工具 async def test_tool(): tool = create_time_tool() # 模拟调用 invocation = { "session_id": "test", "tool_call_id": "test_call", "tool_name": "get_current_time", "arguments": {"format": "%H:%M:%S"} } result = await tool.handler(invocation) print(f"Result: {result}") ``` --- ## ⚠️ 注意事项 ### 1. 工具描述的重要性 工具的 `description` 字段非常重要,它告诉 AI 何时应该使用这个工具: ```python # ❌ 差的描述 description="Get time" # ✅ 好的描述 description="Get the current date and time. Use this when the user asks 'what time is it', 'what's the date', or needs to know the current timestamp." ``` ### 2. 参数定义 使用标准的 JSON Schema 定义参数: ```python parameters={ "type": "object", "properties": { "param_name": { "type": "string", # string, integer, boolean, array, object "description": "Clear description", "enum": ["option1", "option2"], # 可选:枚举值 "default": "default_value" # 可选:默认值 } }, "required": ["param_name"] # 必需参数 } ``` ### 3. 错误处理 总是捕获异常并返回有意义的错误: ```python try: result = do_something() return ToolResult( textResultForLlm=f"Success: {result}", resultType="success", error=None, toolTelemetry={} ) except Exception as e: return ToolResult( textResultForLlm=f"Error occurred: {str(e)}", resultType="failure", error=str(e), # 用于调试 toolTelemetry={} ) ``` ### 4. 异步 vs 同步 工具处理函数可以是同步或异步: ```python # 同步工具 def sync_handler(invocation): result = calculate(invocation["arguments"]) return ToolResult(...) # 异步工具(推荐) async def async_handler(invocation): result = await fetch_data(invocation["arguments"]) return ToolResult(...) ``` --- ## 🚀 快速开始清单 - [ ] 1. 在 Valves 中添加 `ENABLE_TOOLS` 配置 - [ ] 2. 定义 2-3 个简单的工具函数 - [ ] 3. 实现 `_initialize_custom_tools()` 方法 - [ ] 4. 修改 `SessionConfig` 传入 `tools` 参数 - [ ] 5. 在 `stream_response` 中添加工具事件处理 - [ ] 6. 测试:询问时间、计算数学、生成随机数 - [ ] 7. 添加调试日志 - [ ] 8. 同步中文版本 --- ## 📚 完整的工具事件列表 根据 SDK 源码,可能的工具相关事件: - `tool_invocation_started` / `tool_call_started` - `tool_invocation_completed` / `tool_call_completed` - `tool_invocation_failed` / `tool_call_failed` - `tool_parameter_validation_failed` 实际事件名称可能因 SDK 版本而异,建议先记录所有事件类型: ```python def handler(event): print(f"Event type: {event.type}") ``` --- **快速实现入口:** 从示例 1(获取时间)开始,这是最简单的工具,可以快速验证整个流程! **作者:** Fu-Jie **日期:** 2026-01-26