From 219ba83df365ac7e885b27ba008413b695de30fa Mon Sep 17 00:00:00 2001 From: fujie Date: Wed, 28 Jan 2026 02:14:30 +0800 Subject: [PATCH] feat(infographic): release v1.5.0 with smart language detection & organize debug tools --- .github/copilot-instructions.md | 82 +- .github/workflows/publish_plugin.yml | 1 + docs/features/plugin/development/events.mdx | 49 +- docs/plugins/pipes/github-copilot-sdk.md | 66 +- docs/plugins/pipes/github-copilot-sdk.zh.md | 66 +- plugins/actions/infographic/README.md | 11 +- plugins/actions/infographic/README_CN.md | 11 +- plugins/actions/infographic/infographic.py | 56 +- plugins/actions/infographic/infographic_cn.py | 58 +- .../guides/COPILOT_TOOLS_QUICKSTART.md | 568 ++++++++++++ .../guides/NATIVE_TOOL_DISPLAY_GUIDE.md | 480 ++++++++++ .../guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md | 480 ++++++++++ .../guides/NATIVE_TOOL_DISPLAY_USAGE.md | 182 ++++ .../guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md | 182 ++++ .../guides/OPENWEBUI_FUNCTION_INTEGRATION.md | 509 +++++++++++ .../guides/SESSIONCONFIG_INTEGRATION_GUIDE.md | 708 +++++++++++++++ .../github-copilot-sdk/guides/TOOLS_USAGE.md | 191 ++++ .../guides/TOOL_IMPLEMENTATION_GUIDE.md | 431 +++++++++ .../github-copilot-sdk/guides/WORKFLOW.md | 835 ++++++++++++++++++ .../github-copilot-sdk/guides/WORKFLOW_CN.md | 835 ++++++++++++++++++ .../github-copilot-sdk/test_capabilities.py | 124 +++ .../github-copilot-sdk/test_injection.py | 94 ++ .../debug/language-debug/language_debug.py | 359 ++++++++ scripts/extract_plugin_versions.py | 41 +- 24 files changed, 6320 insertions(+), 99 deletions(-) create mode 100644 plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md create mode 100644 plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md create mode 100644 plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md create mode 100644 plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md create mode 100644 plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md create mode 100644 plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md create mode 100644 plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md create mode 100644 plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md create mode 100644 plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md create mode 100644 plugins/debug/github-copilot-sdk/guides/WORKFLOW.md create mode 100644 plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md create mode 100644 plugins/debug/github-copilot-sdk/test_capabilities.py create mode 100644 plugins/debug/github-copilot-sdk/test_injection.py create mode 100644 plugins/debug/language-debug/language_debug.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c2fabfc..b3f45e0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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) diff --git a/.github/workflows/publish_plugin.yml b/.github/workflows/publish_plugin.yml index 2d1729c..9e02117 100644 --- a/.github/workflows/publish_plugin.yml +++ b/.github/workflows/publish_plugin.yml @@ -6,6 +6,7 @@ on: - main paths: - 'plugins/**/*.py' + - '!plugins/debug/**' release: types: [published] workflow_dispatch: diff --git a/docs/features/plugin/development/events.mdx b/docs/features/plugin/development/events.mdx index 09f66ab..aaa3ee0 100644 --- a/docs/features/plugin/development/events.mdx +++ b/docs/features/plugin/development/events.mdx @@ -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! 🚀** \ No newline at end of file +**Happy event-driven coding in Open WebUI! 🚀** diff --git a/docs/plugins/pipes/github-copilot-sdk.md b/docs/plugins/pipes/github-copilot-sdk.md index 6cf084e..fadfd03 100644 --- a/docs/plugins/pipes/github-copilot-sdk.md +++ b/docs/plugins/pipes/github-copilot-sdk.md @@ -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. diff --git a/docs/plugins/pipes/github-copilot-sdk.zh.md b/docs/plugins/pipes/github-copilot-sdk.zh.md index 842dc60..c5fdd87 100644 --- a/docs/plugins/pipes/github-copilot-sdk.zh.md +++ b/docs/plugins/pipes/github-copilot-sdk.zh.md @@ -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`。 diff --git a/plugins/actions/infographic/README.md b/plugins/actions/infographic/README.md index 53e665f..332dd9c 100644 --- a/plugins/actions/infographic/README.md +++ b/plugins/actions/infographic/README.md @@ -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: diff --git a/plugins/actions/infographic/README_CN.md b/plugins/actions/infographic/README_CN.md index 667599a..7d1ebf5 100644 --- a/plugins/actions/infographic/README_CN.md +++ b/plugins/actions/infographic/README_CN.md @@ -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 渲染: diff --git a/plugins/actions/infographic/infographic.py b/plugins/actions/infographic/infographic.py index e1461e1..bf8fcad 100644 --- a/plugins/actions/infographic/infographic.py +++ b/plugins/actions/infographic/infographic.py @@ -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() diff --git a/plugins/actions/infographic/infographic_cn.py b/plugins/actions/infographic/infographic_cn.py index 7ee754f..2482e80 100644 --- a/plugins/actions/infographic/infographic_cn.py +++ b/plugins/actions/infographic/infographic_cn.py @@ -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() diff --git a/plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md b/plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md new file mode 100644 index 0000000..890b891 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md new file mode 100644 index 0000000..43aa7c9 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md new file mode 100644 index 0000000..5e10cca --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md @@ -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 +**状态:** 分析完成 - 实施等待测试 diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md new file mode 100644 index 0000000..9ed1fda --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md new file mode 100644 index 0000000..f646255 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md b/plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md new file mode 100644 index 0000000..78b09c8 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md b/plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..ba2b7a4 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md @@ -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 文档**: + +--- + +## ✅ 实施检查清单 + +使用此清单跟踪实施进度: + +- [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 +**更新:** 随功能实施持续更新 diff --git a/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md b/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md new file mode 100644 index 0000000..0a6baed --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md b/plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..6092180 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md @@ -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 diff --git a/plugins/debug/github-copilot-sdk/guides/WORKFLOW.md b/plugins/debug/github-copilot-sdk/guides/WORKFLOW.md new file mode 100644 index 0000000..09e502f --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/WORKFLOW.md @@ -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, # 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: `\n{reasoning}` +2. `message_delta` arrives → Close `` if open → `content_sent = True` → Output: `{content}` +3. `tool.execution_start` → Output tool indicator (inside/outside ``) +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 `` 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 + +Running Tool: generate_random_number... +Tool `generate_random_number` Completed. Result: 42 + + +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 `` 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: (Source: ...)" +"Session 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 (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)) diff --git a/plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md b/plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md new file mode 100644 index 0000000..e739901 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md @@ -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, # 标签已打开 + "content_sent": False # 主内容已开始 +} +active_tools = {} # 跟踪并发工具执行 +``` + +**状态转换:** + +1. `reasoning_delta` 到达 → `thinking_started = True` → 输出:`\n{reasoning}` +2. `message_delta` 到达 → 如果打开则关闭 `` → `content_sent = True` → 输出:`{content}` +3. `tool.execution_start` → 输出工具指示器(在 `` 内部/外部) +4. `session.complete` → 完成流 + +--- + +## 事件处理机制 + +### 事件类型参考 + +遵循官方 SDK 模式(来自 `copilot.SessionEventType`): + +| 事件类型 | 描述 | 关键数据字段 | 处理器操作 | +|---------|------|-------------|-----------| +| `assistant.message_delta` | 主内容流式传输 | `delta_content` | 产出文本块 | +| `assistant.reasoning_delta` | 思维链 | `delta_content` | 用 `` 标签包装 | +| `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 + +运行工具:generate_random_number... +工具 `generate_random_number` 完成。结果:42 + + +我为你生成了数字 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` | 在 `` 标签中显示模型推理 | +| `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:(来源:...)" +"会话 未找到(...),正在创建新会话。" +``` + +#### 2. 系统提示词未应用 + +**症状**:模型忽略配置的系统提示词 +**原因**: + +- 在 4 个来源中均未找到 +- 会话已恢复(系统提示词仅在创建时设置) + +**解决方案**: + +```python +# 检查调试日志中的: +"从 提取系统提示词(长度: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)) diff --git a/plugins/debug/github-copilot-sdk/test_capabilities.py b/plugins/debug/github-copilot-sdk/test_capabilities.py new file mode 100644 index 0000000..484ac36 --- /dev/null +++ b/plugins/debug/github-copilot-sdk/test_capabilities.py @@ -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()) diff --git a/plugins/debug/github-copilot-sdk/test_injection.py b/plugins/debug/github-copilot-sdk/test_injection.py new file mode 100644 index 0000000..3d9169b --- /dev/null +++ b/plugins/debug/github-copilot-sdk/test_injection.py @@ -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()) diff --git a/plugins/debug/language-debug/language_debug.py b/plugins/debug/language-debug/language_debug.py new file mode 100644 index 0000000..a60dfc4 --- /dev/null +++ b/plugins/debug/language-debug/language_debug.py @@ -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 = """ + + + + + + + + + +
+ +
+ + + +""" + +CONTENT_TEMPLATE = """ +
+
+ 🧭 UI Language Debugger +
+
+
python.ui_language{python_language}
+
document.documentElement.lang-
+
document.documentElement.getAttribute('lang')-
+
document.documentElement.dir-
+
document.body.lang-
+
navigator.language-
+
navigator.languages-
+
localStorage.language-
+
localStorage.locale-
+
localStorage.i18n-
+
localStorage.settings-
+
document.documentElement.dataset-
+
+
+""" + +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 = """ + +""" + + +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 "" in base_html: + base_html = base_html.replace( + "", + f"{new_content}\n ", + ) + + 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 "" in base_html: + base_html = base_html.replace( + "", + f"{new_scripts}\n ", + ) + + 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 diff --git a/scripts/extract_plugin_versions.py b/scripts/extract_plugin_versions.py index cb90a7c..128dece 100644 --- a/scripts/extract_plugin_versions.py +++ b/scripts/extract_plugin_versions.py @@ -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