Files
Fu-Jie_openwebui-extensions/plugins/debug/legacy_docs/guides/TOOL_IMPLEMENTATION_GUIDE.md

11 KiB
Raw Blame History

GitHub Copilot SDK - Tool 功能实现指南

📋 概述

本指南介绍如何在 GitHub Copilot SDK Pipe 中实现 Function/Tool Calling 功能。


🏗️ 架构设计

工作流程

OpenWebUI Tools/Functions
    ↓ (转换)
Copilot SDK Tool Definition
    ↓ (注册)
Session Tool Handlers
    ↓ (调用)
Tool Execution → Result
    ↓ (返回)
Continue Conversation

核心接口

1. Tool Definition工具定义

from copilot.types import Tool

tool = Tool(
    name="get_weather",
    description="Get current weather for a location",
    parameters={
        "type": "object",
        "properties": {
            "location": {
                "type": "string",
                "description": "City name, e.g., 'San Francisco'"
            },
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }
        },
        "required": ["location"]
    },
    handler=weather_handler  # 处理函数
)

2. Tool Handler处理函数

from copilot.types import ToolInvocation, ToolResult

async def weather_handler(invocation: ToolInvocation) -> ToolResult:
    """
    invocation 包含:
    - session_id: str
    - tool_call_id: str
    - tool_name: str
    - arguments: dict  # {"location": "San Francisco", "unit": "celsius"}
    """
    location = invocation["arguments"]["location"]
    
    # 执行实际逻辑
    weather_data = await fetch_weather(location)
    
    # 返回结果
    return ToolResult(
        textResultForLlm=f"Weather in {location}: {weather_data['temp']}°C, {weather_data['condition']}",
        resultType="success",  # or "failure"
        error=None,
        toolTelemetry={"execution_time_ms": 150}
    )

3. Session Configuration会话配置

from copilot.types import SessionConfig

session_config = SessionConfig(
    model="claude-sonnet-4.5",
    tools=[tool1, tool2, tool3],  # ✅ 传递工具列表
    available_tools=["get_weather", "search_web"],  # 可选:过滤可用工具
    excluded_tools=["dangerous_tool"],  # 可选:排除工具
)

session = await client.create_session(config=session_config)

💻 实现方案

方案 A桥接 OpenWebUI Tools推荐

1. 添加 Valves 配置

class Valves(BaseModel):
    ENABLE_TOOLS: bool = Field(
        default=True,
        description="Enable OpenWebUI tool integration"
    )
    TOOL_TIMEOUT: int = Field(
        default=30,
        description="Tool execution timeout (seconds)"
    )
    AVAILABLE_TOOLS: str = Field(
        default="",
        description="Filter specific tools (comma separated, empty = all)"
    )

2. 实现 Tool 转换器

def _convert_openwebui_tools_to_copilot(
    self, 
    owui_tools: List[dict],
    __event_call__=None
) -> List[dict]:
    """
    将 OpenWebUI tools 转换为 Copilot SDK 格式
    
    OpenWebUI Tool 格式:
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather info",
            "parameters": {...}  # JSON Schema
        }
    }
    """
    copilot_tools = []
    
    for tool in owui_tools:
        if tool.get("type") != "function":
            continue
        
        func = tool.get("function", {})
        tool_name = func.get("name")
        
        if not tool_name:
            continue
        
        # 应用过滤器
        if self.valves.AVAILABLE_TOOLS:
            allowed = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
            if tool_name not in allowed:
                continue
        
        copilot_tools.append({
            "name": tool_name,
            "description": func.get("description", ""),
            "parameters": func.get("parameters", {}),
            "handler": self._create_tool_handler(tool_name, __event_call__)
        })
        
        self._emit_debug_log_sync(
            f"Registered tool: {tool_name}",
            __event_call__
        )
    
    return copilot_tools

3. 实现动态 Tool Handler

def _create_tool_handler(self, tool_name: str, __event_call__=None):
    """为每个 tool 创建 handler 函数"""
    
    async def handler(invocation: dict) -> dict:
        """
        Tool handler 实现
        
        invocation 结构:
        {
            "session_id": "...",
            "tool_call_id": "...",
            "tool_name": "get_weather",
            "arguments": {"location": "Beijing"}
        }
        """
        try:
            self._emit_debug_log_sync(
                f"Tool called: {invocation['tool_name']} with {invocation['arguments']}",
                __event_call__
            )
            
            # 方法 1: 调用 OpenWebUI 内部 Function API
            result = await self._execute_openwebui_function(
                function_name=invocation["tool_name"],
                arguments=invocation["arguments"]
            )
            
            # 方法 2: 通过 __event_emitter__ 触发(需要测试)
            # 方法 3: 直接实现工具逻辑
            
            return {
                "textResultForLlm": str(result),
                "resultType": "success",
                "error": None,
                "toolTelemetry": {}
            }
            
        except asyncio.TimeoutError:
            return {
                "textResultForLlm": "Tool execution timed out.",
                "resultType": "failure",
                "error": "timeout",
                "toolTelemetry": {}
            }
        except Exception as e:
            self._emit_debug_log_sync(
                f"Tool error: {e}",
                __event_call__
            )
            return {
                "textResultForLlm": f"Tool execution failed: {str(e)}",
                "resultType": "failure",
                "error": str(e),
                "toolTelemetry": {}
            }
    
    return handler

