feat(infographic): release v1.5.0 with smart language detection & organize debug tools
This commit is contained in:
82
.github/copilot-instructions.md
vendored
82
.github/copilot-instructions.md
vendored
@@ -62,18 +62,41 @@ plugins/
|
||||
│ ├── ACTION_PLUGIN_TEMPLATE_CN.py # Chinese template
|
||||
│ └── README.md
|
||||
├── filters/ # Filter 插件 (输入处理)
|
||||
│ ├── my_filter/
|
||||
│ │ ├── my_filter.py
|
||||
│ │ ├── 我的过滤器.py
|
||||
│ │ ├── README.md
|
||||
│ │ └── README_CN.md
|
||||
│ └── README.md
|
||||
│ └── ...
|
||||
├── pipes/ # Pipe 插件 (输出处理)
|
||||
│ └── ...
|
||||
└── pipelines/ # Pipeline 插件
|
||||
├── pipelines/ # Pipeline 插件
|
||||
└── ...
|
||||
├── debug/ # 调试与开发工具 (Debug & Development Tools)
|
||||
│ ├── my_debug_tool/
|
||||
│ │ ├── debug_script.py
|
||||
│ │ └── notes.md
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
#### 调试目录规范 (Debug Directory Standards)
|
||||
|
||||
`plugins/debug/` 目录用于存放调试用的脚本、临时验证代码或开发笔记。
|
||||
|
||||
**目录结构 (Directory Structure)**:
|
||||
应根据调试工具所属的插件或功能模块进行子目录分类,而非将文件散落在根目录。
|
||||
|
||||
```
|
||||
plugins/debug/
|
||||
├── my_plugin_name/ # 特定插件的调试文件 (Debug files for specific plugin)
|
||||
│ ├── debug_script.py
|
||||
│ └── guides/
|
||||
├── common_tools/ # 通用调试工具 (General debug tools)
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
**规范说明 (Guidelines)**:
|
||||
- **不强制要求 README**: 该目录下的子项目不需要包含 `README.md`。
|
||||
- **发布豁免**: 该目录下的内容**绝不会**被发布脚本处理。
|
||||
- **内容灵活性**: 可以包含 Python 脚本、Markdown 文档、JSON 数据等。
|
||||
- **分类存放**: 任何调试产物(如 `test_*.py`, `inspect_*.py`)都不应直接存放在项目根目录,必须移动到此目录下相应的子文件夹中。
|
||||
|
||||
### 3. 文档字符串规范 (Docstring Standard)
|
||||
|
||||
每个插件文件必须以标准化的文档字符串开头:
|
||||
@@ -409,6 +432,51 @@ async def long_running_task_with_notification(self, event_emitter, ...):
|
||||
return task_future.result()
|
||||
```
|
||||
|
||||
### 7. 前端数据获取与交互规范 (Frontend Data Access & Interaction)
|
||||
|
||||
#### 获取前端信息 (Retrieving Frontend Info)
|
||||
|
||||
当需要获取用户浏览器的上下文信息(如语言、时区、LocalStorage)时,**必须**使用 `__event_call__` 的 `execute` 类型,而不是通过文件上传或复杂的 API 请求。
|
||||
|
||||
```python
|
||||
async def _get_frontend_value(self, js_code: str) -> str:
|
||||
"""Helper to execute JS and get return value."""
|
||||
try:
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": js_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
return str(response)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to execute JS: {e}")
|
||||
return ""
|
||||
|
||||
# 示例:获取界面语言 (Get UI Language)
|
||||
async def get_user_language(self):
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
return await self._get_frontend_value(js_code)
|
||||
```
|
||||
|
||||
#### 适用场景与引导 (Usage Guidelines)
|
||||
|
||||
- **语言适配**: 动态获取界面语言 (`ru-RU`, `zh-CN`) 自动切换输出语言。
|
||||
- **时区处理**: 获取 `Intl.DateTimeFormat().resolvedOptions().timeZone` 处理时间。
|
||||
- **客户端存储**: 读取 `localStorage` 中的用户偏好设置。
|
||||
- **硬件能力**: 获取 `navigator.clipboard` 或 `navigator.geolocation` (需授权)。
|
||||
|
||||
**注意**: 即使插件有 `Valves` 配置,也应优先尝试自动探测,提升用户体验。
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Action 插件规范 (Action Plugin Standards)
|
||||
|
||||
1
.github/workflows/publish_plugin.yml
vendored
1
.github/workflows/publish_plugin.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/**/*.py'
|
||||
- '!plugins/debug/**'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -349,6 +349,53 @@ await __event_emitter__(
|
||||
)
|
||||
```
|
||||
|
||||
#### Advanced Use Case: Retrieving Frontend Data
|
||||
|
||||
One of the most powerful capabilities of the `execute` event type is the ability to fetch data from the browser environment (JavaScript) and return it to your Python backend. This allows plugins to access information like:
|
||||
|
||||
- `localStorage` items (user preferences, tokens)
|
||||
- `navigator` properties (language, geolocation, platform)
|
||||
- `document` properties (cookies, URL parameters)
|
||||
|
||||
**How it works:**
|
||||
The JavaScript code you provide in the `"code"` field is executed in the browser. If your JS code includes a `return` statement, that value is sent back to Python as the result of `await __event_call__`.
|
||||
|
||||
**Example: Getting the User's UI Language**
|
||||
|
||||
```python
|
||||
try:
|
||||
# Execute JS on the frontend to get language settings
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
# This JS code runs in the browser.
|
||||
# The 'return' value is sent back to Python.
|
||||
"code": """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
""",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 'response' will contain the string returned by JS (e.g., "en-US", "zh-CN")
|
||||
# Note: Wrap in try-except to handle potential timeouts or JS errors
|
||||
logger.info(f"Frontend Language: {response}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get frontend data: {e}")
|
||||
```
|
||||
|
||||
**Key capabilities unlocked:**
|
||||
- **Context Awareness:** Adapt responses based on user time zone or language.
|
||||
- **Client-Side Storage:** Use `localStorage` to persist simple plugin settings without a database.
|
||||
- **Hardware Access:** Request geolocation or clipboard access (requires user permission).
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ When & Where to Use Events
|
||||
@@ -421,4 +468,4 @@ Refer to this document for common event types and structures, and explore Open W
|
||||
|
||||
---
|
||||
|
||||
**Happy event-driven coding in Open WebUI! 🚀**
|
||||
**Happy event-driven coding in Open WebUI! 🚀**
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# GitHub Copilot SDK Pipe for OpenWebUI
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.1.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.2.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that allows you to use GitHub Copilot models (such as `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`) directly within OpenWebUI. It is built upon the official [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk), providing a native integration experience.
|
||||
|
||||
## 🚀 What's New (v0.1.0)
|
||||
## 🚀 What's New (v0.2.3)
|
||||
|
||||
* **♾️ Infinite Sessions**: Automatic context compaction for long-running conversations. No more context limit errors!
|
||||
* **🧠 Thinking Process**: Real-time display of model reasoning/thinking process (for supported models).
|
||||
* **📂 Workspace Control**: Restricted workspace directory for secure file operations.
|
||||
* **🔍 Model Filtering**: Exclude specific models using keywords (e.g., `codex`, `haiku`).
|
||||
* **💾 Session Persistence**: Improved session resume logic using OpenWebUI chat ID mapping.
|
||||
* **🧩 Per-user Overrides**: Added user-level overrides for `REASONING_EFFORT`, `CLI_PATH`, `DEBUG`, `SHOW_THINKING`, and `MODEL_ID`.
|
||||
* **🧠 Thinking Output Reliability**: Thinking visibility now respects the user setting and is correctly passed into streaming.
|
||||
* **📝 Formatting Enforcement**: Added automatic formatting hints to ensure outputs are well-structured (paragraphs, lists).
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
* **🚀 Official SDK Integration**: Built on the official SDK for stability and reliability.
|
||||
* **🛠️ Custom Tools Support**: Example tools included (random number). Easy to extend with your own tools.
|
||||
* **💬 Multi-turn Conversation**: Automatically concatenates history context so Copilot understands your previous messages.
|
||||
* **🌊 Streaming Output**: Supports typewriter effect for fast responses.
|
||||
* **🖼️ Multimodal Support**: Supports image uploads, automatically converting them to attachments for Copilot (requires model support).
|
||||
* **🛠️ Zero-config Installation**: Automatically detects and downloads the GitHub Copilot CLI, ready to use out of the box.
|
||||
* **🔑 Secure Authentication**: Supports Fine-grained Personal Access Tokens for minimized permissions.
|
||||
* **🐛 Debug Mode**: Built-in detailed log output for easy connection troubleshooting.
|
||||
* **🐛 Debug Mode**: Built-in detailed log output (browser console) for easy troubleshooting.
|
||||
* **⚠️ Single Node Only**: Due to local session storage, this plugin currently supports single-node OpenWebUI deployment or multi-node with sticky sessions enabled.
|
||||
|
||||
## 📦 Installation & Usage
|
||||
|
||||
@@ -41,24 +41,59 @@ Find "GitHub Copilot" in the function list and click the **⚙️ (Valves)** ico
|
||||
| **GH_TOKEN** | **(Required)** Your GitHub Token. | - |
|
||||
| **MODEL_ID** | The model name to use. Recommended `gpt-5-mini` or `gpt-5`. | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Path to the Copilot CLI. Will download automatically if not found. | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | Whether to enable debug logs (output to chat). | `True` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process. | `True` |
|
||||
| **DEBUG** | Whether to enable debug logs (output to browser console). | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI log level: none, error, warning, info, debug, all. | `error` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | Show session workspace path and summary in debug mode. | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | Exclude models containing these keywords (comma separated). | - |
|
||||
| **WORKSPACE_DIR** | Restricted workspace directory for file operations. | - |
|
||||
| **INFINITE_SESSION** | Enable Infinite Sessions (automatic context compaction). | `True` |
|
||||
| **COMPACTION_THRESHOLD** | Background compaction threshold (0.0-1.0). | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | Buffer exhaustion threshold (0.0-1.0). | `0.95` |
|
||||
| **TIMEOUT** | Timeout for each stream chunk (seconds). | `300` |
|
||||
| **CUSTOM_ENV_VARS** | Custom environment variables (JSON format). | - |
|
||||
| **REASONING_EFFORT** | Reasoning effort level: low, medium, high. `xhigh` is supported for gpt-5.2-codex. | `medium` |
|
||||
| **ENFORCE_FORMATTING** | Add formatting instructions to system prompt for better readability. | `True` |
|
||||
| **ENABLE_TOOLS** | Enable custom tools (example: random number). | `False` |
|
||||
| **AVAILABLE_TOOLS** | Available tools: 'all' or comma-separated list. | `all` |
|
||||
|
||||
### 3. Get GH_TOKEN
|
||||
#### User Valves (per-user overrides)
|
||||
|
||||
These optional settings can be set per user (overrides global Valves):
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | Reasoning effort level (low/medium/high/xhigh). | - |
|
||||
| **CLI_PATH** | Custom path to Copilot CLI. | - |
|
||||
| **DEBUG** | Enable technical debug logs. | `False` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **MODEL_ID** | Custom model ID. | - |
|
||||
|
||||
### 3. Using Custom Tools (🆕 Optional)
|
||||
|
||||
This pipe includes **1 example tool** to demonstrate tool calling:
|
||||
|
||||
* **🎲 generate_random_number**: Generate random integers
|
||||
|
||||
**To enable:**
|
||||
|
||||
1. Set `ENABLE_TOOLS: true` in Valves
|
||||
2. Try: "Give me a random number"
|
||||
|
||||
**📚 For detailed usage and creating your own tools, see [plugins/pipes/github-copilot-sdk/TOOLS_USAGE.md](plugins/pipes/github-copilot-sdk/TOOLS_USAGE.md)**
|
||||
|
||||
### 4. Get GH_TOKEN
|
||||
|
||||
For security, it is recommended to use a **Fine-grained Personal Access Token**:
|
||||
|
||||
1. Visit [GitHub Token Settings](https://github.com/settings/tokens?type=beta).
|
||||
2. Click **Generate new token**.
|
||||
3. **Repository access**: Select `All repositories` or `Public Repositories`.
|
||||
3. **Repository access**: Select **Public repositories** (Required to access Copilot permissions).
|
||||
4. **Permissions**:
|
||||
* Click **Account permissions**.
|
||||
* Find **Copilot Requests**, select **Read and write** (or Access).
|
||||
|
||||
* Click **Account permissions**.
|
||||
* Find **Copilot Requests** (It defaults to **Read-only**, no selection needed).
|
||||
|
||||
5. Generate and copy the Token.
|
||||
|
||||
## 📋 Dependencies
|
||||
@@ -72,9 +107,10 @@ This Pipe will automatically attempt to install the following dependencies:
|
||||
|
||||
* **Stuck on "Waiting..."**:
|
||||
* Check if `GH_TOKEN` is correct and has `Copilot Requests` permission.
|
||||
* Try changing `MODEL_ID` to `gpt-4o` or `copilot-chat`.
|
||||
* **Images not recognized**:
|
||||
* Ensure `MODEL_ID` is a model that supports multimodal input.
|
||||
* **Thinking not shown**:
|
||||
* Ensure **streaming is enabled** and the selected model supports reasoning output.
|
||||
* **CLI Installation Failed**:
|
||||
* Ensure the OpenWebUI container has internet access.
|
||||
* You can manually download the CLI and specify `CLI_PATH` in Valves.
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# GitHub Copilot SDK 官方管道
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.1.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.2.3 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,允许你直接在 OpenWebUI 中使用 GitHub Copilot 模型(如 `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`)。它基于官方 [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk) 构建,提供了原生级的集成体验。
|
||||
|
||||
## 🚀 最新特性 (v0.1.0)
|
||||
## 🚀 最新特性 (v0.2.3)
|
||||
|
||||
* **♾️ 无限会话 (Infinite Sessions)**:支持长对话的自动上下文压缩,告别上下文超限错误!
|
||||
* **🧠 思考过程展示**:实时显示模型的推理/思考过程(需模型支持)。
|
||||
* **📂 工作目录控制**:支持设置受限工作目录,确保文件操作安全。
|
||||
* **🔍 模型过滤**:支持通过关键词排除特定模型(如 `codex`, `haiku`)。
|
||||
* **💾 会话持久化**: 改进的会话恢复逻辑,直接关联 OpenWebUI 聊天 ID,连接更稳定。
|
||||
* **🧩 用户级覆盖**:新增 `REASONING_EFFORT`、`CLI_PATH`、`DEBUG`、`SHOW_THINKING`、`MODEL_ID` 的用户级覆盖。
|
||||
* **🧠 思考输出可靠性**:思考显示会遵循用户设置,并正确传递到流式输出中。
|
||||
* **📝 格式化输出增强**:自动优化输出格式(段落、列表),并解决了在某些界面下显示过于紧凑的问题。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
* **🚀 官方 SDK 集成**:基于官方 SDK,稳定可靠。
|
||||
* **🛠️ 自定义工具支持**:内置示例工具(随机数)。易于扩展自定义工具。
|
||||
* **💬 多轮对话支持**:自动拼接历史上下文,Copilot 能理解你的前文。
|
||||
* **🌊 流式输出 (Streaming)**:支持打字机效果,响应迅速。
|
||||
* **🖼️ 多模态支持**:支持上传图片,自动转换为附件发送给 Copilot(需模型支持)。
|
||||
* **🛠️ 零配置安装**:自动检测并下载 GitHub Copilot CLI,开箱即用。
|
||||
* **🔑 安全认证**:支持 Fine-grained Personal Access Tokens,权限最小化。
|
||||
* **🐛 调试模式**:内置详细的日志输出,方便排查连接问题。
|
||||
* **🐛 调试模式**:内置详细的日志输出(浏览器控制台),方便排查问题。
|
||||
* **⚠️ 仅支持单节点**:由于会话状态存储在本地,本插件目前仅支持 OpenWebUI 单节点部署,或开启了会话粘性 (Sticky Session) 的多节点集群。
|
||||
|
||||
## 📦 安装与使用
|
||||
|
||||
@@ -41,24 +41,59 @@
|
||||
| **GH_TOKEN** | **(必填)** 你的 GitHub Token。 | - |
|
||||
| **MODEL_ID** | 使用的模型名称。推荐 `gpt-5-mini` 或 `gpt-5`。 | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Copilot CLI 的路径。如果未找到会自动下载。 | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | 是否开启调试日志(输出到对话框)。 | `True` |
|
||||
| **SHOW_THINKING** | 是否显示模型推理/思考过程。 | `True` |
|
||||
| **DEBUG** | 是否开启调试日志(输出到浏览器控制台)。 | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI 日志级别: none, error, warning, info, debug, all。 | `error` |
|
||||
| **SHOW_THINKING** | 是否显示模型推理/思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | 在调试模式下显示会话工作空间路径和摘要。 | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | 排除包含这些关键词的模型 (逗号分隔)。 | - |
|
||||
| **WORKSPACE_DIR** | 文件操作的受限工作目录。 | - |
|
||||
| **INFINITE_SESSION** | 启用无限会话 (自动上下文压缩)。 | `True` |
|
||||
| **COMPACTION_THRESHOLD** | 后台压缩阈值 (0.0-1.0)。 | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | 缓冲耗尽阈值 (0.0-1.0)。 | `0.95` |
|
||||
| **TIMEOUT** | 流式数据块超时时间 (秒)。 | `300` |
|
||||
| **CUSTOM_ENV_VARS** | 自定义环境变量 (JSON 格式)。 | - |
|
||||
| **ENABLE_TOOLS** | 启用自定义工具 (示例:随机数)。 | `False` |
|
||||
| **AVAILABLE_TOOLS** | 可用工具: 'all' 或逗号分隔列表。 | `all` |
|
||||
| **REASONING_EFFORT** | 推理强度级别:low, medium, high。`gpt-5.2-codex`额外支持`xhigh`。 | `medium` |
|
||||
| **ENFORCE_FORMATTING** | 是否强制添加格式化指导,以提高输出可读性。 | `True` |
|
||||
|
||||
### 3. 获取 GH_TOKEN
|
||||
#### 用户 Valves(按用户覆盖)
|
||||
|
||||
以下设置可按用户单独配置(覆盖全局 Valves):
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | 推理强度级别(low/medium/high/xhigh)。 | - |
|
||||
| **CLI_PATH** | 自定义 Copilot CLI 路径。 | - |
|
||||
| **DEBUG** | 是否启用技术调试日志。 | `False` |
|
||||
| **SHOW_THINKING** | 是否显示思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **MODEL_ID** | 自定义模型 ID。 | - |
|
||||
|
||||
### 3. 使用自定义工具 (🆕 可选)
|
||||
|
||||
本 Pipe 内置了 **1 个示例工具**来展示工具调用功能:
|
||||
|
||||
* **🎲 generate_random_number**:生成随机整数
|
||||
|
||||
**启用方法:**
|
||||
|
||||
1. 在 Valves 中设置 `ENABLE_TOOLS: true`
|
||||
2. 尝试问:“给我一个随机数”
|
||||
|
||||
**📚 详细使用说明和创建自定义工具,请参阅 [plugins/pipes/github-copilot-sdk/TOOLS_USAGE.md](plugins/pipes/github-copilot-sdk/TOOLS_USAGE.md)**
|
||||
|
||||
### 4. 获取 GH_TOKEN
|
||||
|
||||
为了安全起见,推荐使用 **Fine-grained Personal Access Token**:
|
||||
|
||||
1. 访问 [GitHub Token Settings](https://github.com/settings/tokens?type=beta)。
|
||||
2. 点击 **Generate new token**。
|
||||
3. **Repository access**: 选择 `All repositories` 或 `Public Repositories`。
|
||||
3. **Repository access**: 选择 **Public repositories** (必须选择此项才能看到 Copilot 权限)。
|
||||
4. **Permissions**:
|
||||
* 点击 **Account permissions**。
|
||||
* 找到 **Copilot Requests**,选择 **Read and write** (或 Access)。
|
||||
|
||||
* 点击 **Account permissions**。
|
||||
* 找到 **Copilot Requests** (默认即为 **Read-only**,无需手动修改)。
|
||||
|
||||
5. 生成并复制 Token。
|
||||
|
||||
## 📋 依赖说明
|
||||
@@ -72,9 +107,10 @@
|
||||
|
||||
* **一直显示 "Waiting..."**:
|
||||
* 检查 `GH_TOKEN` 是否正确且拥有 `Copilot Requests` 权限。
|
||||
* 尝试将 `MODEL_ID` 改为 `gpt-4o` 或 `copilot-chat`。
|
||||
* **图片无法识别**:
|
||||
* 确保 `MODEL_ID` 是支持多模态的模型。
|
||||
* **看不到思考过程**:
|
||||
* 确认已开启**流式输出**,且所选模型支持推理输出。
|
||||
* **CLI 安装失败**:
|
||||
* 确保 OpenWebUI 容器有外网访问权限。
|
||||
* 你可以手动下载 CLI 并挂载到容器中,然后在 Valves 中指定 `CLI_PATH`。
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# 📊 Smart Infographic (AntV)
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.4.9 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.5.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click.
|
||||
|
||||
## 🔥 What's New in v1.4.9
|
||||
## 🔥 What's New in v1.5.0
|
||||
|
||||
- 🌐 **Smart Language Detection**: Automatically detects the accurate UI language from your browser.
|
||||
- 🗣️ **Context-Aware Generation**: Generated infographics now strictly follow the language of your input content (e.g., input Japanese -> output Japanese infographic).
|
||||
- 🐛 **Bug Fixes**: Fixed issues with language synchronization between the UI and generated content.
|
||||
|
||||
### Previous: v1.4.9
|
||||
|
||||
- 🎨 **70+ Official Templates**: Integrated comprehensive AntV infographic template library.
|
||||
- 🖼️ **Iconify & unDraw Support**: Richer visuals with official icons and illustrations.
|
||||
@@ -63,7 +69,6 @@ You can adjust the following parameters in the plugin settings to optimize the g
|
||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
|
||||
## 📝 Syntax Example (For Advanced Users)
|
||||
|
||||
You can also input this syntax directly for AI to render:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# 📊 智能信息图 (AntV Infographic)
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.4.9 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.5.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
||||
|
||||
## 🔥 v1.4.9 更新日志
|
||||
## 🔥 v1.5.0 更新日志
|
||||
|
||||
- 🌐 **智能语言检测**:自动从浏览器准确识别当前界面语言设置。
|
||||
- 🗣️ **上下文感知生成**:生成的信息图内容现在严格跟随用户输入内容的语言(例如:输入日语 -> 生成日语信息图)。
|
||||
- 🐛 **问题修复**:修复了界面语言与生成内容语言不同步的问题。
|
||||
|
||||
### 此前: v1.4.9
|
||||
|
||||
- 🎨 **70+ 官方模板**:全面集成 AntV 官方信息图模板库。
|
||||
- 🖼️ **图标与插图支持**:支持 Iconify 图标库与 unDraw 插图库,视觉效果更丰富。
|
||||
@@ -63,7 +69,6 @@
|
||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
|
||||
## 📝 语法示例 (高级用户)
|
||||
|
||||
你也可以直接输入以下语法让 AI 渲染:
|
||||
|
||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.4.9
|
||||
version: 1.5.0
|
||||
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
|
||||
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
|
||||
"""
|
||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Important Language Rule
|
||||
- **GENERATE CONTENT IN INPUT LANGUAGE**: You must generate the text content of the infographic in the **exact same language** as the user's input content (the text you are analyzing).
|
||||
- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the infographic content must be in Chinese. If input is Japanese, output Japanese.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
@@ -958,7 +962,11 @@ class Action:
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""Safely extracts user context information."""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -967,10 +975,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "User")
|
||||
user_language = user_data.get("language", "en-US")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", "en-US"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1469,18 +1499,10 @@ class Action:
|
||||
logger.info("Action: Infographic started (v1.4.0)")
|
||||
|
||||
# Get user information
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = __user__[0].get("language", "en") if __user__ else "en"
|
||||
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "en")
|
||||
user_name = __user__.get("name", "User")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
user_language = user_ctx["user_language"]
|
||||
|
||||
# Get current time
|
||||
now = datetime.now()
|
||||
|
||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.4.9
|
||||
version: 1.5.0
|
||||
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
||||
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
||||
"""
|
||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Important Language Rule (语言规则)
|
||||
- **Priority Input Language (优先使用输入语言)**: You must generate the text content of the infographic in the **exact same language** as the user's input content.
|
||||
- **Example**: If the user provides a summary in Chinese, the labels and descriptions in the infographic must be in Chinese.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
@@ -974,7 +978,11 @@ class Action:
|
||||
"Sunday": "星期日",
|
||||
}
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""安全提取用户上下文信息。"""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -983,10 +991,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "用户")
|
||||
user_language = user_data.get("language", "zh-CN")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'zh-CN'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "用户"),
|
||||
"user_language": user_data.get("language", "zh-CN"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1509,20 +1539,10 @@ class Action:
|
||||
logger.info("Action: 信息图启动 (v1.4.0)")
|
||||
|
||||
# 获取用户信息
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = (
|
||||
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
|
||||
)
|
||||
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "zh-CN")
|
||||
user_name = __user__.get("name", "用户")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
user_language = user_ctx["user_language"]
|
||||
|
||||
# 获取当前时间
|
||||
now = datetime.now()
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
# 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
|
||||
@@ -0,0 +1,480 @@
|
||||
# OpenWebUI Native Tool Call Display Implementation Guide
|
||||
|
||||
**Date:** 2026-01-27
|
||||
**Purpose:** Analyze and implement OpenWebUI's native tool call display mechanism
|
||||
|
||||
---
|
||||
|
||||
## 📸 Current vs Native Display
|
||||
|
||||
### Current Implementation
|
||||
|
||||
```markdown
|
||||
> 🔧 **Running Tool**: `search_chats`
|
||||
|
||||
> ✅ **Tool Completed**: {...}
|
||||
```
|
||||
|
||||
### OpenWebUI Native Display (from screenshot)
|
||||
|
||||
- ✅ Collapsible panel: "查看来自 search_chats 的结果"
|
||||
- ✅ Formatted JSON display
|
||||
- ✅ Syntax highlighting
|
||||
- ✅ Expand/collapse functionality
|
||||
- ✅ Clean visual separation
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Understanding OpenWebUI's Tool Call Format
|
||||
|
||||
### Standard OpenAI Tool Call Message Format
|
||||
|
||||
OpenWebUI follows the OpenAI Chat Completion API format for tool calls:
|
||||
|
||||
#### 1. Assistant Message with Tool Calls
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None, # or explanatory text
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_chats",
|
||||
"arguments": '{"query": ""}'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Tool Response Message
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"name": "search_chats", # Optional but recommended
|
||||
"content": '{"count": 5, "results": [...]}' # JSON string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Implementation Strategy for Native Display
|
||||
|
||||
### Option 1: Event Emitter Approach (Recommended)
|
||||
|
||||
Use OpenWebUI's event emitter to send structured tool call data:
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# When tool execution starts
|
||||
if event_type == "tool.execution_start":
|
||||
await self._emit_tool_call_start(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
# When tool execution completes
|
||||
elif event_type == "tool.execution_complete":
|
||||
await self._emit_tool_call_result(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
result=result_content
|
||||
)
|
||||
```
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
```python
|
||||
async def _emit_tool_call_start(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
arguments: dict
|
||||
):
|
||||
"""Emit a tool call start event to OpenWebUI."""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# OpenWebUI expects tool_calls in assistant message format
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to emit tool call start: {e}")
|
||||
|
||||
async def _emit_tool_call_result(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
result: Any
|
||||
):
|
||||
"""Emit a tool call result to OpenWebUI."""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# Format result as JSON string
|
||||
if isinstance(result, str):
|
||||
result_content = result
|
||||
else:
|
||||
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
# OpenWebUI expects tool results in tool message format
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": result_content
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to emit tool result: {e}")
|
||||
```
|
||||
|
||||
### Option 2: Message History Injection
|
||||
|
||||
Modify the conversation history to include tool calls:
|
||||
|
||||
```python
|
||||
# After tool execution, append to messages
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments)
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": json.dumps(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Challenges with Current Architecture
|
||||
|
||||
### 1. Streaming Context
|
||||
|
||||
Our current implementation uses:
|
||||
|
||||
- **Queue-based streaming**: Events → Queue → Yield chunks
|
||||
- **Text chunks only**: We yield plain text, not structured messages
|
||||
|
||||
OpenWebUI's native display requires:
|
||||
|
||||
- **Structured message events**: Not text chunks
|
||||
- **Message-level control**: Need to emit complete messages
|
||||
|
||||
### 2. Event Emitter Compatibility
|
||||
|
||||
**Current usage:**
|
||||
|
||||
```python
|
||||
# We use event_emitter for status/notifications
|
||||
await event_emitter({
|
||||
"type": "status",
|
||||
"data": {"description": "Processing..."}
|
||||
})
|
||||
```
|
||||
|
||||
**Need for tool calls:**
|
||||
|
||||
```python
|
||||
# Need to emit message-type events
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"content": "..."
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Question:** Does `__event_emitter__` support `message` type events?
|
||||
|
||||
### 3. Session SDK Events vs OpenWebUI Messages
|
||||
|
||||
**Copilot SDK events:**
|
||||
|
||||
- `tool.execution_start` → We get tool name, arguments
|
||||
- `tool.execution_complete` → We get tool result
|
||||
- Designed for streaming text output
|
||||
|
||||
**OpenWebUI messages:**
|
||||
|
||||
- Expect structured message objects
|
||||
- Not designed for mid-stream injection
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Experimental Implementation
|
||||
|
||||
### Step 1: Add Valve for Native Display
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="Use OpenWebUI's native tool call display instead of markdown formatting"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 2: Modify Tool Event Handling
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# ...existing code...
|
||||
|
||||
def handler(event):
|
||||
event_type = get_event_type(event)
|
||||
|
||||
if event_type == "tool.execution_start":
|
||||
tool_name = safe_get_data_attr(event, "name")
|
||||
|
||||
# Get tool arguments
|
||||
tool_input = safe_get_data_attr(event, "input") or {}
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||
|
||||
if tool_call_id:
|
||||
active_tools[tool_call_id] = {
|
||||
"name": tool_name,
|
||||
"arguments": tool_input
|
||||
}
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# Emit structured tool call
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_start(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
tool_input
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Current markdown display
|
||||
queue.put_nowait(f"\n\n> 🔧 **Running Tool**: `{tool_name}`\n\n")
|
||||
|
||||
elif event_type == "tool.execution_complete":
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||
tool_info = active_tools.get(tool_call_id, {})
|
||||
tool_name = tool_info.get("name", "Unknown")
|
||||
|
||||
# Extract result
|
||||
result_obj = safe_get_data_attr(event, "result")
|
||||
result_content = ""
|
||||
if hasattr(result_obj, "content"):
|
||||
result_content = result_obj.content
|
||||
elif isinstance(result_obj, dict):
|
||||
result_content = result_obj.get("content", "")
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# Emit structured tool result
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_result(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
result_content
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Current markdown display
|
||||
queue.put_nowait(f"> ✅ **Tool Completed**: {result_content}\n\n")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Testing Plan
|
||||
|
||||
### Test 1: Event Emitter Message Type Support
|
||||
|
||||
```python
|
||||
# In a test conversation, try:
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "Test message"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Expected:** Message appears in chat
|
||||
**If fails:** Event emitter doesn't support message type
|
||||
|
||||
### Test 2: Tool Call Message Format
|
||||
|
||||
```python
|
||||
# Send a tool call message
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": "test_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"arguments": '{"param": "value"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
# Send tool result
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": "test_123",
|
||||
"name": "test_tool",
|
||||
"content": '{"result": "success"}'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Expected:** OpenWebUI displays collapsible tool panel
|
||||
**If fails:** Event format doesn't match OpenWebUI expectations
|
||||
|
||||
### Test 3: Mid-Stream Tool Call Injection
|
||||
|
||||
Test if tool call messages can be injected during streaming:
|
||||
|
||||
```python
|
||||
# Start streaming text
|
||||
yield "Processing your request..."
|
||||
|
||||
# Mid-stream: emit tool call
|
||||
await __event_emitter__({"type": "message", "data": {...}})
|
||||
|
||||
# Continue streaming
|
||||
yield "Done!"
|
||||
```
|
||||
|
||||
**Expected:** Tool panel appears mid-response
|
||||
**Risk:** May break streaming flow
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
- [x] Add `REASONING_EFFORT` valve (completed)
|
||||
- [ ] Add `USE_NATIVE_TOOL_DISPLAY` valve
|
||||
- [ ] Implement `_emit_tool_call_start()` helper
|
||||
- [ ] Implement `_emit_tool_call_result()` helper
|
||||
- [ ] Modify tool event handling in `stream_response()`
|
||||
- [ ] Test event emitter message type support
|
||||
- [ ] Test tool call message format
|
||||
- [ ] Test mid-stream injection
|
||||
- [ ] Update documentation
|
||||
- [ ] Add user configuration guide
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Recommendation
|
||||
|
||||
### Hybrid Approach (Safest)
|
||||
|
||||
Keep both display modes:
|
||||
|
||||
1. **Default (Current):** Markdown-based display
|
||||
- ✅ Works reliably with streaming
|
||||
- ✅ No OpenWebUI API dependencies
|
||||
- ✅ Consistent across versions
|
||||
|
||||
2. **Experimental (Native):** Structured tool messages
|
||||
- ✅ Better visual integration
|
||||
- ⚠️ Requires testing with OpenWebUI internals
|
||||
- ⚠️ May not work in all scenarios
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```python
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="[EXPERIMENTAL] Use OpenWebUI's native tool call display"
|
||||
)
|
||||
```
|
||||
|
||||
### Why Markdown Display is Currently Better
|
||||
|
||||
1. **Reliability:** Always works with streaming
|
||||
2. **Flexibility:** Can customize format easily
|
||||
3. **Context:** Shows tools inline with reasoning
|
||||
4. **Compatibility:** Works across OpenWebUI versions
|
||||
|
||||
### When to Use Native Display
|
||||
|
||||
- Non-streaming mode (easier to inject messages)
|
||||
- After confirming event emitter supports message type
|
||||
- For tools with large JSON results (better formatting)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Research OpenWebUI Source Code**
|
||||
- Check `__event_emitter__` implementation
|
||||
- Verify supported event types
|
||||
- Test message injection patterns
|
||||
|
||||
2. **Create Proof of Concept**
|
||||
- Simple test plugin
|
||||
- Emit tool call messages
|
||||
- Verify UI rendering
|
||||
|
||||
3. **Document Findings**
|
||||
- Update this guide with test results
|
||||
- Add code examples that work
|
||||
- Create migration guide if successful
|
||||
|
||||
---
|
||||
|
||||
## 🔗 References
|
||||
|
||||
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||
- [OpenWebUI Plugin Development](https://docs.openwebui.com/)
|
||||
- [Copilot SDK Events](https://github.com/github/copilot-sdk)
|
||||
|
||||
---
|
||||
|
||||
**Author:** Fu-Jie
|
||||
**Status:** Analysis Complete - Implementation Pending Testing
|
||||
@@ -0,0 +1,480 @@
|
||||
# OpenWebUI 原生工具调用展示实现指南
|
||||
|
||||
**日期:** 2026-01-27
|
||||
**目的:** 分析并实现 OpenWebUI 的原生工具调用展示机制
|
||||
|
||||
---
|
||||
|
||||
## 📸 当前展示 vs 原生展示
|
||||
|
||||
### 当前实现
|
||||
|
||||
```markdown
|
||||
> 🔧 **Running Tool**: `search_chats`
|
||||
|
||||
> ✅ **Tool Completed**: {...}
|
||||
```
|
||||
|
||||
### OpenWebUI 原生展示(来自截图)
|
||||
|
||||
- ✅ 可折叠面板:"查看来自 search_chats 的结果"
|
||||
- ✅ 格式化的 JSON 显示
|
||||
- ✅ 语法高亮
|
||||
- ✅ 展开/折叠功能
|
||||
- ✅ 清晰的视觉分隔
|
||||
|
||||
---
|
||||
|
||||
## 🔍 理解 OpenWebUI 的工具调用格式
|
||||
|
||||
### 标准 OpenAI 工具调用消息格式
|
||||
|
||||
OpenWebUI 遵循 OpenAI Chat Completion API 的工具调用格式:
|
||||
|
||||
#### 1. 带工具调用的助手消息
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None, # 或解释性文本
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_chats",
|
||||
"arguments": '{"query": ""}'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 工具响应消息
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"name": "search_chats", # 可选但推荐
|
||||
"content": '{"count": 5, "results": [...]}' # JSON 字符串
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 原生展示的实现策略
|
||||
|
||||
### 方案 1:事件发射器方法(推荐)
|
||||
|
||||
使用 OpenWebUI 的事件发射器发送结构化工具调用数据:
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# 工具执行开始时
|
||||
if event_type == "tool.execution_start":
|
||||
await self._emit_tool_call_start(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
# 工具执行完成时
|
||||
elif event_type == "tool.execution_complete":
|
||||
await self._emit_tool_call_result(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
result=result_content
|
||||
)
|
||||
```
|
||||
|
||||
#### 辅助方法
|
||||
|
||||
```python
|
||||
async def _emit_tool_call_start(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
arguments: dict
|
||||
):
|
||||
"""向 OpenWebUI 发射工具调用开始事件。"""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# OpenWebUI 期望 assistant 消息格式的 tool_calls
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"发射工具调用开始事件失败: {e}")
|
||||
|
||||
async def _emit_tool_call_result(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
result: Any
|
||||
):
|
||||
"""向 OpenWebUI 发射工具调用结果。"""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# 将结果格式化为 JSON 字符串
|
||||
if isinstance(result, str):
|
||||
result_content = result
|
||||
else:
|
||||
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
# OpenWebUI 期望 tool 消息格式的工具结果
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": result_content
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"发射工具结果失败: {e}")
|
||||
```
|
||||
|
||||
### 方案 2:消息历史注入
|
||||
|
||||
修改对话历史以包含工具调用:
|
||||
|
||||
```python
|
||||
# 工具执行后,追加到消息中
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments)
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": json.dumps(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 当前架构的挑战
|
||||
|
||||
### 1. 流式上下文
|
||||
|
||||
我们当前的实现使用:
|
||||
|
||||
- **基于队列的流式传输**:事件 → 队列 → 产出块
|
||||
- **仅文本块**:我们产出纯文本,而非结构化消息
|
||||
|
||||
OpenWebUI 的原生展示需要:
|
||||
|
||||
- **结构化消息事件**:不是文本块
|
||||
- **消息级别控制**:需要发射完整消息
|
||||
|
||||
### 2. 事件发射器兼容性
|
||||
|
||||
**当前用法:**
|
||||
|
||||
```python
|
||||
# 我们使用 event_emitter 发送状态/通知
|
||||
await event_emitter({
|
||||
"type": "status",
|
||||
"data": {"description": "处理中..."}
|
||||
})
|
||||
```
|
||||
|
||||
**工具调用所需:**
|
||||
|
||||
```python
|
||||
# 需要发射 message 类型事件
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"content": "..."
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**问题:** `__event_emitter__` 是否支持 `message` 类型事件?
|
||||
|
||||
### 3. Session SDK 事件 vs OpenWebUI 消息
|
||||
|
||||
**Copilot SDK 事件:**
|
||||
|
||||
- `tool.execution_start` → 获取工具名称、参数
|
||||
- `tool.execution_complete` → 获取工具结果
|
||||
- 为流式文本输出设计
|
||||
|
||||
**OpenWebUI 消息:**
|
||||
|
||||
- 期望结构化消息对象
|
||||
- 不为中间流注入设计
|
||||
|
||||
---
|
||||
|
||||
## 🧪 实验性实现
|
||||
|
||||
### 步骤 1:添加原生展示 Valve
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="使用 OpenWebUI 的原生工具调用展示,而非 Markdown 格式"
|
||||
)
|
||||
```
|
||||
|
||||
### 步骤 2:修改工具事件处理
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# ...现有代码...
|
||||
|
||||
def handler(event):
|
||||
event_type = get_event_type(event)
|
||||
|
||||
if event_type == "tool.execution_start":
|
||||
tool_name = safe_get_data_attr(event, "name")
|
||||
|
||||
# 获取工具参数
|
||||
tool_input = safe_get_data_attr(event, "input") or {}
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||
|
||||
if tool_call_id:
|
||||
active_tools[tool_call_id] = {
|
||||
"name": tool_name,
|
||||
"arguments": tool_input
|
||||
}
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# 发射结构化工具调用
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_start(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
tool_input
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 当前 Markdown 展示
|
||||
queue.put_nowait(f"\n\n> 🔧 **运行工具**: `{tool_name}`\n\n")
|
||||
|
||||
elif event_type == "tool.execution_complete":
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||
tool_info = active_tools.get(tool_call_id, {})
|
||||
tool_name = tool_info.get("name", "未知")
|
||||
|
||||
# 提取结果
|
||||
result_obj = safe_get_data_attr(event, "result")
|
||||
result_content = ""
|
||||
if hasattr(result_obj, "content"):
|
||||
result_content = result_obj.content
|
||||
elif isinstance(result_obj, dict):
|
||||
result_content = result_obj.get("content", "")
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# 发射结构化工具结果
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_result(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
result_content
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 当前 Markdown 展示
|
||||
queue.put_nowait(f"> ✅ **工具完成**: {result_content}\n\n")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 测试计划
|
||||
|
||||
### 测试 1:事件发射器消息类型支持
|
||||
|
||||
```python
|
||||
# 在测试对话中尝试:
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "测试消息"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**预期:** 消息出现在聊天中
|
||||
**如果失败:** 事件发射器不支持 message 类型
|
||||
|
||||
### 测试 2:工具调用消息格式
|
||||
|
||||
```python
|
||||
# 发送工具调用消息
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": "test_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"arguments": '{"param": "value"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
# 发送工具结果
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": "test_123",
|
||||
"name": "test_tool",
|
||||
"content": '{"result": "success"}'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**预期:** OpenWebUI 显示可折叠工具面板
|
||||
**如果失败:** 事件格式与 OpenWebUI 期望不符
|
||||
|
||||
### 测试 3:中间流工具调用注入
|
||||
|
||||
测试是否可以在流式传输期间注入工具调用消息:
|
||||
|
||||
```python
|
||||
# 开始流式文本
|
||||
yield "正在处理您的请求..."
|
||||
|
||||
# 中间流:发射工具调用
|
||||
await __event_emitter__({"type": "message", "data": {...}})
|
||||
|
||||
# 继续流式传输
|
||||
yield "完成!"
|
||||
```
|
||||
|
||||
**预期:** 工具面板出现在响应中间
|
||||
**风险:** 可能破坏流式传输流程
|
||||
|
||||
---
|
||||
|
||||
## 📋 实施检查清单
|
||||
|
||||
- [x] 添加 `REASONING_EFFORT` valve(已完成)
|
||||
- [ ] 添加 `USE_NATIVE_TOOL_DISPLAY` valve
|
||||
- [ ] 实现 `_emit_tool_call_start()` 辅助方法
|
||||
- [ ] 实现 `_emit_tool_call_result()` 辅助方法
|
||||
- [ ] 修改 `stream_response()` 中的工具事件处理
|
||||
- [ ] 测试事件发射器消息类型支持
|
||||
- [ ] 测试工具调用消息格式
|
||||
- [ ] 测试中间流注入
|
||||
- [ ] 更新文档
|
||||
- [ ] 添加用户配置指南
|
||||
|
||||
---
|
||||
|
||||
## 🤔 建议
|
||||
|
||||
### 混合方法(最安全)
|
||||
|
||||
保留两种展示模式:
|
||||
|
||||
1. **默认(当前):** 基于 Markdown 的展示
|
||||
- ✅ 与流式传输可靠工作
|
||||
- ✅ 无 OpenWebUI API 依赖
|
||||
- ✅ 跨版本一致
|
||||
|
||||
2. **实验性(原生):** 结构化工具消息
|
||||
- ✅ 更好的视觉集成
|
||||
- ⚠️ 需要测试 OpenWebUI 内部
|
||||
- ⚠️ 可能不适用于所有场景
|
||||
|
||||
**配置:**
|
||||
|
||||
```python
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="[实验性] 使用 OpenWebUI 的原生工具调用展示"
|
||||
)
|
||||
```
|
||||
|
||||
### 为什么 Markdown 展示目前更好
|
||||
|
||||
1. **可靠性:** 始终与流式传输兼容
|
||||
2. **灵活性:** 可以轻松自定义格式
|
||||
3. **上下文:** 与推理内联显示工具
|
||||
4. **兼容性:** 跨 OpenWebUI 版本工作
|
||||
|
||||
### 何时使用原生展示
|
||||
|
||||
- 非流式模式(更容易注入消息)
|
||||
- 确认事件发射器支持 message 类型后
|
||||
- 对于具有大型 JSON 结果的工具(更好的格式化)
|
||||
|
||||
---
|
||||
|
||||
## 📚 后续步骤
|
||||
|
||||
1. **研究 OpenWebUI 源代码**
|
||||
- 检查 `__event_emitter__` 实现
|
||||
- 验证支持的事件类型
|
||||
- 测试消息注入模式
|
||||
|
||||
2. **创建概念验证**
|
||||
- 简单测试插件
|
||||
- 发射工具调用消息
|
||||
- 验证 UI 渲染
|
||||
|
||||
3. **记录发现**
|
||||
- 使用测试结果更新本指南
|
||||
- 添加有效的代码示例
|
||||
- 如果成功,创建迁移指南
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||
- [OpenWebUI 插件开发](https://docs.openwebui.com/)
|
||||
- [Copilot SDK 事件](https://github.com/github/copilot-sdk)
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**状态:** 分析完成 - 实施等待测试
|
||||
@@ -0,0 +1,182 @@
|
||||
# Native Tool Display Usage Guide
|
||||
|
||||
## 🎨 What is Native Tool Display?
|
||||
|
||||
Native Tool Display is an experimental feature that integrates with OpenWebUI's built-in tool call visualization system. When enabled, tool calls and their results are displayed in **collapsible JSON panels** instead of plain markdown text.
|
||||
|
||||
### Visual Comparison
|
||||
|
||||
**Traditional Display (markdown):**
|
||||
|
||||
```
|
||||
> 🔧 Running Tool: `get_current_time`
|
||||
> ✅ Tool Completed: 2026-01-27 10:30:00
|
||||
```
|
||||
|
||||
**Native Display (collapsible panels):**
|
||||
|
||||
- Tool call appears in a collapsible `assistant.tool_calls` panel
|
||||
- Tool result appears in a separate collapsible `tool.content` panel
|
||||
- JSON syntax highlighting for better readability
|
||||
- Compact by default, expandable on click
|
||||
|
||||
## 🚀 How to Enable
|
||||
|
||||
1. Open the GitHub Copilot SDK Pipe configuration (Valves)
|
||||
2. Set `USE_NATIVE_TOOL_DISPLAY` to `true`
|
||||
3. Save the configuration
|
||||
4. Start a new conversation with tool calls
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- OpenWebUI with native tool display support
|
||||
- `__event_emitter__` must support `message` type events
|
||||
- Tool-enabled models (e.g., GPT-4, Claude Sonnet)
|
||||
|
||||
## ⚙️ How It Works
|
||||
|
||||
### OpenAI Standard Format
|
||||
|
||||
The native display uses the OpenAI standard message format:
|
||||
|
||||
**Tool Call (Assistant Message):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"arguments": "{\"timezone\":\"UTC\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool Result (Tool Message):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": "2026-01-27 10:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
### Message Flow
|
||||
|
||||
1. **Tool Execution Start**:
|
||||
- SDK emits `tool.execution_start` event
|
||||
- Plugin sends `assistant` message with `tool_calls` array
|
||||
- OpenWebUI displays collapsible tool call panel
|
||||
|
||||
2. **Tool Execution Complete**:
|
||||
- SDK emits `tool.execution_complete` event
|
||||
- Plugin sends `tool` message with `tool_call_id` and `content`
|
||||
- OpenWebUI displays collapsible result panel
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Panel Not Showing
|
||||
|
||||
**Symptoms:** Tool calls still appear as markdown text
|
||||
|
||||
**Possible Causes:**
|
||||
|
||||
1. `__event_emitter__` doesn't support `message` type events
|
||||
2. OpenWebUI version too old
|
||||
3. Feature not enabled (`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Enable DEBUG mode to see error messages in browser console
|
||||
- Check browser console for "Native message emission failed" warnings
|
||||
- Update OpenWebUI to latest version
|
||||
- Keep `USE_NATIVE_TOOL_DISPLAY = false` to use traditional markdown display
|
||||
|
||||
### Duplicate Tool Information
|
||||
|
||||
**Symptoms:** Tool calls appear in both native panels and markdown
|
||||
|
||||
**Cause:** Mixed display modes
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure `USE_NATIVE_TOOL_DISPLAY` is either `true` (native only) or `false` (markdown only)
|
||||
- Restart the conversation after changing this setting
|
||||
|
||||
## 🧪 Experimental Status
|
||||
|
||||
This feature is marked as **EXPERIMENTAL** because:
|
||||
|
||||
1. **Event Emitter API**: The `__event_emitter__` support for `message` type events is not fully documented
|
||||
2. **OpenWebUI Version Dependency**: Requires recent versions of OpenWebUI with native tool display support
|
||||
3. **Streaming Architecture**: May have compatibility issues with streaming responses
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
If native message emission fails:
|
||||
|
||||
- Plugin automatically falls back to markdown display
|
||||
- Error logged to browser console (when DEBUG is enabled)
|
||||
- No interruption to conversation flow
|
||||
|
||||
## 📊 Performance Considerations
|
||||
|
||||
Native display has slightly better performance characteristics:
|
||||
|
||||
| Aspect | Native Display | Markdown Display |
|
||||
|--------|----------------|------------------|
|
||||
| **Rendering** | Native UI components | Markdown parser |
|
||||
| **Interactivity** | Collapsible panels | Static text |
|
||||
| **JSON Parsing** | Handled by UI | Not formatted |
|
||||
| **Token Usage** | Minimal overhead | Formatting tokens |
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
Planned improvements for native tool display:
|
||||
|
||||
- [ ] Automatic fallback detection
|
||||
- [ ] Tool call history persistence
|
||||
- [ ] Rich metadata display (execution time, arguments preview)
|
||||
- [ ] Copy tool call JSON button
|
||||
- [ ] Tool call replay functionality
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Enable DEBUG First**: Test with DEBUG mode before using in production
|
||||
2. **Monitor Browser Console**: Check for warning messages during tool calls
|
||||
3. **Test with Simple Tools**: Verify with built-in tools before custom implementations
|
||||
4. **Keep Fallback Option**: Don't rely solely on native display until it exits experimental status
|
||||
|
||||
## 📖 Related Documentation
|
||||
|
||||
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - How to create and use custom tools
|
||||
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - Technical implementation details
|
||||
- [WORKFLOW.md](WORKFLOW.md) - Complete integration workflow
|
||||
|
||||
## 🐛 Reporting Issues
|
||||
|
||||
If you encounter issues with native tool display:
|
||||
|
||||
1. Enable `DEBUG` and `USE_NATIVE_TOOL_DISPLAY`
|
||||
2. Open browser console (F12)
|
||||
3. Trigger a tool call
|
||||
4. Copy any error messages
|
||||
5. Report to [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
Include:
|
||||
|
||||
- OpenWebUI version
|
||||
- Browser and version
|
||||
- Error messages from console
|
||||
- Steps to reproduce
|
||||
|
||||
---
|
||||
|
||||
**Author:** Fu-Jie | **Version:** 0.2.0 | **License:** MIT
|
||||
@@ -0,0 +1,182 @@
|
||||
# 原生工具显示使用指南
|
||||
|
||||
## 🎨 什么是原生工具显示?
|
||||
|
||||
原生工具显示是一项实验性功能,与 OpenWebUI 的内置工具调用可视化系统集成。启用后,工具调用及其结果将以**可折叠的 JSON 面板**显示,而不是纯文本 markdown。
|
||||
|
||||
### 视觉对比
|
||||
|
||||
**传统显示 (markdown):**
|
||||
|
||||
```
|
||||
> 🔧 正在运行工具: `get_current_time`
|
||||
> ✅ 工具已完成: 2026-01-27 10:30:00
|
||||
```
|
||||
|
||||
**原生显示 (可折叠面板):**
|
||||
|
||||
- 工具调用显示在可折叠的 `assistant.tool_calls` 面板中
|
||||
- 工具结果显示在单独的可折叠 `tool.content` 面板中
|
||||
- JSON 语法高亮,提高可读性
|
||||
- 默认折叠,点击即可展开
|
||||
|
||||
## 🚀 如何启用
|
||||
|
||||
1. 打开 GitHub Copilot SDK Pipe 配置 (Valves)
|
||||
2. 将 `USE_NATIVE_TOOL_DISPLAY` 设置为 `true`
|
||||
3. 保存配置
|
||||
4. 开始新的对话并使用工具调用
|
||||
|
||||
## 📋 要求
|
||||
|
||||
- 支持原生工具显示的 OpenWebUI
|
||||
- `__event_emitter__` 必须支持 `message` 类型事件
|
||||
- 支持工具的模型(例如 GPT-4、Claude Sonnet)
|
||||
|
||||
## ⚙️ 工作原理
|
||||
|
||||
### OpenAI 标准格式
|
||||
|
||||
原生显示使用 OpenAI 标准消息格式:
|
||||
|
||||
**工具调用(助手消息):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"arguments": "{\"timezone\":\"UTC\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**工具结果(工具消息):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": "2026-01-27 10:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
### 消息流程
|
||||
|
||||
1. **工具执行开始**:
|
||||
- SDK 发出 `tool.execution_start` 事件
|
||||
- 插件发送带有 `tool_calls` 数组的 `assistant` 消息
|
||||
- OpenWebUI 显示可折叠的工具调用面板
|
||||
|
||||
2. **工具执行完成**:
|
||||
- SDK 发出 `tool.execution_complete` 事件
|
||||
- 插件发送带有 `tool_call_id` 和 `content` 的 `tool` 消息
|
||||
- OpenWebUI 显示可折叠的结果面板
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 面板未显示
|
||||
|
||||
**症状:** 工具调用仍以 markdown 文本形式显示
|
||||
|
||||
**可能原因:**
|
||||
|
||||
1. `__event_emitter__` 不支持 `message` 类型事件
|
||||
2. OpenWebUI 版本过旧
|
||||
3. 功能未启用(`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 启用 DEBUG 模式查看浏览器控制台中的错误消息
|
||||
- 检查浏览器控制台的 "Native message emission failed" 警告
|
||||
- 更新 OpenWebUI 到最新版本
|
||||
- 保持 `USE_NATIVE_TOOL_DISPLAY = false` 使用传统 markdown 显示
|
||||
|
||||
### 重复的工具信息
|
||||
|
||||
**症状:** 工具调用同时出现在原生面板和 markdown 中
|
||||
|
||||
**原因:** 混合显示模式
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 确保 `USE_NATIVE_TOOL_DISPLAY` 为 `true`(仅原生)或 `false`(仅 markdown)
|
||||
- 更改设置后重启对话
|
||||
|
||||
## 🧪 实验性状态
|
||||
|
||||
此功能标记为**实验性**,因为:
|
||||
|
||||
1. **事件发射器 API**:`__event_emitter__` 对 `message` 类型事件的支持未完全文档化
|
||||
2. **OpenWebUI 版本依赖**:需要支持原生工具显示的较新 OpenWebUI 版本
|
||||
3. **流式架构**:可能与流式响应存在兼容性问题
|
||||
|
||||
### 回退行为
|
||||
|
||||
如果原生消息发送失败:
|
||||
|
||||
- 插件自动回退到 markdown 显示
|
||||
- 错误记录到浏览器控制台(启用 DEBUG 时)
|
||||
- 不会中断对话流程
|
||||
|
||||
## 📊 性能考虑
|
||||
|
||||
原生显示具有略好的性能特征:
|
||||
|
||||
| 方面 | 原生显示 | Markdown 显示 |
|
||||
|------|----------|---------------|
|
||||
| **渲染** | 原生 UI 组件 | Markdown 解析器 |
|
||||
| **交互性** | 可折叠面板 | 静态文本 |
|
||||
| **JSON 解析** | 由 UI 处理 | 未格式化 |
|
||||
| **Token 使用** | 最小开销 | 格式化 token |
|
||||
|
||||
## 🔮 未来增强
|
||||
|
||||
原生工具显示的计划改进:
|
||||
|
||||
- [ ] 自动回退检测
|
||||
- [ ] 工具调用历史持久化
|
||||
- [ ] 丰富的元数据显示(执行时间、参数预览)
|
||||
- [ ] 复制工具调用 JSON 按钮
|
||||
- [ ] 工具调用重放功能
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **先启用 DEBUG**:在生产环境使用前先在 DEBUG 模式下测试
|
||||
2. **监控浏览器控制台**:在工具调用期间检查警告消息
|
||||
3. **使用简单工具测试**:在自定义实现前先用内置工具验证
|
||||
4. **保留回退选项**:在退出实验性状态前不要完全依赖原生显示
|
||||
|
||||
## 📖 相关文档
|
||||
|
||||
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - 如何创建和使用自定义工具
|
||||
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - 技术实现细节
|
||||
- [WORKFLOW.md](WORKFLOW.md) - 完整集成工作流程
|
||||
|
||||
## 🐛 报告问题
|
||||
|
||||
如果您在使用原生工具显示时遇到问题:
|
||||
|
||||
1. 启用 `DEBUG` 和 `USE_NATIVE_TOOL_DISPLAY`
|
||||
2. 打开浏览器控制台(F12)
|
||||
3. 触发工具调用
|
||||
4. 复制任何错误消息
|
||||
5. 报告到 [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
包含:
|
||||
|
||||
- OpenWebUI 版本
|
||||
- 浏览器和版本
|
||||
- 控制台的错误消息
|
||||
- 复现步骤
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie | **版本:** 0.2.0 | **许可证:** MIT
|
||||
@@ -0,0 +1,509 @@
|
||||
# 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
|
||||
@@ -0,0 +1,708 @@
|
||||
# SessionConfig 完整功能集成指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档详细说明如何将 GitHub Copilot SDK 的 `SessionConfig` 所有功能集成到 OpenWebUI Pipe 中。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能清单与集成状态
|
||||
|
||||
| 功能 | 状态 | 优先级 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `session_id` | ✅ 已实现 | 高 | 使用 OpenWebUI chat_id |
|
||||
| `model` | ✅ 已实现 | 高 | 从 body 动态获取 |
|
||||
| `tools` | ✅ 已实现 | 高 | v0.2.0 新增示例工具 |
|
||||
| `streaming` | ✅ 已实现 | 高 | 支持流式输出 |
|
||||
| `infinite_sessions` | ✅ 已实现 | 高 | 自动上下文压缩 |
|
||||
| `system_message` | ⚠️ 部分支持 | 中 | 可通过 Valves 添加 |
|
||||
| `available_tools` | ⚠️ 部分支持 | 中 | 已有 AVAILABLE_TOOLS |
|
||||
| `excluded_tools` | 🔲 未实现 | 低 | 可添加到 Valves |
|
||||
| `on_permission_request` | 🔲 未实现 | 中 | 需要 UI 交互支持 |
|
||||
| `provider` (BYOK) | 🔲 未实现 | 低 | 高级功能 |
|
||||
| `mcp_servers` | 🔲 未实现 | 低 | MCP 协议支持 |
|
||||
| `custom_agents` | 🔲 未实现 | 低 | 自定义代理 |
|
||||
| `config_dir` | 🔲 未实现 | 低 | 可通过 WORKSPACE_DIR |
|
||||
| `skill_directories` | 🔲 未实现 | 低 | 技能系统 |
|
||||
| `disabled_skills` | 🔲 未实现 | 低 | 技能过滤 |
|
||||
|
||||
---
|
||||
|
||||
## 📖 详细集成方案
|
||||
|
||||
### 1. ✅ session_id(已实现)
|
||||
|
||||
**功能:** 持久化会话 ID
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id if chat_id else None, # 使用 OpenWebUI 的 chat_id
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
- OpenWebUI 的 `chat_id` 直接映射为 Copilot 的 `session_id`
|
||||
- 会话状态持久化到磁盘
|
||||
- 支持跨重启恢复对话
|
||||
|
||||
---
|
||||
|
||||
### 2. ✅ model(已实现)
|
||||
|
||||
**功能:** 选择 Copilot 模型
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
# 从用户选择的模型中提取
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):]
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
MODEL_ID: str = Field(
|
||||
default="claude-sonnet-4.5",
|
||||
description="默认模型(动态获取失败时使用)"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ✅ tools(已实现 - v0.2.0)
|
||||
|
||||
**功能:** 自定义工具/函数调用
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
custom_tools = self._initialize_custom_tools()
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
ENABLE_TOOLS: bool = Field(default=False)
|
||||
AVAILABLE_TOOLS: str = Field(default="all")
|
||||
```
|
||||
|
||||
**内置示例工具:**
|
||||
|
||||
- `get_current_time` - 获取当前时间
|
||||
- `calculate` - 数学计算
|
||||
- `generate_random_number` - 随机数生成
|
||||
|
||||
**扩展方法:** 参考 [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
|
||||
---
|
||||
|
||||
### 4. ⚠️ system_message(部分支持)
|
||||
|
||||
**功能:** 自定义系统提示词
|
||||
|
||||
**集成方案:**
|
||||
|
||||
#### 方案 A:通过 Valves 添加(推荐)
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SYSTEM_MESSAGE: str = Field(
|
||||
default="",
|
||||
description="Custom system message (append mode)"
|
||||
)
|
||||
SYSTEM_MESSAGE_MODE: str = Field(
|
||||
default="append",
|
||||
description="System message mode: 'append' or 'replace'"
|
||||
)
|
||||
```
|
||||
|
||||
**实现:**
|
||||
|
||||
```python
|
||||
async def pipe(self, body, ...):
|
||||
system_message_config = None
|
||||
|
||||
if self.valves.SYSTEM_MESSAGE:
|
||||
if self.valves.SYSTEM_MESSAGE_MODE == "replace":
|
||||
system_message_config = {
|
||||
"mode": "replace",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
else:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:从 OpenWebUI 系统提示词读取
|
||||
|
||||
```python
|
||||
# 从 body 中获取系统提示词
|
||||
system_prompt = body.get("system", "")
|
||||
if system_prompt:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": system_prompt
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- `append` 模式:在默认系统提示词后追加
|
||||
- `replace` 模式:完全替换(移除 SDK 安全保护)
|
||||
|
||||
---
|
||||
|
||||
### 5. ⚠️ available_tools / excluded_tools
|
||||
|
||||
**功能:** 工具白名单/黑名单
|
||||
|
||||
**当前部分支持:**
|
||||
|
||||
```python
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="'all' or comma-separated list"
|
||||
)
|
||||
```
|
||||
|
||||
**增强实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="Available tools (comma-separated or 'all')"
|
||||
)
|
||||
EXCLUDED_TOOLS: str = Field(
|
||||
default="",
|
||||
description="Excluded tools (comma-separated)"
|
||||
)
|
||||
```
|
||||
|
||||
**应用到 SessionConfig:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
available_tools=self._parse_tool_list(self.valves.AVAILABLE_TOOLS),
|
||||
excluded_tools=self._parse_tool_list(self.valves.EXCLUDED_TOOLS),
|
||||
...
|
||||
)
|
||||
|
||||
def _parse_tool_list(self, value: str) -> list[str]:
|
||||
"""解析工具列表"""
|
||||
if not value or value == "all":
|
||||
return []
|
||||
return [t.strip() for t in value.split(",") if t.strip()]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 🔲 on_permission_request(未实现)
|
||||
|
||||
**功能:** 处理权限请求(shell 命令、文件写入等)
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- Copilot 需要执行 shell 命令
|
||||
- 需要写入文件
|
||||
- 需要访问 URL
|
||||
|
||||
**集成挑战:**
|
||||
|
||||
- 需要 OpenWebUI 前端支持实时权限弹窗
|
||||
- 需要异步处理用户确认
|
||||
|
||||
**推荐方案:**
|
||||
|
||||
#### 方案 A:自动批准(开发/测试环境)
|
||||
|
||||
```python
|
||||
async def auto_approve_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""自动批准所有权限请求(危险!)"""
|
||||
return {
|
||||
"kind": "approved",
|
||||
"rules": []
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=auto_approve_permission_handler,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:基于规则的批准
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
async def rule_based_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
kind = request.get("kind")
|
||||
|
||||
if kind == "shell" and not self.valves.ALLOW_SHELL_COMMANDS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "write" and not self.valves.ALLOW_FILE_WRITE:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "url" and not self.valves.ALLOW_URL_ACCESS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
return {"kind": "approved", "rules": []}
|
||||
```
|
||||
|
||||
#### 方案 C:通过 Event Emitter 请求用户确认(理想)
|
||||
|
||||
```python
|
||||
async def interactive_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""通过前端请求用户确认"""
|
||||
if not __event_emitter__:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
|
||||
# 发送权限请求到前端
|
||||
response_queue = asyncio.Queue()
|
||||
await __event_emitter__({
|
||||
"type": "permission_request",
|
||||
"data": {
|
||||
"kind": request.get("kind"),
|
||||
"description": request.get("description"),
|
||||
"response_queue": response_queue
|
||||
}
|
||||
})
|
||||
|
||||
# 等待用户响应(带超时)
|
||||
try:
|
||||
user_response = await asyncio.wait_for(
|
||||
response_queue.get(),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if user_response.get("approved"):
|
||||
return {"kind": "approved", "rules": []}
|
||||
else:
|
||||
return {"kind": "denied-interactively-by-user"}
|
||||
except asyncio.TimeoutError:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 🔲 provider(BYOK - Bring Your Own Key)
|
||||
|
||||
**功能:** 使用自己的 API 密钥连接 OpenAI/Azure/Anthropic
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 不使用 GitHub Copilot 配额
|
||||
- 直接连接云服务提供商
|
||||
- 使用 Azure OpenAI 部署
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(
|
||||
default="openai",
|
||||
description="Provider type: openai, azure, anthropic"
|
||||
)
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
PROVIDER_BEARER_TOKEN: str = Field(default="")
|
||||
AZURE_API_VERSION: str = Field(default="2024-10-21")
|
||||
|
||||
def _build_provider_config(self) -> dict | None:
|
||||
"""构建 Provider 配置"""
|
||||
if not self.valves.USE_CUSTOM_PROVIDER:
|
||||
return None
|
||||
|
||||
config = {
|
||||
"type": self.valves.PROVIDER_TYPE,
|
||||
"base_url": self.valves.PROVIDER_BASE_URL,
|
||||
}
|
||||
|
||||
if self.valves.PROVIDER_API_KEY:
|
||||
config["api_key"] = self.valves.PROVIDER_API_KEY
|
||||
|
||||
if self.valves.PROVIDER_BEARER_TOKEN:
|
||||
config["bearer_token"] = self.valves.PROVIDER_BEARER_TOKEN
|
||||
|
||||
if self.valves.PROVIDER_TYPE == "azure":
|
||||
config["azure"] = {
|
||||
"api_version": self.valves.AZURE_API_VERSION
|
||||
}
|
||||
|
||||
# 自动推断 wire_api
|
||||
if self.valves.PROVIDER_TYPE == "anthropic":
|
||||
config["wire_api"] = "responses"
|
||||
else:
|
||||
config["wire_api"] = "completions"
|
||||
|
||||
return config
|
||||
```
|
||||
|
||||
**应用:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. ✅ streaming(已实现)
|
||||
|
||||
**功能:** 流式输出
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
streaming=body.get("stream", False),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 🔲 mcp_servers(MCP 协议)
|
||||
|
||||
**功能:** Model Context Protocol 服务器集成
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 连接外部数据源(数据库、API)
|
||||
- 集成第三方服务
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
MCP_SERVERS_CONFIG: str = Field(
|
||||
default="{}",
|
||||
description="MCP servers configuration (JSON format)"
|
||||
)
|
||||
|
||||
def _parse_mcp_servers(self) -> dict | None:
|
||||
"""解析 MCP 服务器配置"""
|
||||
if not self.valves.MCP_SERVERS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.MCP_SERVERS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"type": "local",
|
||||
"command": "mcp-server-sqlite",
|
||||
"args": ["--db", "/path/to/db.sqlite"],
|
||||
"tools": ["*"]
|
||||
},
|
||||
"weather": {
|
||||
"type": "http",
|
||||
"url": "https://weather-api.example.com/mcp",
|
||||
"tools": ["get_weather", "get_forecast"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. 🔲 custom_agents
|
||||
|
||||
**功能:** 自定义 AI 代理
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 专门化的子代理(如代码审查、文档编写)
|
||||
- 不同的提示词策略
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CUSTOM_AGENTS_CONFIG: str = Field(
|
||||
default="[]",
|
||||
description="Custom agents configuration (JSON array)"
|
||||
)
|
||||
|
||||
def _parse_custom_agents(self) -> list | None:
|
||||
"""解析自定义代理配置"""
|
||||
if not self.valves.CUSTOM_AGENTS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.CUSTOM_AGENTS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "code_reviewer",
|
||||
"display_name": "Code Reviewer",
|
||||
"description": "Reviews code for best practices",
|
||||
"prompt": "You are an expert code reviewer. Focus on security, performance, and maintainability.",
|
||||
"tools": ["read_file", "write_file"],
|
||||
"infer": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. 🔲 config_dir
|
||||
|
||||
**功能:** 自定义配置目录
|
||||
|
||||
**当前支持:**
|
||||
|
||||
- 已有 `WORKSPACE_DIR` 控制工作目录
|
||||
|
||||
**增强方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CONFIG_DIR: str = Field(
|
||||
default="",
|
||||
description="Custom config directory for session state"
|
||||
)
|
||||
|
||||
session_config = SessionConfig(
|
||||
config_dir=self.valves.CONFIG_DIR if self.valves.CONFIG_DIR else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12. 🔲 skill_directories / disabled_skills
|
||||
|
||||
**功能:** Copilot Skills 系统
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 加载自定义技能包
|
||||
- 禁用特定技能
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SKILL_DIRECTORIES: str = Field(
|
||||
default="",
|
||||
description="Comma-separated skill directories"
|
||||
)
|
||||
DISABLED_SKILLS: str = Field(
|
||||
default="",
|
||||
description="Comma-separated disabled skills"
|
||||
)
|
||||
|
||||
def _parse_skills_config(self):
|
||||
"""解析技能配置"""
|
||||
skill_dirs = []
|
||||
if self.valves.SKILL_DIRECTORIES:
|
||||
skill_dirs = [
|
||||
d.strip()
|
||||
for d in self.valves.SKILL_DIRECTORIES.split(",")
|
||||
if d.strip()
|
||||
]
|
||||
|
||||
disabled = []
|
||||
if self.valves.DISABLED_SKILLS:
|
||||
disabled = [
|
||||
s.strip()
|
||||
for s in self.valves.DISABLED_SKILLS.split(",")
|
||||
if s.strip()
|
||||
]
|
||||
|
||||
return skill_dirs, disabled
|
||||
|
||||
# 应用
|
||||
skill_dirs, disabled_skills = self._parse_skills_config()
|
||||
session_config = SessionConfig(
|
||||
skill_directories=skill_dirs if skill_dirs else None,
|
||||
disabled_skills=disabled_skills if disabled_skills else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13. ✅ infinite_sessions(已实现)
|
||||
|
||||
**功能:** 无限会话与自动上下文压缩
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
INFINITE_SESSION: bool = Field(default=True)
|
||||
COMPACTION_THRESHOLD: float = Field(default=0.8)
|
||||
BUFFER_THRESHOLD: float = Field(default=0.95)
|
||||
|
||||
infinite_session_config = None
|
||||
if self.valves.INFINITE_SESSION:
|
||||
infinite_session_config = {
|
||||
"enabled": True,
|
||||
"background_compaction_threshold": self.valves.COMPACTION_THRESHOLD,
|
||||
"buffer_exhaustion_threshold": self.valves.BUFFER_THRESHOLD,
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
infinite_sessions=infinite_session_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 实施优先级建议
|
||||
|
||||
### 🔥 高优先级(立即实现)
|
||||
|
||||
1. **system_message** - 用户最常需要的功能
|
||||
2. **on_permission_request (基于规则)** - 安全性需求
|
||||
|
||||
### 📌 中优先级(下一阶段)
|
||||
|
||||
3. **excluded_tools** - 完善工具管理
|
||||
4. **provider (BYOK)** - 高级用户需求
|
||||
5. **config_dir** - 增强会话管理
|
||||
|
||||
### 📋 低优先级(可选)
|
||||
|
||||
6. **mcp_servers** - 高级集成
|
||||
7. **custom_agents** - 专业化功能
|
||||
8. **skill_directories** - 生态系统功能
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速实施计划
|
||||
|
||||
### Phase 1: 基础增强(1-2小时)
|
||||
|
||||
```python
|
||||
# 添加到 Valves
|
||||
SYSTEM_MESSAGE: str = Field(default="")
|
||||
SYSTEM_MESSAGE_MODE: str = Field(default="append")
|
||||
EXCLUDED_TOOLS: str = Field(default="")
|
||||
|
||||
# 添加到 pipe() 方法
|
||||
system_message_config = self._build_system_message_config()
|
||||
excluded_tools = self._parse_tool_list(self.valves.EXCLUDED_TOOLS)
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
excluded_tools=excluded_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 2: 权限管理(2-3小时)
|
||||
|
||||
```python
|
||||
# 添加权限控制 Valves
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
# 实现权限处理器
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=self._create_permission_handler(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 3: BYOK 支持(3-4小时)
|
||||
|
||||
```python
|
||||
# 添加 Provider Valves
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(default="openai")
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
|
||||
# 实现 Provider 配置
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- **SDK 类型定义**: `/opt/homebrew/.../copilot/types.py`
|
||||
- **工具系统**: [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
- **SDK 文档**: <https://github.com/github/copilot-sdk>
|
||||
|
||||
---
|
||||
|
||||
## ✅ 实施检查清单
|
||||
|
||||
使用此清单跟踪实施进度:
|
||||
|
||||
- [x] session_id
|
||||
- [x] model
|
||||
- [x] tools
|
||||
- [x] streaming
|
||||
- [x] infinite_sessions
|
||||
- [ ] system_message
|
||||
- [ ] available_tools (完善)
|
||||
- [ ] excluded_tools
|
||||
- [ ] on_permission_request
|
||||
- [ ] provider (BYOK)
|
||||
- [ ] mcp_servers
|
||||
- [ ] custom_agents
|
||||
- [ ] config_dir
|
||||
- [ ] skill_directories
|
||||
- [ ] disabled_skills
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** v1.0
|
||||
**日期:** 2026-01-26
|
||||
**更新:** 随功能实施持续更新
|
||||
191
plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md
Normal file
191
plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 🛠️ Custom Tools Usage / 自定义工具使用指南
|
||||
|
||||
## Overview / 概览
|
||||
|
||||
This pipe includes **1 example custom tool** that demonstrates how to use GitHub Copilot SDK's tool calling feature.
|
||||
|
||||
本 Pipe 包含 **1 个示例自定义工具**,展示如何使用 GitHub Copilot SDK 的工具调用功能。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start / 快速开始
|
||||
|
||||
### 1. Enable Tools / 启用工具
|
||||
|
||||
In Valves configuration:
|
||||
在 Valves 配置中:
|
||||
|
||||
```
|
||||
ENABLE_TOOLS: true
|
||||
AVAILABLE_TOOLS: all
|
||||
```
|
||||
|
||||
### 2. Test with Conversations / 测试对话
|
||||
|
||||
Try these examples:
|
||||
尝试这些示例:
|
||||
|
||||
**English:**
|
||||
|
||||
- "Give me a random number between 1 and 100"
|
||||
|
||||
**中文:**
|
||||
|
||||
- "给我一个 1 到 100 之间的随机数"
|
||||
|
||||
---
|
||||
|
||||
## 📦 Included Tools / 内置工具
|
||||
|
||||
### 1. `generate_random_number` / 生成随机数
|
||||
|
||||
**Description:** Generate a random integer
|
||||
**描述:** 生成随机整数
|
||||
|
||||
**Parameters / 参数:**
|
||||
|
||||
- `min` (optional): Minimum value (default: 1)
|
||||
- `max` (optional): Maximum value (default: 100)
|
||||
- `min` (可选): 最小值 (默认: 1)
|
||||
- `max` (可选): 最大值 (默认: 100)
|
||||
|
||||
**Example / 示例:**
|
||||
|
||||
```
|
||||
User: "Give me a random number between 1 and 10"
|
||||
Copilot: [calls generate_random_number with min=1, max=10] "Generated random number: 7"
|
||||
|
||||
用户: "给我一个 1 到 10 之间的随机数"
|
||||
Copilot: [调用 generate_random_number,参数 min=1, max=10] "生成的随机数: 7"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Advanced Configuration / 高级配置
|
||||
|
||||
### Select Specific Tools / 选择特定工具
|
||||
|
||||
Instead of enabling all tools, specify which ones to use:
|
||||
不启用所有工具,而是指定要使用的工具:
|
||||
|
||||
```
|
||||
ENABLE_TOOLS: true
|
||||
AVAILABLE_TOOLS: generate_random_number
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How Tool Calling Works / 工具调用的工作原理
|
||||
|
||||
```
|
||||
1. User asks a question / 用户提问
|
||||
↓
|
||||
2. Copilot decides if it needs a tool / Copilot 决定是否需要工具
|
||||
↓
|
||||
3. If yes, Copilot calls the appropriate tool / 如果需要,调用相应工具
|
||||
↓
|
||||
4. Tool executes and returns result / 工具执行并返回结果
|
||||
↓
|
||||
5. Copilot uses the result to answer / Copilot 使用结果回答
|
||||
```
|
||||
|
||||
### Visual Feedback / 可视化反馈
|
||||
|
||||
When tools are called, you'll see:
|
||||
当工具被调用时,你会看到:
|
||||
|
||||
```
|
||||
🔧 **Calling tool**: `generate_random_number`
|
||||
✅ **Tool `generate_random_number` completed**
|
||||
|
||||
Generated random number: 7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Creating Your Own Tools / 创建自定义工具
|
||||
|
||||
Want to add your own tools? Follow this pattern (module-level tools):
|
||||
想要添加自己的工具?遵循这个模式(模块级工具):
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from copilot import define_tool
|
||||
|
||||
class MyToolParams(BaseModel):
|
||||
param_name: str = Field(description="Parameter description")
|
||||
|
||||
|
||||
@define_tool(description="Clear description of what the tool does and when to use it")
|
||||
async def my_tool(params: MyToolParams) -> str:
|
||||
# Do something
|
||||
result = do_something(params.param_name)
|
||||
return f"Result: {result}"
|
||||
```
|
||||
|
||||
Then register it in `_initialize_custom_tools()`:
|
||||
然后将它添加到 `_initialize_custom_tools()`:
|
||||
|
||||
```python
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
"my_tool": my_tool, # ✅ Add here
|
||||
}
|
||||
|
||||
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]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Notes / 重要说明
|
||||
|
||||
### Security / 安全性
|
||||
|
||||
- Tools run in the same process as the pipe
|
||||
- Be careful with tools that execute code or access files
|
||||
- Always validate input parameters
|
||||
|
||||
- 工具在与 Pipe 相同的进程中运行
|
||||
- 谨慎处理执行代码或访问文件的工具
|
||||
- 始终验证输入参数
|
||||
|
||||
### Performance / 性能
|
||||
|
||||
- Tool execution is synchronous during streaming
|
||||
- Long-running tools may cause delays
|
||||
- Consider adding timeouts for external API calls
|
||||
|
||||
- 工具执行在流式传输期间是同步的
|
||||
- 长时间运行的工具可能导致延迟
|
||||
- 考虑为外部 API 调用添加超时
|
||||
|
||||
### Debugging / 调试
|
||||
|
||||
- Enable `DEBUG: true` to see tool events in the browser console
|
||||
- Check tool calls in `🔧 Calling tool` messages
|
||||
- Tool errors are displayed in the response
|
||||
|
||||
- 启用 `DEBUG: true` 在浏览器控制台查看工具事件
|
||||
- 在 `🔧 Calling tool` 消息中检查工具调用
|
||||
- 工具错误会显示在响应中
|
||||
|
||||
---
|
||||
|
||||
## 📖 References / 参考资料
|
||||
|
||||
- [Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||
- [COPILOT_TOOLS_QUICKSTART.md](COPILOT_TOOLS_QUICKSTART.md) - Detailed implementation guide
|
||||
- [JSON Schema](https://json-schema.org/) - For parameter definitions
|
||||
|
||||
---
|
||||
|
||||
**Version:** 0.2.3
|
||||
**Last Updated:** 2026-01-27
|
||||
@@ -0,0 +1,431 @@
|
||||
# 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
|
||||
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW.md
Normal file
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW.md
Normal file
@@ -0,0 +1,835 @@
|
||||
# GitHub Copilot SDK Integration Workflow
|
||||
|
||||
**Author:** Fu-Jie
|
||||
**Version:** 0.2.3
|
||||
**Last Updated:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Request Processing Flow](#request-processing-flow)
|
||||
3. [Session Management](#session-management)
|
||||
4. [Streaming Response Handling](#streaming-response-handling)
|
||||
5. [Event Processing Mechanism](#event-processing-mechanism)
|
||||
6. [Tool Execution Flow](#tool-execution-flow)
|
||||
7. [System Prompt Extraction](#system-prompt-extraction)
|
||||
8. [Configuration Parameters](#configuration-parameters)
|
||||
9. [Key Functions Reference](#key-functions-reference)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Component Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe Interface (Entry Point) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (Main Logic) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. Environment Setup (_setup_env) │ │ │
|
||||
│ │ │ 2. Model Selection (request_model parsing) │ │ │
|
||||
│ │ │ 3. Chat Context Extraction │ │ │
|
||||
│ │ │ 4. System Prompt Extraction │ │ │
|
||||
│ │ │ 5. Session Management (create/resume) │ │ │
|
||||
│ │ │ 6. Streaming/Non-streaming Response │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot Client │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK instance) │ │ │
|
||||
│ │ │ • Session (conversation context) │ │ │
|
||||
│ │ │ • Event Stream (async events) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI Process │
|
||||
│ (Backend Agent) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Pipe Interface**: OpenWebUI's standard entry point
|
||||
2. **Environment Manager**: CLI setup, token validation, environment variables
|
||||
3. **Session Manager**: Persistent conversation state with automatic compaction
|
||||
4. **Event Processor**: Asynchronous streaming event handler
|
||||
5. **Tool System**: Custom tool registration and execution
|
||||
6. **Debug Logger**: Frontend console logging for troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Request Processing Flow
|
||||
|
||||
### Complete Request Lifecycle
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI Request] --> B[pipe Entry Point]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{Setup Environment}
|
||||
D --> E[Parse Model ID]
|
||||
E --> F[Extract Chat Context]
|
||||
F --> G[Extract System Prompt]
|
||||
G --> H{Session Exists?}
|
||||
H -->|Yes| I[Resume Session]
|
||||
H -->|No| J[Create New Session]
|
||||
I --> K[Initialize Tools]
|
||||
J --> K
|
||||
K --> L[Process Images]
|
||||
L --> M{Streaming Mode?}
|
||||
M -->|Yes| N[stream_response]
|
||||
M -->|No| O[send_and_wait]
|
||||
N --> P[Async Event Stream]
|
||||
O --> Q[Direct Response]
|
||||
P --> R[Return to OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### Step-by-Step Breakdown
|
||||
|
||||
#### 1. Environment Setup (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
Priority:
|
||||
1. Check VALVES.CLI_PATH
|
||||
2. Search system PATH
|
||||
3. Auto-install via curl (if not found)
|
||||
4. Set GH_TOKEN environment variables
|
||||
"""
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Locate Copilot CLI binary
|
||||
- Set `COPILOT_CLI_PATH` environment variable
|
||||
- Configure `GH_TOKEN` for authentication
|
||||
- Apply custom environment variables
|
||||
|
||||
#### 2. Model Selection
|
||||
|
||||
```python
|
||||
# Input: body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. Chat Context Extraction (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# Priority order for chat_id:
|
||||
# 1. __metadata__ (most reliable)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. System Prompt Extraction (`_extract_system_prompt`)
|
||||
|
||||
Multi-source fallback strategy:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. Model database lookup (by model_id)
|
||||
3. `body.params.system`
|
||||
4. Messages with `role="system"`
|
||||
|
||||
#### 5. Session Creation/Resumption
|
||||
|
||||
**New Session:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**Resume Session:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# Session state preserved: history, tools, workspace
|
||||
except Exception:
|
||||
# Fallback to creating new session
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management
|
||||
|
||||
### Infinite Sessions Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Session Lifecycle │
|
||||
│ │
|
||||
│ ┌──────────┐ create ┌──────────┐ resume ┌───────┴───┐
|
||||
│ │ Chat ID │─────────▶ │ Session │ ◀────────│ OpenWebUI │
|
||||
│ └──────────┘ │ State │ └───────────┘
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Context Window Management │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Messages [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token Usage: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Threshold Reached (0.8) │ │ │
|
||||
│ │ │ → Background Compaction Triggered │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Compacted Summary + Recent Messages │ │ │
|
||||
│ │ │ Token Usage: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # Enable infinite sessions
|
||||
background_compaction_threshold=0.8, # Start compaction at 80% token usage
|
||||
buffer_exhaustion_threshold=0.95 # Emergency threshold at 95%
|
||||
)
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **< 80%**: Normal operation, no compaction
|
||||
- **80-95%**: Background compaction (summarize older messages)
|
||||
- **> 95%**: Force compaction before next request
|
||||
|
||||
---
|
||||
|
||||
## Streaming Response Handling
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
Asynchronous event processing with queue-based buffering.
|
||||
|
||||
Flow:
|
||||
1. Start async send task
|
||||
2. Register event handler
|
||||
3. Process events via queue
|
||||
4. Yield chunks to OpenWebUI
|
||||
5. Clean up resources
|
||||
"""
|
||||
```
|
||||
|
||||
### Event Processing Pipeline
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK Event Stream │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Event Handler │
|
||||
│ (Sync Callback) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Async Queue │
|
||||
│ (Thread-safe) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Consumer Loop │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield to OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### State Management During Streaming
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> tags opened
|
||||
"content_sent": False # Main content has started
|
||||
}
|
||||
active_tools = {} # Track concurrent tool executions
|
||||
```
|
||||
|
||||
**State Transitions:**
|
||||
|
||||
1. `reasoning_delta` arrives → `thinking_started = True` → Output: `<think>\n{reasoning}`
|
||||
2. `message_delta` arrives → Close `</think>` if open → `content_sent = True` → Output: `{content}`
|
||||
3. `tool.execution_start` → Output tool indicator (inside/outside `<think>`)
|
||||
4. `session.complete` → Finalize stream
|
||||
|
||||
---
|
||||
|
||||
## Event Processing Mechanism
|
||||
|
||||
### Event Type Reference
|
||||
|
||||
Following official SDK patterns (from `copilot.SessionEventType`):
|
||||
|
||||
| Event Type | Description | Key Data Fields | Handler Action |
|
||||
|-----------|-------------|-----------------|----------------|
|
||||
| `assistant.message_delta` | Main content streaming | `delta_content` | Yield text chunk |
|
||||
| `assistant.reasoning_delta` | Chain-of-thought | `delta_content` | Wrap in `<think>` tags |
|
||||
| `tool.execution_start` | Tool call initiated | `name`, `tool_call_id` | Display tool indicator |
|
||||
| `tool.execution_complete` | Tool finished | `result.content` | Show completion status |
|
||||
| `session.compaction_start` | Context compaction begins | - | Log debug info |
|
||||
| `session.compaction_complete` | Compaction done | - | Log debug info |
|
||||
| `session.error` | Error occurred | `error`, `message` | Emit error notification |
|
||||
|
||||
### Event Handler Implementation
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""Process streaming events following official SDK patterns."""
|
||||
event_type = get_event_type(event) # Handle enum/string types
|
||||
|
||||
# Extract data using safe_get_data_attr (handles dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # Thread-safe enqueue
|
||||
```
|
||||
|
||||
### Official SDK Pattern Compliance
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
Official pattern: event.data.delta_content
|
||||
Handles both dict and object access patterns.
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict access (JSON-like)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# Object attribute (Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution Flow
|
||||
|
||||
### Tool Registration
|
||||
|
||||
```python
|
||||
# 1. Define tool at module level
|
||||
@define_tool(description="Generate a random integer within a specified range.")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"Generated random number: {number}"
|
||||
|
||||
# 2. Register in _initialize_custom_tools
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# Filter based on AVAILABLE_TOOLS valve
|
||||
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]
|
||||
```
|
||||
|
||||
### Tool Execution Timeline
|
||||
|
||||
```
|
||||
User Message: "Generate a random number between 1 and 100"
|
||||
│
|
||||
▼
|
||||
Model Decision: Use tool `generate_random_number`
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_start
|
||||
│ → Display: "🔧 Running Tool: generate_random_number"
|
||||
▼
|
||||
Tool Function Execution (async)
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_complete
|
||||
│ → Result: "Generated random number: 42"
|
||||
│ → Display: "✅ Tool Completed: 42"
|
||||
▼
|
||||
Model generates response using tool result
|
||||
│
|
||||
▼
|
||||
Event: assistant.message_delta
|
||||
│ → "I generated the number 42 for you."
|
||||
▼
|
||||
Stream Complete
|
||||
```
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
**Before Content:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
Running Tool: generate_random_number...
|
||||
Tool `generate_random_number` Completed. Result: 42
|
||||
</think>
|
||||
|
||||
I generated the number 42 for you.
|
||||
```
|
||||
|
||||
**After Content Started:**
|
||||
|
||||
```markdown
|
||||
The number is
|
||||
|
||||
> 🔧 **Running Tool**: `generate_random_number`
|
||||
|
||||
> ✅ **Tool Completed**: 42
|
||||
|
||||
actually 42.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Prompt Extraction
|
||||
|
||||
### Multi-Source Priority System
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
Priority order:
|
||||
1. metadata.model.params.system (highest)
|
||||
2. Model database lookup
|
||||
3. body.params.system
|
||||
4. messages[role="system"] (fallback)
|
||||
"""
|
||||
```
|
||||
|
||||
### Source 1: Metadata Model Params
|
||||
|
||||
```python
|
||||
# OpenWebUI injects model configuration
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # Priority 1
|
||||
```
|
||||
|
||||
### Source 2: Model Database
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# Try multiple model ID variations
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # From valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### Source 3: Body Params
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### Source 4: System Message
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### Configuration in SessionConfig
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # Append to conversation context
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... other params
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### Valve Definitions
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub Fine-grained Token (requires 'Copilot Requests' permission) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | Default model when dynamic fetching fails |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Path to Copilot CLI binary |
|
||||
| `DEBUG` | bool | `False` | Enable frontend console debug logging |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI log level: none, error, warning, info, debug, all |
|
||||
| `SHOW_THINKING` | bool | `True` | Display model reasoning in `<think>` tags |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | Show session workspace path in debug mode |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | Comma-separated keywords to exclude models |
|
||||
| `WORKSPACE_DIR` | str | `""` | Restricted workspace directory (empty = process cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | Enable automatic context compaction |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | Background compaction at 80% token usage |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | Emergency threshold at 95% |
|
||||
| `TIMEOUT` | int | `300` | Stream chunk timeout (seconds) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | JSON string of custom environment variables |
|
||||
| `ENABLE_TOOLS` | bool | `False` | Enable custom tool system |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | Available tools: "all" or comma-separated list |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Set by _setup_env
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Custom variables (from CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Functions Reference
|
||||
|
||||
### Entry Points
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: OpenWebUI stable entry point
|
||||
- **Returns**: Delegates to `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: Main request processing logic
|
||||
- **Flow**: Setup → Extract → Session → Response
|
||||
- **Returns**: `str` (non-streaming) or `AsyncGenerator` (streaming)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **Purpose**: Dynamic model list fetching
|
||||
- **Returns**: List of available models with multiplier info
|
||||
- **Caching**: Uses `_model_cache` to avoid repeated API calls
|
||||
|
||||
### Session Management
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **Purpose**: Construct SessionConfig object
|
||||
- **Returns**: `SessionConfig` with infinite sessions and tools
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract chat_id with priority fallback
|
||||
- **Returns**: `{"chat_id": str}`
|
||||
|
||||
### Streaming
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **Purpose**: Async streaming event processor
|
||||
- **Yields**: Text chunks to OpenWebUI
|
||||
- **Resources**: Auto-cleanup client and session
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **Purpose**: Sync event callback (inside `stream_response`)
|
||||
- **Action**: Parse event → Enqueue chunks → Update state
|
||||
|
||||
### Helpers
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **Purpose**: Send debug logs to frontend console
|
||||
- **Condition**: Only when `DEBUG=True`
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **Purpose**: Locate CLI, set environment variables
|
||||
- **Side Effects**: Modifies `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **Purpose**: Multi-source system prompt extraction
|
||||
- **Returns**: `(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract text and images from multimodal messages
|
||||
- **Returns**: `(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **Purpose**: Register and filter custom tools
|
||||
- **Returns**: List of tool functions
|
||||
|
||||
### Utility Functions
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **Purpose**: Extract event type string from enum/string
|
||||
- **Handles**: `SessionEventType` enum → `.value` extraction
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **Purpose**: Safe attribute extraction from event.data
|
||||
- **Handles**: Both dict access and object attribute access
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```python
|
||||
# In OpenWebUI Valves UI:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### Debug Output Location
|
||||
|
||||
**Frontend Console:**
|
||||
|
||||
```javascript
|
||||
// Open browser DevTools (F12)
|
||||
// Look for logs with prefix: [Copilot Pipe]
|
||||
console.debug("[Copilot Pipe] Extracted ChatID: abc123 (Source: __metadata__)")
|
||||
```
|
||||
|
||||
**Backend Logs:**
|
||||
|
||||
```python
|
||||
# Python logging output
|
||||
logger.debug(f"[Copilot Pipe] Session resumed: {chat_id}")
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Session Not Resuming
|
||||
|
||||
**Symptom:** New session created every request
|
||||
**Causes:**
|
||||
|
||||
- `chat_id` not extracted correctly
|
||||
- Session expired on Copilot side
|
||||
- `INFINITE_SESSION=False` (sessions not persistent)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted ChatID: <id> (Source: ...)"
|
||||
"Session <id> not found (...), creating new."
|
||||
```
|
||||
|
||||
#### 2. System Prompt Not Applied
|
||||
|
||||
**Symptom:** Model ignores configured system prompt
|
||||
**Causes:**
|
||||
|
||||
- Not found in any of 4 sources
|
||||
- Session resumed (system prompt only set on creation)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted system prompt from <source> (length: X)"
|
||||
"Configured system message (mode: append)"
|
||||
```
|
||||
|
||||
#### 3. Tools Not Available
|
||||
|
||||
**Symptom:** Model can't use custom tools
|
||||
**Causes:**
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- Tool not registered in `_initialize_custom_tools`
|
||||
- Wrong `AVAILABLE_TOOLS` filter
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Enabled X custom tools: ['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Model List Caching
|
||||
|
||||
```python
|
||||
# First request: Fetch from API
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # Cache result
|
||||
|
||||
# Subsequent requests: Use cache
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### Session Persistence
|
||||
|
||||
**Impact:** Eliminates redundant model initialization on every request
|
||||
|
||||
```python
|
||||
# Without session:
|
||||
# Each request: Initialize model → Load context → Generate → Discard
|
||||
|
||||
# With session (chat_id):
|
||||
# First request: Initialize model → Load context → Generate → Save
|
||||
# Later: Resume → Generate (instant)
|
||||
```
|
||||
|
||||
### Streaming vs Non-streaming
|
||||
|
||||
**Streaming:**
|
||||
|
||||
- Lower perceived latency (first token faster)
|
||||
- Better UX for long responses
|
||||
- Resource cleanup via generator exit
|
||||
|
||||
**Non-streaming:**
|
||||
|
||||
- Simpler error handling
|
||||
- Atomic response (no partial output)
|
||||
- Use for short responses
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Protection
|
||||
|
||||
```python
|
||||
# ❌ Never log tokens
|
||||
logger.debug(f"Token: {self.valves.GH_TOKEN}") # DON'T DO THIS
|
||||
|
||||
# ✅ Mask sensitive data
|
||||
logger.debug(f"Token configured: {'*' * 10}")
|
||||
```
|
||||
|
||||
### Workspace Isolation
|
||||
|
||||
```python
|
||||
# Set WORKSPACE_DIR to restrict file access
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI respects this directory
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```python
|
||||
# Validate chat_id format
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"Invalid chat_id format: {chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **Multi-Session Management**: Support multiple parallel sessions per user
|
||||
2. **Session Analytics**: Track token usage, compaction frequency
|
||||
3. **Tool Result Caching**: Avoid redundant tool calls
|
||||
4. **Custom Event Filters**: User-configurable event handling
|
||||
5. **Workspace Templates**: Pre-configured workspace environments
|
||||
6. **Streaming Abort**: Graceful cancellation of long-running requests
|
||||
|
||||
### API Evolution
|
||||
|
||||
Monitoring Copilot SDK updates for:
|
||||
|
||||
- New event types (e.g., `assistant.function_call`)
|
||||
- Enhanced tool capabilities
|
||||
- Improved session serialization
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe Development](https://docs.openwebui.com/)
|
||||
- [Awesome OpenWebUI Project](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
|
||||
---
|
||||
|
||||
**License:** MIT
|
||||
**Maintainer:** Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md
Normal file
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md
Normal file
@@ -0,0 +1,835 @@
|
||||
# GitHub Copilot SDK 集成工作流程
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** 0.2.3
|
||||
**最后更新:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构概览](#架构概览)
|
||||
2. [请求处理流程](#请求处理流程)
|
||||
3. [会话管理](#会话管理)
|
||||
4. [流式响应处理](#流式响应处理)
|
||||
5. [事件处理机制](#事件处理机制)
|
||||
6. [工具执行流程](#工具执行流程)
|
||||
7. [系统提示词提取](#系统提示词提取)
|
||||
8. [配置参数](#配置参数)
|
||||
9. [核心函数参考](#核心函数参考)
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
### 组件图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe 接口 (入口点) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (主逻辑) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. 环境设置 (_setup_env) │ │ │
|
||||
│ │ │ 2. 模型选择 (request_model 解析) │ │ │
|
||||
│ │ │ 3. 聊天上下文提取 │ │ │
|
||||
│ │ │ 4. 系统提示词提取 │ │ │
|
||||
│ │ │ 5. 会话管理 (创建/恢复) │ │ │
|
||||
│ │ │ 6. 流式/非流式响应 │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot 客户端 │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK 实例) │ │ │
|
||||
│ │ │ • Session (对话上下文) │ │ │
|
||||
│ │ │ • Event Stream (异步事件流) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI 进程 │
|
||||
│ (后端代理) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **Pipe 接口**:OpenWebUI 的标准入口点
|
||||
2. **环境管理器**:CLI 设置、令牌验证、环境变量
|
||||
3. **会话管理器**:持久化对话状态,自动压缩
|
||||
4. **事件处理器**:异步流式事件处理器
|
||||
5. **工具系统**:自定义工具注册和执行
|
||||
6. **调试日志器**:前端控制台日志,用于故障排除
|
||||
|
||||
---
|
||||
|
||||
## 请求处理流程
|
||||
|
||||
### 完整请求生命周期
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI 请求] --> B[pipe 入口点]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{设置环境}
|
||||
D --> E[解析模型 ID]
|
||||
E --> F[提取聊天上下文]
|
||||
F --> G[提取系统提示词]
|
||||
G --> H{会话存在?}
|
||||
H -->|是| I[恢复会话]
|
||||
H -->|否| J[创建新会话]
|
||||
I --> K[初始化工具]
|
||||
J --> K
|
||||
K --> L[处理图片]
|
||||
L --> M{流式模式?}
|
||||
M -->|是| N[stream_response]
|
||||
M -->|否| O[send_and_wait]
|
||||
N --> P[异步事件流]
|
||||
O --> Q[直接响应]
|
||||
P --> R[返回到 OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### 逐步分解
|
||||
|
||||
#### 1. 环境设置 (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
优先级:
|
||||
1. 检查 VALVES.CLI_PATH
|
||||
2. 搜索系统 PATH
|
||||
3. 自动通过 curl 安装(如果未找到)
|
||||
4. 设置 GH_TOKEN 环境变量
|
||||
"""
|
||||
```
|
||||
|
||||
**操作:**
|
||||
|
||||
- 定位 Copilot CLI 二进制文件
|
||||
- 设置 `COPILOT_CLI_PATH` 环境变量
|
||||
- 配置 `GH_TOKEN` 进行身份验证
|
||||
- 应用自定义环境变量
|
||||
|
||||
#### 2. 模型选择
|
||||
|
||||
```python
|
||||
# 输入:body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. 聊天上下文提取 (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# chat_id 的优先级顺序:
|
||||
# 1. __metadata__(最可靠)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. 系统提示词提取 (`_extract_system_prompt`)
|
||||
|
||||
多源回退策略:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. 模型数据库查询(按 model_id)
|
||||
3. `body.params.system`
|
||||
4. 包含 `role="system"` 的消息
|
||||
|
||||
#### 5. 会话创建/恢复
|
||||
|
||||
**新会话:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**恢复会话:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# 会话状态保留:历史、工具、工作区
|
||||
except Exception:
|
||||
# 回退到创建新会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话管理
|
||||
|
||||
### 无限会话架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 会话生命周期 │
|
||||
│ │
|
||||
│ ┌──────────┐ 创建 ┌──────────┐ 恢复 ┌───────────┐ │
|
||||
│ │ Chat ID │─────▶ │ Session │ ◀────────│ OpenWebUI │ │
|
||||
│ └──────────┘ │ State │ └───────────┘ │
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ 上下文窗口管理 │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 消息 [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token 使用率: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 达到阈值 (0.8) │ │ │
|
||||
│ │ │ → 后台压缩触发 │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 压缩摘要 + 最近消息 │ │ │
|
||||
│ │ │ Token 使用率: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 配置参数
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # 启用无限会话
|
||||
background_compaction_threshold=0.8, # 在 80% token 使用率时开始压缩
|
||||
buffer_exhaustion_threshold=0.95 # 95% 紧急阈值
|
||||
)
|
||||
```
|
||||
|
||||
**行为:**
|
||||
|
||||
- **< 80%**:正常操作,无压缩
|
||||
- **80-95%**:后台压缩(总结旧消息)
|
||||
- **> 95%**:在下一个请求前强制压缩
|
||||
|
||||
---
|
||||
|
||||
## 流式响应处理
|
||||
|
||||
### 事件驱动架构
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
使用基于队列的缓冲进行异步事件处理。
|
||||
|
||||
流程:
|
||||
1. 启动异步发送任务
|
||||
2. 注册事件处理器
|
||||
3. 通过队列处理事件
|
||||
4. 向 OpenWebUI 产出块
|
||||
5. 清理资源
|
||||
"""
|
||||
```
|
||||
|
||||
### 事件处理管道
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK 事件流 │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 事件处理器 │
|
||||
│ (同步回调) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 异步队列 │
|
||||
│ (线程安全) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 消费者循环 │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield 到 OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### 流式传输期间的状态管理
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> 标签已打开
|
||||
"content_sent": False # 主内容已开始
|
||||
}
|
||||
active_tools = {} # 跟踪并发工具执行
|
||||
```
|
||||
|
||||
**状态转换:**
|
||||
|
||||
1. `reasoning_delta` 到达 → `thinking_started = True` → 输出:`<think>\n{reasoning}`
|
||||
2. `message_delta` 到达 → 如果打开则关闭 `</think>` → `content_sent = True` → 输出:`{content}`
|
||||
3. `tool.execution_start` → 输出工具指示器(在 `<think>` 内部/外部)
|
||||
4. `session.complete` → 完成流
|
||||
|
||||
---
|
||||
|
||||
## 事件处理机制
|
||||
|
||||
### 事件类型参考
|
||||
|
||||
遵循官方 SDK 模式(来自 `copilot.SessionEventType`):
|
||||
|
||||
| 事件类型 | 描述 | 关键数据字段 | 处理器操作 |
|
||||
|---------|------|-------------|-----------|
|
||||
| `assistant.message_delta` | 主内容流式传输 | `delta_content` | 产出文本块 |
|
||||
| `assistant.reasoning_delta` | 思维链 | `delta_content` | 用 `<think>` 标签包装 |
|
||||
| `tool.execution_start` | 工具调用启动 | `name`, `tool_call_id` | 显示工具指示器 |
|
||||
| `tool.execution_complete` | 工具完成 | `result.content` | 显示完成状态 |
|
||||
| `session.compaction_start` | 上下文压缩开始 | - | 记录调试信息 |
|
||||
| `session.compaction_complete` | 压缩完成 | - | 记录调试信息 |
|
||||
| `session.error` | 发生错误 | `error`, `message` | 发出错误通知 |
|
||||
|
||||
### 事件处理器实现
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""遵循官方 SDK 模式处理流式事件。"""
|
||||
event_type = get_event_type(event) # 处理枚举/字符串类型
|
||||
|
||||
# 使用 safe_get_data_attr 提取数据(处理 dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # 线程安全入队
|
||||
```
|
||||
|
||||
### 官方 SDK 模式合规性
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
官方模式:event.data.delta_content
|
||||
处理 dict 和对象访问模式。
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict 访问(类似 JSON)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# 对象属性(Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具执行流程
|
||||
|
||||
### 工具注册
|
||||
|
||||
```python
|
||||
# 1. 在模块级别定义工具
|
||||
@define_tool(description="在指定范围内生成随机整数。")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"生成的随机数: {number}"
|
||||
|
||||
# 2. 在 _initialize_custom_tools 中注册
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# 根据 AVAILABLE_TOOLS valve 过滤
|
||||
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]
|
||||
```
|
||||
|
||||
### 工具执行时间线
|
||||
|
||||
```
|
||||
用户消息:生成一个 1 到 100 之间的随机数
|
||||
│
|
||||
▼
|
||||
模型决策:使用工具 `generate_random_number`
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_start
|
||||
│ → 显示:"🔧 运行工具:generate_random_number"
|
||||
▼
|
||||
工具函数执行(异步)
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_complete
|
||||
│ → 结果:"生成的随机数:42"
|
||||
│ → 显示:"✅ 工具完成:42"
|
||||
▼
|
||||
模型使用工具结果生成响应
|
||||
│
|
||||
▼
|
||||
事件:assistant.message_delta
|
||||
│ → "我为你生成了数字 42。"
|
||||
▼
|
||||
流完成
|
||||
```
|
||||
|
||||
### 视觉指示器
|
||||
|
||||
**内容前:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
运行工具:generate_random_number...
|
||||
工具 `generate_random_number` 完成。结果:42
|
||||
</think>
|
||||
|
||||
我为你生成了数字 42。
|
||||
```
|
||||
|
||||
**内容开始后:**
|
||||
|
||||
```markdown
|
||||
数字是
|
||||
|
||||
> 🔧 **运行工具**:`generate_random_number`
|
||||
|
||||
> ✅ **工具完成**:42
|
||||
|
||||
实际上是 42。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 系统提示词提取
|
||||
|
||||
### 多源优先级系统
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
优先级顺序:
|
||||
1. metadata.model.params.system(最高)
|
||||
2. 模型数据库查询
|
||||
3. body.params.system
|
||||
4. messages[role="system"](回退)
|
||||
"""
|
||||
```
|
||||
|
||||
### 来源 1:元数据模型参数
|
||||
|
||||
```python
|
||||
# OpenWebUI 注入模型配置
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # 优先级 1
|
||||
```
|
||||
|
||||
### 来源 2:模型数据库
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# 尝试多个模型 ID 变体
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # 来自 valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### 来源 3:Body 参数
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### 来源 4:系统消息
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### SessionConfig 中的配置
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # 追加到对话上下文
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... 其他参数
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置参数
|
||||
|
||||
### Valve 定义
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|--------|------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub 精细化令牌(需要 'Copilot Requests' 权限) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | 动态获取失败时的默认模型 |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Copilot CLI 二进制文件路径 |
|
||||
| `DEBUG` | bool | `False` | 启用前端控制台调试日志 |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI 日志级别:none、error、warning、info、debug、all |
|
||||
| `SHOW_THINKING` | bool | `True` | 在 `<think>` 标签中显示模型推理 |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | 在调试模式下显示会话工作区路径 |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | 逗号分隔的关键字,用于排除模型 |
|
||||
| `WORKSPACE_DIR` | str | `""` | 限制的工作区目录(空 = 进程 cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | 启用自动上下文压缩 |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | 80% token 使用率时后台压缩 |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | 95% 紧急阈值 |
|
||||
| `TIMEOUT` | int | `300` | 流块超时(秒) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | 自定义环境变量的 JSON 字符串 |
|
||||
| `ENABLE_TOOLS` | bool | `False` | 启用自定义工具系统 |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | 可用工具:"all" 或逗号分隔列表 |
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 由 _setup_env 设置
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# 自定义变量(来自 CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心函数参考
|
||||
|
||||
### 入口点
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:OpenWebUI 稳定入口点
|
||||
- **返回**:委托给 `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:主请求处理逻辑
|
||||
- **流程**:设置 → 提取 → 会话 → 响应
|
||||
- **返回**:`str`(非流式)或 `AsyncGenerator`(流式)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **目的**:动态模型列表获取
|
||||
- **返回**:带有倍数信息的可用模型列表
|
||||
- **缓存**:使用 `_model_cache` 避免重复 API 调用
|
||||
|
||||
### 会话管理
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **目的**:构建 SessionConfig 对象
|
||||
- **返回**:带有无限会话和工具的 `SessionConfig`
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **目的**:使用优先级回退提取 chat_id
|
||||
- **返回**:`{"chat_id": str}`
|
||||
|
||||
### 流式传输
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **目的**:异步流式事件处理器
|
||||
- **产出**:文本块到 OpenWebUI
|
||||
- **资源**:自动清理客户端和会话
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **目的**:同步事件回调(在 `stream_response` 内)
|
||||
- **操作**:解析事件 → 入队块 → 更新状态
|
||||
|
||||
### 辅助函数
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **目的**:将调试日志发送到前端控制台
|
||||
- **条件**:仅当 `DEBUG=True` 时
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **目的**:定位 CLI,设置环境变量
|
||||
- **副作用**:修改 `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **目的**:多源系统提示词提取
|
||||
- **返回**:`(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **目的**:从多模态消息中提取文本和图片
|
||||
- **返回**:`(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **目的**:注册和过滤自定义工具
|
||||
- **返回**:工具函数列表
|
||||
|
||||
### 实用函数
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **目的**:从枚举/字符串提取事件类型字符串
|
||||
- **处理**:`SessionEventType` 枚举 → `.value` 提取
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **目的**:从 event.data 安全提取属性
|
||||
- **处理**:dict 访问和对象属性访问
|
||||
|
||||
---
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
### 启用调试模式
|
||||
|
||||
```python
|
||||
# 在 OpenWebUI Valves UI 中:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### 调试输出位置
|
||||
|
||||
**前端控制台:**
|
||||
|
||||
```javascript
|
||||
// 打开浏览器开发工具 (F12)
|
||||
// 查找前缀为 [Copilot Pipe] 的日志
|
||||
console.debug("[Copilot Pipe] 提取的 ChatID:abc123(来源:__metadata__)")
|
||||
```
|
||||
|
||||
**后端日志:**
|
||||
|
||||
```python
|
||||
# Python 日志输出
|
||||
logger.debug(f"[Copilot Pipe] 会话已恢复:{chat_id}")
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 会话未恢复
|
||||
|
||||
**症状**:每次请求都创建新会话
|
||||
**原因**:
|
||||
|
||||
- `chat_id` 提取不正确
|
||||
- Copilot 端会话过期
|
||||
- `INFINITE_SESSION=False`(会话不持久)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"提取的 ChatID:<id>(来源:...)"
|
||||
"会话 <id> 未找到(...),正在创建新会话。"
|
||||
```
|
||||
|
||||
#### 2. 系统提示词未应用
|
||||
|
||||
**症状**:模型忽略配置的系统提示词
|
||||
**原因**:
|
||||
|
||||
- 在 4 个来源中均未找到
|
||||
- 会话已恢复(系统提示词仅在创建时设置)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"从 <source> 提取系统提示词(长度:X)"
|
||||
"配置系统消息(模式:append)"
|
||||
```
|
||||
|
||||
#### 3. 工具不可用
|
||||
|
||||
**症状**:模型无法使用自定义工具
|
||||
**原因**:
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- 工具未在 `_initialize_custom_tools` 中注册
|
||||
- 错误的 `AVAILABLE_TOOLS` 过滤器
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"已启用 X 个自定义工具:['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 模型列表缓存
|
||||
|
||||
```python
|
||||
# 第一次请求:从 API 获取
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # 缓存结果
|
||||
|
||||
# 后续请求:使用缓存
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### 会话持久化
|
||||
|
||||
**影响**:消除每次请求的冗余模型初始化
|
||||
|
||||
```python
|
||||
# 没有会话:
|
||||
# 每次请求:初始化模型 → 加载上下文 → 生成 → 丢弃
|
||||
|
||||
# 有会话(chat_id):
|
||||
# 第一次请求:初始化模型 → 加载上下文 → 生成 → 保存
|
||||
# 后续:恢复 → 生成(即时)
|
||||
```
|
||||
|
||||
### 流式 vs 非流式
|
||||
|
||||
**流式:**
|
||||
|
||||
- 降低感知延迟(首个 token 更快)
|
||||
- 长响应的更好用户体验
|
||||
- 通过生成器退出进行资源清理
|
||||
|
||||
**非流式:**
|
||||
|
||||
- 更简单的错误处理
|
||||
- 原子响应(无部分输出)
|
||||
- 用于短响应
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 令牌保护
|
||||
|
||||
```python
|
||||
# ❌ 永远不要记录令牌
|
||||
logger.debug(f"令牌:{self.valves.GH_TOKEN}") # 不要这样做
|
||||
|
||||
# ✅ 屏蔽敏感数据
|
||||
logger.debug(f"令牌已配置:{'*' * 10}")
|
||||
```
|
||||
|
||||
### 工作区隔离
|
||||
|
||||
```python
|
||||
# 设置 WORKSPACE_DIR 以限制文件访问
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI 遵守此目录
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### 输入验证
|
||||
|
||||
```python
|
||||
# 验证 chat_id 格式
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"无效的 chat_id 格式:{chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未来增强
|
||||
|
||||
### 计划功能
|
||||
|
||||
1. **多会话管理**:支持每个用户的多个并行会话
|
||||
2. **会话分析**:跟踪 token 使用率、压缩频率
|
||||
3. **工具结果缓存**:避免冗余工具调用
|
||||
4. **自定义事件过滤器**:用户可配置的事件处理
|
||||
5. **工作区模板**:预配置的工作区环境
|
||||
6. **流式中止**:优雅取消长时间运行的请求
|
||||
|
||||
### API 演进
|
||||
|
||||
监控 Copilot SDK 更新:
|
||||
|
||||
- 新事件类型(例如 `assistant.function_call`)
|
||||
- 增强的工具功能
|
||||
- 改进的会话序列化
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [GitHub Copilot SDK 文档](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe 开发](https://docs.openwebui.com/)
|
||||
- [Awesome OpenWebUI 项目](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
|
||||
---
|
||||
|
||||
**许可证**:MIT
|
||||
**维护者**:Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
124
plugins/debug/github-copilot-sdk/test_capabilities.py
Normal file
124
plugins/debug/github-copilot-sdk/test_capabilities.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from copilot import CopilotClient, define_tool
|
||||
from copilot.types import SessionConfig
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# Define a simple tool for testing
|
||||
class RandomNumberParams(BaseModel):
|
||||
min: int = Field(description="Minimum value")
|
||||
max: int = Field(description="Maximum value")
|
||||
|
||||
|
||||
@define_tool(description="Generate a random integer within a range.")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
import random
|
||||
|
||||
return f"Result: {random.randint(params.min, params.max)}"
|
||||
|
||||
|
||||
async def main():
|
||||
print(f"Running tests with Python: {sys.executable}")
|
||||
|
||||
# 1. Setup Client
|
||||
client = CopilotClient({"log_level": "error"})
|
||||
await client.start()
|
||||
|
||||
try:
|
||||
print("\n=== Test 1: Session Creation & Formatting Injection ===")
|
||||
# Use gpt-4o or similar capable model
|
||||
model_id = "gpt-5-mini"
|
||||
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": "You are a test assistant. Always start your response with 'TEST_PREFIX: '.",
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
model=model_id,
|
||||
system_message=system_message_config,
|
||||
tools=[generate_random_number],
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
session_id = session.session_id
|
||||
print(f"Session Created: {session_id}")
|
||||
|
||||
# Test 1.1: Check system prompt effect
|
||||
resp = await session.send_and_wait(
|
||||
{"prompt": "Say hello.", "mode": "immediate"}
|
||||
)
|
||||
content = resp.data.content
|
||||
print(f"Response 1: {content}")
|
||||
|
||||
if "TEST_PREFIX:" in content:
|
||||
print("✅ System prompt injection active.")
|
||||
else:
|
||||
print("⚠️ System prompt injection NOT detected.")
|
||||
|
||||
print("\n=== Test 2: Tool Execution ===")
|
||||
# Test Tool Usage
|
||||
prompt_with_tool = (
|
||||
"Generate a random number between 100 and 200 using the tool."
|
||||
)
|
||||
print(f"Sending: {prompt_with_tool}")
|
||||
|
||||
# We need to listen to events to verify tool execution,
|
||||
# but send_and_wait handles it internally and returns the final answer.
|
||||
# We check if the final answer mentions the result.
|
||||
|
||||
resp_tool = await session.send_and_wait(
|
||||
{"prompt": prompt_with_tool, "mode": "immediate"}
|
||||
)
|
||||
tool_content = resp_tool.data.content
|
||||
print(f"Response 2: {tool_content}")
|
||||
|
||||
if "Result:" in tool_content or any(char.isdigit() for char in tool_content):
|
||||
print("✅ Tool likely executed (numbers found).")
|
||||
else:
|
||||
print("⚠️ Tool execution uncertain.")
|
||||
|
||||
print("\n=== Test 3: Context Retention (Memory) ===")
|
||||
# Store a fact
|
||||
await session.send_and_wait(
|
||||
{"prompt": "My secret code is 'BLUE-42'. Remember it.", "mode": "immediate"}
|
||||
)
|
||||
print("Fact sent.")
|
||||
|
||||
# Retrieve it
|
||||
resp_mem = await session.send_and_wait(
|
||||
{"prompt": "What is my secret code?", "mode": "immediate"}
|
||||
)
|
||||
mem_content = resp_mem.data.content
|
||||
print(f"Response 3: {mem_content}")
|
||||
|
||||
if "BLUE-42" in mem_content:
|
||||
print("✅ Context retention successful.")
|
||||
else:
|
||||
print("⚠️ Context retention failed.")
|
||||
|
||||
# Cleanup
|
||||
await session.destroy()
|
||||
|
||||
print("\n=== Test 4: Resume Session (Simulation) ===")
|
||||
# Note: Actual resuming depends on backend persistence.
|
||||
# The SDK's client.resume_session(id) tries to find it.
|
||||
# Since we destroyed it above, we expect failure or new session logic in real app.
|
||||
# But let's create a new one to persist, close client, and try to resume if process was same?
|
||||
# Actually persistence usually requires the Copilot Agent/Extension host to keep state or file backed.
|
||||
# The Python SDK defaults to file-based workspace in standard generic usage?
|
||||
# Let's just skip complex resume testing for this simple script as it depends on environment (vscode-chat-session vs file).
|
||||
print("Skipping complex resume test in script.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Test Failed: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
print("\nTests Completed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
94
plugins/debug/github-copilot-sdk/test_injection.py
Normal file
94
plugins/debug/github-copilot-sdk/test_injection.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from copilot import CopilotClient
|
||||
from copilot.types import SessionConfig
|
||||
|
||||
# Define the formatting instruction exactly as in the plugin
|
||||
FORMATTING_INSTRUCTION = (
|
||||
"\n\n[Formatting Guidelines]\n"
|
||||
"When providing explanations or descriptions:\n"
|
||||
"- Use clear paragraph breaks (double line breaks)\n"
|
||||
"- Break long sentences into multiple shorter ones\n"
|
||||
"- Use bullet points or numbered lists for multiple items\n"
|
||||
"- Add headings (##, ###) for major sections\n"
|
||||
"- Ensure proper spacing between different topics"
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
print(f"Python executable: {sys.executable}")
|
||||
|
||||
# Check for GH_TOKEN
|
||||
token = os.environ.get("GH_TOKEN")
|
||||
if token:
|
||||
print("GH_TOKEN is set.")
|
||||
else:
|
||||
print(
|
||||
"Warning: GH_TOKEN not found in environment variables. Relying on CLI auth."
|
||||
)
|
||||
|
||||
client_config = {"log_level": "debug"}
|
||||
|
||||
client = CopilotClient(client_config)
|
||||
|
||||
try:
|
||||
print("Starting client...")
|
||||
await client.start()
|
||||
|
||||
# Test 1: Check available models
|
||||
try:
|
||||
models = await client.list_models()
|
||||
print(f"Connection successful. Found {len(models)} models.")
|
||||
model_id = "gpt-5-mini" # User requested model
|
||||
except Exception as e:
|
||||
print(f"Failed to list models: {e}")
|
||||
return
|
||||
|
||||
print(f"\nCreating session with model {model_id} and system injection...")
|
||||
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": "You are a helpful assistant." + FORMATTING_INSTRUCTION,
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
model=model_id, system_message=system_message_config
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
print(f"Session created: {session.session_id}")
|
||||
|
||||
# Test 2: Ask the model to summarize its instructions
|
||||
prompt = "Please summarize the [Formatting Guidelines] you have been given in a list."
|
||||
|
||||
print(f"\nSending prompt: '{prompt}'")
|
||||
response = await session.send_and_wait({"prompt": prompt, "mode": "immediate"})
|
||||
|
||||
print("\n--- Model Response ---")
|
||||
content = response.data.content if response and response.data else "No content"
|
||||
print(content)
|
||||
print("----------------------")
|
||||
|
||||
required_keywords = ["paragraph", "break", "heading", "spacing", "bullet"]
|
||||
found_keywords = [kw for kw in required_keywords if kw in content.lower()]
|
||||
|
||||
if len(found_keywords) >= 3:
|
||||
print(
|
||||
f"\n✅ SUCCESS: Model summarized the guidelines correctly. Found match for: {found_keywords}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\n⚠️ UNCERTAIN: Summary might be generic. Found keywords: {found_keywords}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
print("\nClient stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
359
plugins/debug/language-debug/language_debug.py
Normal file
359
plugins/debug/language-debug/language_debug.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""
|
||||
title: UI Language Debugger
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.1.0
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxwYXRoIGQ9Im01IDggNiA2Ii8+CiAgPHBhdGggZD0ibTQgMTQgNi02IDItMiIvPgogIDxwYXRoIGQ9Ik0yIDVoMTIiLz4KICA8cGF0aCBkPSJNNyAyaDEiLz4KICA8cGF0aCBkPSJtMjIgMjItNS0xMC01IDEwIi8+CiAgPHBhdGggZD0iTTE0IDE4aDYiLz4KPC9zdmc+Cg==
|
||||
description: Debug UI language detection in the browser console and on-page panel.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HTML_WRAPPER_TEMPLATE = """
|
||||
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="{user_language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
#main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* STYLES_INSERTION_POINT */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<!-- CONTENT_INSERTION_POINT -->
|
||||
</div>
|
||||
<!-- SCRIPTS_INSERTION_POINT -->
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CONTENT_TEMPLATE = """
|
||||
<div class="lang-debug-card" id="lang-debug-card-{unique_id}">
|
||||
<div class="lang-debug-header">
|
||||
🧭 UI Language Debugger
|
||||
</div>
|
||||
<div class="lang-debug-body">
|
||||
<div class="lang-debug-row"><span>python.ui_language</span><code id="lang-py-{unique_id}">{python_language}</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.lang</span><code id="lang-html-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.getAttribute('lang')</span><code id="lang-attr-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dir</span><code id="lang-dir-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.body.lang</span><code id="lang-body-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.language</span><code id="lang-nav-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.languages</span><code id="lang-navs-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.language</span><code id="lang-store-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.locale</span><code id="lang-locale-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.i18n</span><code id="lang-i18n-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.settings</span><code id="lang-settings-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dataset</span><code id="lang-dataset-{unique_id}">-</code></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
STYLE_TEMPLATE = """
|
||||
.lang-debug-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.lang-debug-header {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.lang-debug-body {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.lang-debug-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.9em;
|
||||
color: #1f2937;
|
||||
}
|
||||
.lang-debug-row code {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
color: #0f172a;
|
||||
}
|
||||
"""
|
||||
|
||||
SCRIPT_TEMPLATE = """
|
||||
<script>
|
||||
(function() {{
|
||||
const uniqueId = "{unique_id}";
|
||||
const get = (id) => document.getElementById(id + '-' + uniqueId);
|
||||
|
||||
const safe = (value) => {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
if (Array.isArray(value)) return value.join(", ");
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const safeJson = (value) => {
|
||||
try {
|
||||
return value ? JSON.stringify(JSON.parse(value)) : "-";
|
||||
} catch (e) {
|
||||
return value ? String(value) : "-";
|
||||
}
|
||||
};
|
||||
|
||||
const settingsRaw = localStorage.getItem('settings');
|
||||
const i18nRaw = localStorage.getItem('i18n');
|
||||
const localeRaw = localStorage.getItem('locale');
|
||||
|
||||
const payload = {{
|
||||
htmlLang: document.documentElement.lang,
|
||||
htmlAttr: document.documentElement.getAttribute('lang'),
|
||||
htmlDir: document.documentElement.dir,
|
||||
bodyLang: document.body ? document.body.lang : "",
|
||||
navigatorLanguage: navigator.language,
|
||||
navigatorLanguages: navigator.languages,
|
||||
localStorageLanguage: localStorage.getItem('language'),
|
||||
localStorageLocale: localeRaw,
|
||||
localStorageI18n: i18nRaw,
|
||||
localStorageSettings: settingsRaw,
|
||||
htmlDataset: document.documentElement.dataset,
|
||||
}};
|
||||
|
||||
get('lang-html').textContent = safe(payload.htmlLang);
|
||||
get('lang-attr').textContent = safe(payload.htmlAttr);
|
||||
get('lang-dir').textContent = safe(payload.htmlDir);
|
||||
get('lang-body').textContent = safe(payload.bodyLang);
|
||||
get('lang-nav').textContent = safe(payload.navigatorLanguage);
|
||||
get('lang-navs').textContent = safe(payload.navigatorLanguages);
|
||||
get('lang-store').textContent = safe(payload.localStorageLanguage);
|
||||
get('lang-locale').textContent = safe(payload.localStorageLocale);
|
||||
get('lang-i18n').textContent = safeJson(payload.localStorageI18n);
|
||||
get('lang-settings').textContent = safeJson(payload.localStorageSettings);
|
||||
get('lang-dataset').textContent = safe(payload.htmlDataset);
|
||||
|
||||
console.group('🧭 UI Language Debugger');
|
||||
console.log(payload);
|
||||
console.groupEnd();
|
||||
}})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
SHOW_STATUS: bool = Field(
|
||||
default=True,
|
||||
description="Whether to show operation status updates.",
|
||||
)
|
||||
SHOW_DEBUG_LOG: bool = Field(
|
||||
default=True,
|
||||
description="Whether to print debug logs in the browser console.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
elif isinstance(__user__, dict):
|
||||
user_data = __user__
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", ""),
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
self, body: dict, __metadata__: Optional[dict] = None
|
||||
) -> Dict[str, str]:
|
||||
chat_id = ""
|
||||
message_id = ""
|
||||
|
||||
if isinstance(body, dict):
|
||||
chat_id = body.get("chat_id", "")
|
||||
message_id = body.get("id", "")
|
||||
|
||||
if not chat_id or not message_id:
|
||||
body_metadata = body.get("metadata", {})
|
||||
if isinstance(body_metadata, dict):
|
||||
if not chat_id:
|
||||
chat_id = body_metadata.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = body_metadata.get("message_id", "")
|
||||
|
||||
if __metadata__ and isinstance(__metadata__, dict):
|
||||
if not chat_id:
|
||||
chat_id = __metadata__.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = __metadata__.get("message_id", "")
|
||||
|
||||
return {
|
||||
"chat_id": str(chat_id).strip(),
|
||||
"message_id": str(message_id).strip(),
|
||||
}
|
||||
|
||||
async def _emit_status(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
description: str,
|
||||
done: bool = False,
|
||||
):
|
||||
if self.valves.SHOW_STATUS and emitter:
|
||||
await emitter(
|
||||
{"type": "status", "data": {"description": description, "done": done}}
|
||||
)
|
||||
|
||||
async def _emit_debug_log(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
title: str,
|
||||
data: dict,
|
||||
):
|
||||
if not self.valves.SHOW_DEBUG_LOG or not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
js_code = f"""
|
||||
(async function() {{
|
||||
console.group("🛠️ {title}");
|
||||
console.log({json.dumps(data, ensure_ascii=False)});
|
||||
console.groupEnd();
|
||||
}})();
|
||||
"""
|
||||
|
||||
await emitter({"type": "execute", "data": {"code": js_code}})
|
||||
except Exception as e:
|
||||
logger.error("Error emitting debug log: %s", e, exc_info=True)
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html: str,
|
||||
new_content: str,
|
||||
new_styles: str = "",
|
||||
new_scripts: str = "",
|
||||
user_language: str = "en-US",
|
||||
) -> str:
|
||||
if not existing_html:
|
||||
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||
else:
|
||||
base_html = existing_html
|
||||
|
||||
if "<!-- CONTENT_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- CONTENT_INSERTION_POINT -->",
|
||||
f"{new_content}\n <!-- CONTENT_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
if new_styles and "/* STYLES_INSERTION_POINT */" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"/* STYLES_INSERTION_POINT */",
|
||||
f"{new_styles}\n /* STYLES_INSERTION_POINT */",
|
||||
)
|
||||
|
||||
if new_scripts and "<!-- SCRIPTS_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- SCRIPTS_INSERTION_POINT -->",
|
||||
f"{new_scripts}\n <!-- SCRIPTS_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
return base_html
|
||||
|
||||
async def action(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[Dict[str, Any]] = None,
|
||||
__event_emitter__: Optional[Any] = None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
) -> Optional[dict]:
|
||||
await self._emit_status(__event_emitter__, "Detecting UI language...", False)
|
||||
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: user context",
|
||||
user_ctx,
|
||||
)
|
||||
|
||||
ui_language = ""
|
||||
if __event_call__:
|
||||
try:
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": "return (localStorage.getItem('locale') || localStorage.getItem('language') || (navigator.languages && navigator.languages[0]) || navigator.language || document.documentElement.lang || '')",
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: execute response",
|
||||
{"response": response},
|
||||
)
|
||||
if isinstance(response, dict) and "value" in response:
|
||||
ui_language = response.get("value", "") or ""
|
||||
elif isinstance(response, str):
|
||||
ui_language = response
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to read UI language from frontend: %s", e, exc_info=True
|
||||
)
|
||||
|
||||
unique_id = f"lang_{int(__import__('time').time() * 1000)}"
|
||||
content_html = CONTENT_TEMPLATE.replace("{unique_id}", unique_id).replace(
|
||||
"{python_language}", ui_language or "-"
|
||||
)
|
||||
script_html = SCRIPT_TEMPLATE.replace("{unique_id}", unique_id)
|
||||
script_html = script_html.replace("{{", "{").replace("}}", "}")
|
||||
|
||||
final_html = self._merge_html(
|
||||
"",
|
||||
content_html,
|
||||
STYLE_TEMPLATE,
|
||||
script_html,
|
||||
"en",
|
||||
)
|
||||
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] = (
|
||||
body["messages"][-1].get("content", "") + "\n\n" + html_embed_tag
|
||||
)
|
||||
|
||||
await self._emit_status(__event_emitter__, "UI language captured.", True)
|
||||
return body
|
||||
@@ -139,42 +139,45 @@ def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[
|
||||
print(f"Error parsing {previous_file}", file=sys.stderr)
|
||||
return {"added": current, "updated": [], "removed": []}
|
||||
|
||||
# Create lookup dictionaries by title
|
||||
# Helper to extract title/version from either simple dict or raw post object
|
||||
# Create lookup dictionaries by file_path (fallback to title)
|
||||
# Helper to extract title/version/file_path from either simple dict or raw post object
|
||||
def get_info(p):
|
||||
if "data" in p and "function" in p["data"]:
|
||||
# It's a raw post object
|
||||
manifest = p["data"]["function"].get("meta", {}).get("manifest", {})
|
||||
title = manifest.get("title") or p.get("title")
|
||||
version = manifest.get("version", "0.0.0")
|
||||
return title, version, p
|
||||
file_path = p.get("file_path")
|
||||
return title, version, file_path, p
|
||||
else:
|
||||
# It's a simple dict
|
||||
return p.get("title"), p.get("version"), p
|
||||
return p.get("title"), p.get("version"), p.get("file_path"), p
|
||||
|
||||
current_by_title = {}
|
||||
current_by_key = {}
|
||||
for p in current:
|
||||
title, _, _ = get_info(p)
|
||||
if title:
|
||||
current_by_title[title] = p
|
||||
title, _, file_path, _ = get_info(p)
|
||||
key = file_path or title
|
||||
if key:
|
||||
current_by_key[key] = p
|
||||
|
||||
previous_by_title = {}
|
||||
previous_by_key = {}
|
||||
for p in previous:
|
||||
title, _, _ = get_info(p)
|
||||
if title:
|
||||
previous_by_title[title] = p
|
||||
title, _, file_path, _ = get_info(p)
|
||||
key = file_path or title
|
||||
if key:
|
||||
previous_by_key[key] = p
|
||||
|
||||
result = {"added": [], "updated": [], "removed": []}
|
||||
|
||||
# Find added and updated plugins
|
||||
for title, plugin in current_by_title.items():
|
||||
curr_title, curr_ver, _ = get_info(plugin)
|
||||
for key, plugin in current_by_key.items():
|
||||
curr_title, curr_ver, _file_path, _ = get_info(plugin)
|
||||
|
||||
if title not in previous_by_title:
|
||||
if key not in previous_by_key:
|
||||
result["added"].append(plugin)
|
||||
else:
|
||||
prev_plugin = previous_by_title[title]
|
||||
_, prev_ver, _ = get_info(prev_plugin)
|
||||
prev_plugin = previous_by_key[key]
|
||||
_, prev_ver, _prev_file_path, _ = get_info(prev_plugin)
|
||||
|
||||
if curr_ver != prev_ver:
|
||||
result["updated"].append(
|
||||
@@ -185,8 +188,8 @@ def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[
|
||||
)
|
||||
|
||||
# Find removed plugins
|
||||
for title, plugin in previous_by_title.items():
|
||||
if title not in current_by_title:
|
||||
for key, plugin in previous_by_key.items():
|
||||
if key not in current_by_key:
|
||||
result["removed"].append(plugin)
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user