432 lines
11 KiB
Markdown
432 lines
11 KiB
Markdown
|
|
# 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(工具定义)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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(处理函数)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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(会话配置)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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 配置
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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 转换器
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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() 方法
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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 调用事件
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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 配置
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class Valves(BaseModel):
|
|||
|
|
CUSTOM_TOOLS: str = Field(
|
|||
|
|
default="[]",
|
|||
|
|
description="Custom tools JSON: [{name, description, parameters, implementation}]"
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 工具定义示例
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"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 定义
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 在 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 调用链
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 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. 测试错误处理
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 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 流程
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔗 参考资源
|
|||
|
|
|
|||
|
|
- [GitHub Copilot SDK 官方文档](https://github.com/github/copilot-sdk)
|
|||
|
|
- [OpenWebUI Function API](https://docs.openwebui.com/features/plugin-system)
|
|||
|
|
- [JSON Schema 规范](https://json-schema.org/)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 实现清单
|
|||
|
|
|
|||
|
|
- [ ] 添加 ENABLE_TOOLS Valve
|
|||
|
|
- [ ] 实现 _convert_openwebui_tools_to_copilot()
|
|||
|
|
- [ ] 实现 _create_tool_handler()
|
|||
|
|
- [ ] 修改 SessionConfig 传递 tools
|
|||
|
|
- [ ] 处理 tool 事件流
|
|||
|
|
- [ ] 添加调试日志
|
|||
|
|
- [ ] 测试基础 tool 调用
|
|||
|
|
- [ ] 测试错误处理
|
|||
|
|
- [ ] 更新文档和 README
|
|||
|
|
- [ ] 同步中文版本
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**作者:** Fu-Jie
|
|||
|
|
**版本:** v1.0
|
|||
|
|
**日期:** 2026-01-26
|