510 lines
14 KiB
Markdown
510 lines
14 KiB
Markdown
|
|
# OpenWebUI Function 集成方案
|
|||
|
|
|
|||
|
|
## 🎯 核心挑战
|
|||
|
|
|
|||
|
|
在 Copilot Tool Handler 中调用 OpenWebUI Functions 的关键问题:
|
|||
|
|
|
|||
|
|
**问题:** Copilot SDK 的 Tool Handler 是一个独立的回调函数,如何在这个上下文中访问和执行 OpenWebUI 的 Function?
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 OpenWebUI Function 系统分析
|
|||
|
|
|
|||
|
|
### 1. Function 数据结构
|
|||
|
|
|
|||
|
|
OpenWebUI 的 Function/Tool 传递格式:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
body = {
|
|||
|
|
"tools": [
|
|||
|
|
{
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": "get_weather",
|
|||
|
|
"description": "Get current weather",
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"location": {"type": "string"}
|
|||
|
|
},
|
|||
|
|
"required": ["location"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Function 执行机制
|
|||
|
|
|
|||
|
|
OpenWebUI Functions 的执行方式有几种可能:
|
|||
|
|
|
|||
|
|
#### 选项 A: 通过 Function ID 调用内部 API
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 假设 OpenWebUI 提供内部 API
|
|||
|
|
from open_webui.apps.webui.models.functions import Functions
|
|||
|
|
|
|||
|
|
function_id = "function_uuid" # 需要从配置中获取
|
|||
|
|
result = await Functions.execute_function(
|
|||
|
|
function_id=function_id,
|
|||
|
|
arguments={"location": "Beijing"}
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 选项 B: 通过 **event_emitter** 触发
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 通过事件系统触发 function 执行
|
|||
|
|
if __event_emitter__:
|
|||
|
|
await __event_emitter__({
|
|||
|
|
"type": "function_call",
|
|||
|
|
"data": {
|
|||
|
|
"name": "get_weather",
|
|||
|
|
"arguments": {"location": "Beijing"}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 选项 C: 自己实现 Function 逻辑
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 在 Pipe 内部实现常用功能
|
|||
|
|
class Pipe:
|
|||
|
|
def _builtin_get_weather(self, location: str) -> dict:
|
|||
|
|
# 实现天气查询
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _builtin_search_web(self, query: str) -> dict:
|
|||
|
|
# 实现网页搜索
|
|||
|
|
pass
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💡 推荐方案:混合架构
|
|||
|
|
|
|||
|
|
### 架构设计
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
User Message
|
|||
|
|
↓
|
|||
|
|
OpenWebUI UI (Functions 已配置)
|
|||
|
|
↓
|
|||
|
|
Pipe.pipe(body) - body 包含 tools[]
|
|||
|
|
↓
|
|||
|
|
转换为 Copilot Tools + 存储 Function Registry
|
|||
|
|
↓
|
|||
|
|
Copilot 决定调用 Tool
|
|||
|
|
↓
|
|||
|
|
Tool Handler 查询 Registry → 执行对应逻辑
|
|||
|
|
↓
|
|||
|
|
返回结果给 Copilot
|
|||
|
|
↓
|
|||
|
|
继续生成回答
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 核心实现
|
|||
|
|
|
|||
|
|
#### 1. Function Registry(函数注册表)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class Pipe:
|
|||
|
|
def __init__(self):
|
|||
|
|
# ...
|
|||
|
|
self._function_registry = {} # {function_name: callable}
|
|||
|
|
self._function_metadata = {} # {function_name: metadata}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 注册 Functions
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _register_openwebui_functions(
|
|||
|
|
self,
|
|||
|
|
owui_functions: List[dict],
|
|||
|
|
__event_emitter__=None,
|
|||
|
|
__event_call__=None
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
注册 OpenWebUI Functions 到内部 registry
|
|||
|
|
|
|||
|
|
关键:将 function 定义和执行逻辑关联起来
|
|||
|
|
"""
|
|||
|
|
for func_def in owui_functions:
|
|||
|
|
if func_def.get("type") != "function":
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
func_info = func_def.get("function", {})
|
|||
|
|
func_name = func_info.get("name")
|
|||
|
|
|
|||
|
|
if not func_name:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 存储元数据
|
|||
|
|
self._function_metadata[func_name] = {
|
|||
|
|
"description": func_info.get("description", ""),
|
|||
|
|
"parameters": func_info.get("parameters", {}),
|
|||
|
|
"original_def": func_def
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 创建执行器(关键)
|
|||
|
|
executor = self._create_function_executor(
|
|||
|
|
func_name,
|
|||
|
|
func_def,
|
|||
|
|
__event_emitter__,
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
self._function_registry[func_name] = executor
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. Function Executor 工厂
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _create_function_executor(
|
|||
|
|
self,
|
|||
|
|
func_name: str,
|
|||
|
|
func_def: dict,
|
|||
|
|
__event_emitter__=None,
|
|||
|
|
__event_call__=None
|
|||
|
|
):
|
|||
|
|
"""
|
|||
|
|
为每个 function 创建执行器
|
|||
|
|
|
|||
|
|
策略:
|
|||
|
|
1. 优先使用内置实现
|
|||
|
|
2. 尝试调用 OpenWebUI API
|
|||
|
|
3. 返回错误
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
async def executor(arguments: dict) -> dict:
|
|||
|
|
# 策略 1: 检查是否有内置实现
|
|||
|
|
builtin_method = getattr(self, f"_builtin_{func_name}", None)
|
|||
|
|
if builtin_method:
|
|||
|
|
self._emit_debug_log_sync(
|
|||
|
|
f"Using builtin implementation for {func_name}",
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
result = builtin_method(arguments)
|
|||
|
|
if inspect.iscoroutine(result):
|
|||
|
|
result = await result
|
|||
|
|
return {"success": True, "result": result}
|
|||
|
|
except Exception as e:
|
|||
|
|
return {"success": False, "error": str(e)}
|
|||
|
|
|
|||
|
|
# 策略 2: 尝试通过 Event Emitter 调用
|
|||
|
|
if __event_emitter__:
|
|||
|
|
try:
|
|||
|
|
# 尝试触发 function_call 事件
|
|||
|
|
response_queue = asyncio.Queue()
|
|||
|
|
|
|||
|
|
await __event_emitter__({
|
|||
|
|
"type": "function_call",
|
|||
|
|
"data": {
|
|||
|
|
"name": func_name,
|
|||
|
|
"arguments": arguments,
|
|||
|
|
"response_queue": response_queue # 回调队列
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 等待结果(带超时)
|
|||
|
|
result = await asyncio.wait_for(
|
|||
|
|
response_queue.get(),
|
|||
|
|
timeout=self.valves.TOOL_TIMEOUT
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return {"success": True, "result": result}
|
|||
|
|
except asyncio.TimeoutError:
|
|||
|
|
return {"success": False, "error": "Function execution timeout"}
|
|||
|
|
except Exception as e:
|
|||
|
|
self._emit_debug_log_sync(
|
|||
|
|
f"Event emitter call failed: {e}",
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
# 继续尝试其他方法
|
|||
|
|
|
|||
|
|
# 策略 3: 尝试调用 OpenWebUI internal API
|
|||
|
|
try:
|
|||
|
|
# 这需要研究 OpenWebUI 源码确定正确的调用方式
|
|||
|
|
from open_webui.apps.webui.models.functions import Functions
|
|||
|
|
|
|||
|
|
# 需要获取 function_id(这是关键问题)
|
|||
|
|
function_id = self._get_function_id_by_name(func_name)
|
|||
|
|
|
|||
|
|
if function_id:
|
|||
|
|
result = await Functions.execute(
|
|||
|
|
function_id=function_id,
|
|||
|
|
params=arguments
|
|||
|
|
)
|
|||
|
|
return {"success": True, "result": result}
|
|||
|
|
except ImportError:
|
|||
|
|
pass
|
|||
|
|
except Exception as e:
|
|||
|
|
self._emit_debug_log_sync(
|
|||
|
|
f"OpenWebUI API call failed: {e}",
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 策略 4: 返回"未实现"错误
|
|||
|
|
return {
|
|||
|
|
"success": False,
|
|||
|
|
"error": f"Function '{func_name}' is not implemented. "
|
|||
|
|
"Please implement it as a builtin method or ensure OpenWebUI API is available."
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return executor
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. Tool Handler 实现
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _create_tool_handler(self, tool_name: str, __event_call__=None):
|
|||
|
|
"""为 Copilot SDK 创建 Tool Handler"""
|
|||
|
|
|
|||
|
|
async def handler(invocation: dict) -> dict:
|
|||
|
|
"""
|
|||
|
|
Copilot Tool Handler
|
|||
|
|
|
|||
|
|
invocation: {
|
|||
|
|
"session_id": str,
|
|||
|
|
"tool_call_id": str,
|
|||
|
|
"tool_name": str,
|
|||
|
|
"arguments": dict
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 从 registry 获取 executor
|
|||
|
|
executor = self._function_registry.get(invocation["tool_name"])
|
|||
|
|
|
|||
|
|
if not executor:
|
|||
|
|
return {
|
|||
|
|
"textResultForLlm": f"Function '{invocation['tool_name']}' not found.",
|
|||
|
|
"resultType": "failure",
|
|||
|
|
"error": "function_not_found",
|
|||
|
|
"toolTelemetry": {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 执行 function
|
|||
|
|
self._emit_debug_log_sync(
|
|||
|
|
f"Executing function: {invocation['tool_name']}({invocation['arguments']})",
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
exec_result = await executor(invocation["arguments"])
|
|||
|
|
|
|||
|
|
# 处理结果
|
|||
|
|
if exec_result.get("success"):
|
|||
|
|
result_text = str(exec_result.get("result", ""))
|
|||
|
|
return {
|
|||
|
|
"textResultForLlm": result_text,
|
|||
|
|
"resultType": "success",
|
|||
|
|
"error": None,
|
|||
|
|
"toolTelemetry": {}
|
|||
|
|
}
|
|||
|
|
else:
|
|||
|
|
error_msg = exec_result.get("error", "Unknown error")
|
|||
|
|
return {
|
|||
|
|
"textResultForLlm": f"Function execution failed: {error_msg}",
|
|||
|
|
"resultType": "failure",
|
|||
|
|
"error": error_msg,
|
|||
|
|
"toolTelemetry": {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
self._emit_debug_log_sync(
|
|||
|
|
f"Tool handler error: {e}",
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
return {
|
|||
|
|
"textResultForLlm": "An unexpected error occurred during function execution.",
|
|||
|
|
"resultType": "failure",
|
|||
|
|
"error": str(e),
|
|||
|
|
"toolTelemetry": {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return handler
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔌 内置 Functions 实现示例
|
|||
|
|
|
|||
|
|
### 示例 1: 获取当前时间
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _builtin_get_current_time(self, arguments: dict) -> str:
|
|||
|
|
"""内置实现:获取当前时间"""
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
timezone = arguments.get("timezone", "UTC")
|
|||
|
|
format_str = arguments.get("format", "%Y-%m-%d %H:%M:%S")
|
|||
|
|
|
|||
|
|
now = datetime.now()
|
|||
|
|
return now.strftime(format_str)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 2: 简单计算器
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def _builtin_calculate(self, arguments: dict) -> str:
|
|||
|
|
"""内置实现:数学计算"""
|
|||
|
|
expression = arguments.get("expression", "")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 安全的数学计算(仅允许基本运算)
|
|||
|
|
allowed_chars = set("0123456789+-*/()., ")
|
|||
|
|
if not all(c in allowed_chars for c in expression):
|
|||
|
|
raise ValueError("Invalid characters in expression")
|
|||
|
|
|
|||
|
|
result = eval(expression, {"__builtins__": {}})
|
|||
|
|
return str(result)
|
|||
|
|
except Exception as e:
|
|||
|
|
raise ValueError(f"Calculation error: {e}")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 3: 网页搜索(需要外部 API)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def _builtin_search_web(self, arguments: dict) -> str:
|
|||
|
|
"""内置实现:网页搜索(使用 DuckDuckGo)"""
|
|||
|
|
query = arguments.get("query", "")
|
|||
|
|
max_results = arguments.get("max_results", 5)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 使用 duckduckgo_search 库
|
|||
|
|
from duckduckgo_search import DDGS
|
|||
|
|
|
|||
|
|
results = []
|
|||
|
|
with DDGS() as ddgs:
|
|||
|
|
for r in ddgs.text(query, max_results=max_results):
|
|||
|
|
results.append({
|
|||
|
|
"title": r.get("title", ""),
|
|||
|
|
"url": r.get("href", ""),
|
|||
|
|
"snippet": r.get("body", "")
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 格式化结果
|
|||
|
|
formatted = "\n\n".join([
|
|||
|
|
f"**{r['title']}**\n{r['url']}\n{r['snippet']}"
|
|||
|
|
for r in results
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
return formatted
|
|||
|
|
except Exception as e:
|
|||
|
|
raise ValueError(f"Search failed: {e}")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🚀 完整集成流程
|
|||
|
|
|
|||
|
|
### pipe() 方法中的集成
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
async def pipe(
|
|||
|
|
self,
|
|||
|
|
body: dict,
|
|||
|
|
__metadata__: Optional[dict] = None,
|
|||
|
|
__event_emitter__=None,
|
|||
|
|
__event_call__=None,
|
|||
|
|
) -> Union[str, AsyncGenerator]:
|
|||
|
|
# ... 现有代码 ...
|
|||
|
|
|
|||
|
|
# ✅ Step 1: 提取 OpenWebUI Functions
|
|||
|
|
owui_functions = body.get("tools", [])
|
|||
|
|
|
|||
|
|
# ✅ Step 2: 注册 Functions
|
|||
|
|
if self.valves.ENABLE_TOOLS and owui_functions:
|
|||
|
|
self._register_openwebui_functions(
|
|||
|
|
owui_functions,
|
|||
|
|
__event_emitter__,
|
|||
|
|
__event_call__
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ✅ Step 3: 转换为 Copilot Tools
|
|||
|
|
copilot_tools = []
|
|||
|
|
for func_name in self._function_registry.keys():
|
|||
|
|
metadata = self._function_metadata[func_name]
|
|||
|
|
copilot_tools.append({
|
|||
|
|
"name": func_name,
|
|||
|
|
"description": metadata["description"],
|
|||
|
|
"parameters": metadata["parameters"],
|
|||
|
|
"handler": self._create_tool_handler(func_name, __event_call__)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# ✅ Step 4: 创建 Session 并传递 Tools
|
|||
|
|
session_config = SessionConfig(
|
|||
|
|
model=real_model_id,
|
|||
|
|
tools=copilot_tools, # ✅ 关键
|
|||
|
|
...
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
session = await client.create_session(config=session_config)
|
|||
|
|
|
|||
|
|
# ... 后续代码 ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ 待解决问题
|
|||
|
|
|
|||
|
|
### 1. Function ID 映射
|
|||
|
|
|
|||
|
|
**问题:** OpenWebUI Functions 通常通过 UUID 标识,但 body 中只有 name
|
|||
|
|
|
|||
|
|
**解决思路:**
|
|||
|
|
|
|||
|
|
- 在 OpenWebUI 启动时建立 name → id 映射表
|
|||
|
|
- 或者修改 OpenWebUI 在 body 中同时传递 id
|
|||
|
|
|
|||
|
|
### 2. Event Emitter 回调机制
|
|||
|
|
|
|||
|
|
**问题:** 不确定 **event_emitter** 是否支持 function_call 事件
|
|||
|
|
|
|||
|
|
**验证方法:**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 测试代码
|
|||
|
|
await __event_emitter__({
|
|||
|
|
"type": "function_call",
|
|||
|
|
"data": {"name": "test_func", "arguments": {}}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 异步执行超时
|
|||
|
|
|
|||
|
|
**问题:** 某些 Functions 可能执行很慢
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
|
|||
|
|
- 实现 timeout 机制(已在 executor 中实现)
|
|||
|
|
- 对于长时间运行的任务,考虑返回"processing"状态
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 实现清单
|
|||
|
|
|
|||
|
|
- [ ] 实现 _function_registry 和 _function_metadata
|
|||
|
|
- [ ] 实现 _register_openwebui_functions()
|
|||
|
|
- [ ] 实现 _create_function_executor()
|
|||
|
|
- [ ] 实现 _create_tool_handler()
|
|||
|
|
- [ ] 实现 3-5 个常用内置 Functions
|
|||
|
|
- [ ] 测试 Function 注册和调用流程
|
|||
|
|
- [ ] 验证 **event_emitter** 机制
|
|||
|
|
- [ ] 研究 OpenWebUI Functions API
|
|||
|
|
- [ ] 添加错误处理和超时机制
|
|||
|
|
- [ ] 更新文档
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**下一步行动:**
|
|||
|
|
|
|||
|
|
1. 实现基础的 Function Registry
|
|||
|
|
2. 添加 2-3 个简单的内置 Functions(如 get_time, calculate)
|
|||
|
|
3. 测试基本的 Tool Calling 流程
|
|||
|
|
4. 根据测试结果调整架构
|
|||
|
|
|
|||
|
|
**作者:** Fu-Jie
|
|||
|
|
**日期:** 2026-01-26
|