4. 集成到 pipe() 方法

async def pipe(
    self,
    body: dict,
    __metadata__: Optional[dict] = None,
    __event_emitter__=None,
    __event_call__=None,
) -> Union[str, AsyncGenerator]:
    # ... 现有代码 ...
    
    # ✅ 提取并转换 tools
    copilot_tools = []
    if self.valves.ENABLE_TOOLS and body.get("tools"):
        copilot_tools = self._convert_openwebui_tools_to_copilot(
            body["tools"],
            __event_call__
        )
        
        await self._emit_debug_log(
            f"Enabled {len(copilot_tools)} tools",
            __event_call__
        )
    
    # ✅ 传递给 SessionConfig
    session_config = SessionConfig(
        session_id=chat_id if chat_id else None,
        model=real_model_id,
        streaming=body.get("stream", False),
        tools=copilot_tools,  # ✅ 关键
        infinite_sessions=infinite_session_config,
    )
    
    session = await client.create_session(config=session_config)
    # ...

5. 处理 Tool 调用事件

def stream_response(...):
    def handler(event):
        event_type = str(event.type)
        
        # ✅ Tool 调用开始
        if "tool_invocation_started" in event_type:
            tool_name = get_event_data(event, "tool_name", "")
            yield f"\n🔧 **Calling tool**: `{tool_name}`\n"
        
        # ✅ Tool 调用完成
        elif "tool_invocation_completed" in event_type:
            tool_name = get_event_data(event, "tool_name", "")
            result = get_event_data(event, "result", "")
            yield f"\n✅ **Tool result**: {result}\n"
        
        # ✅ Tool 调用失败
        elif "tool_invocation_failed" in event_type:
            tool_name = get_event_data(event, "tool_name", "")
            error = get_event_data(event, "error", "")
            yield f"\n❌ **Tool failed**: `{tool_name}` - {error}\n"

方案 B自定义 Tool 实现

Valves 配置

class Valves(BaseModel):
    CUSTOM_TOOLS: str = Field(
        default="[]",
        description="Custom tools JSON: [{name, description, parameters, implementation}]"
    )

工具定义示例

[
  {
    "name": "calculate",
    "description": "Perform mathematical calculations",
    "parameters": {
      "type": "object",
      "properties": {
        "expression": {
          "type": "string",
          "description": "Math expression, e.g., '2 + 2 * 3'"
        }
      },
      "required": ["expression"]
    },
    "implementation": "eval"  // 或指定 Python 函数名
  }
]

🧪 测试方案

1. 测试 Tool 定义

# 在 OpenWebUI 中创建一个简单的 Function:
# Name: get_time
# Description: Get current time
# Parameters: {"type": "object", "properties": {}}

# 测试对话:
# User: "What time is it?"
# Expected: Copilot 调用 get_time tool返回当前时间

2. 测试 Tool 调用链

# User: "Search for Python tutorials and summarize the top 3 results"
# Expected Flow:
# 1. Copilot calls search_web(query="Python tutorials")
# 2. Copilot receives search results
# 3. Copilot summarizes top 3
# 4. Returns final answer

3. 测试错误处理

# User: "Call a non-existent tool"
# Expected: 返回 "Tool not supported" error

📊 事件监听

Tool 相关事件类型:

  • tool_invocation_started - Tool 调用开始
  • tool_invocation_completed - Tool 完成
  • tool_invocation_failed - Tool 失败
  • tool_parameter_validation_failed - 参数验证失败

⚠️ 注意事项

1. 安全性

  • 验证 tool parameters
  • 限制执行超时
  • 不暴露详细错误信息给 LLM
  • 禁止执行危险命令(如 rm -rf

2. 性能

  • ⏱️ 设置合理的 timeout
  • 🔄 考虑异步执行长时间运行的 tool
  • 📊 记录 tool 执行时间toolTelemetry

3. 调试

  • 🐛 在 DEBUG 模式下记录所有 tool 调用
  • 📝 记录 arguments 和 results
  • 🔍 使用前端 console 显示 tool 流程

🔗 参考资源


📝 实现清单

  • 添加 ENABLE_TOOLS Valve
  • 实现 _convert_openwebui_tools_to_copilot()
  • 实现 _create_tool_handler()
  • 修改 SessionConfig 传递 tools
  • 处理 tool 事件流
  • 添加调试日志
  • 测试基础 tool 调用
  • 测试错误处理
  • 更新文档和 README
  • 同步中文版本

作者: Fu-Jie
版本: v1.0
日期: 2026-01-26