Compare commits
29 Commits
v2026.02.2
...
v2026.02.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb79bc9633 | ||
|
|
0c7d427b93 | ||
|
|
07bc5f027e | ||
|
|
701fc3e906 | ||
|
|
d392af66c9 | ||
|
|
67cf86fb26 | ||
|
|
fe98b0e007 | ||
|
|
3236d19e28 | ||
|
|
354c1eee6b | ||
|
|
6b3eb8064b | ||
|
|
f32e90e182 | ||
|
|
8001ab18ee | ||
|
|
dcfde9c0dc | ||
|
|
dbcf7421ea | ||
|
|
1705baf976 | ||
|
|
8e8d478ece | ||
|
|
acc9cd7ff2 | ||
|
|
e4582c3197 | ||
|
|
d0eb72467d | ||
|
|
6b6e62398a | ||
|
|
fd22ed8fa0 | ||
|
|
4fccd1893e | ||
|
|
a74e03fff8 | ||
|
|
850838226d | ||
|
|
a52ac34d59 | ||
|
|
3263ab9db6 | ||
|
|
7c7daef30b | ||
|
|
64754ba26b | ||
|
|
4188410d61 |
@@ -18,7 +18,7 @@ This is a **universal testing framework** for publishing the latest `github_copi
|
||||
|
||||
| Attribute | Fixed Value |
|
||||
|------|--------|
|
||||
| **Deployment Script** | `/Users/fujie/app/python/oui/openwebui-extensions/scripts/update_pipe.py` |
|
||||
| **Deployment Script** | `/Users/fujie/app/python/oui/openwebui-extensions/scripts/deploy_pipe.py` |
|
||||
| **Python Path** | `/opt/homebrew/Caskroom/miniconda/base/envs/ai/bin/python3` |
|
||||
| **Test URL** | `http://localhost:3003/?model=github_copilot_official_sdk_pipe.github_copilot_sdk-gpt-4.1` |
|
||||
|
||||
@@ -36,11 +36,11 @@ Example: *Modified tool calling logic -> Test prompt should trigger a specific t
|
||||
Use the `run_command` tool to execute the fixed update task:
|
||||
|
||||
```bash
|
||||
/opt/homebrew/Caskroom/miniconda/base/envs/ai/bin/python3 /Users/fujie/app/python/oui/openwebui-extensions/scripts/update_pipe.py
|
||||
/opt/homebrew/Caskroom/miniconda/base/envs/ai/bin/python3 /Users/fujie/app/python/oui/openwebui-extensions/scripts/deploy_pipe.py
|
||||
```
|
||||
|
||||
> **Mechanism**: `update_pipe.py` automatically loads the API Key from `scripts/.env` in the same directory.
|
||||
> **Verification**: Look for `✅ Successfully updated... version X.X.X`. If a 401 error occurs, remind the user to generate a new API Key in OpenWebUI and update `.env`.
|
||||
> **Mechanism**: `deploy_pipe.py` automatically loads the API Key from `scripts/.env` in the same directory.
|
||||
> **Verification**: Look for `✅ Successfully updated... version X.X.X` or `✅ Successfully created...`. If a 401 error occurs, remind the user to generate a new API Key in OpenWebUI and update `.env`.
|
||||
|
||||
### Step 3: Verify via Browser Subagent (Verify)
|
||||
|
||||
|
||||
23
README.md
23
README.md
@@ -9,6 +9,7 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
|
||||
<!-- STATS_START -->
|
||||
## 📊 Community Stats
|
||||
>
|
||||
> 
|
||||
|
||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||
@@ -19,18 +20,19 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
|  |  |  |  |  |
|
||||
|
||||
|
||||
### 🔥 Top 6 Popular Plugins
|
||||
|
||||
| Rank | Plugin | Version | Downloads | Views | 📅 Updated |
|
||||
| :---: | :--- | :---: | :---: | :---: | :---: |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 4️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 6️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) |  |  |  |  |
|
||||
|
||||
### 📈 Total Downloads Trend
|
||||
|
||||

|
||||
|
||||
*See full stats and charts in [Community Stats Report](./docs/community-stats.md)*
|
||||
@@ -40,10 +42,23 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
|
||||
### 1. [GitHub Copilot SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) [](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4)
|
||||
|
||||
**The ultimate Agent for OpenWebUI.** Supports native code execution (Python/Pandas), raw file analysis, and interactive artifacts.
|
||||
**The ultimate autonomous Agent for OpenWebUI.** Transforming your LLM into a powerful OS-level engineer with native code execution, deep tool autonomy, and professional skill management.
|
||||
|
||||
> [!TIP]
|
||||
> **No GitHub Copilot subscription required!** Supports **BYOK (Bring Your Own Key)** mode using your own OpenAI/Anthropic API keys.
|
||||
|
||||
#### 🚀 Key Leap (v0.9.0+)
|
||||
|
||||
- **🧩 Skills Revolution**: Native support for **SKILL directories** (scripts, templates, resources) coupled with a **Bidirectional Bridge** to OpenWebUI Workspace Skills.
|
||||
- **🛡️ Secure Isolation**: Strict user/session-level **Workspace Sandboxing** with persistent configuration.
|
||||
- **📊 Interactive Delivery**: Professional **File Delivery Protocol** for instant HTML artifacts and persistent downloadable results.
|
||||
- **🛠️ Deterministic Toolchain**: Built-in specialized tools for skill lifecycles (`manage_skills`) and system optimization.
|
||||
|
||||
#### 📺 Demo: Visual Skills & Data Analysis
|
||||
|
||||

|
||||
> *In this demo, the Agent installs a visual enhancement skill and automatically generates an interactive dashboard from World Cup data.*
|
||||
|
||||
#### 🌟 Featured Real-World Cases
|
||||
|
||||
- **[GitHub Star Forecasting](./docs/plugins/pipes/star-prediction-example.md)**: Automatically parsing CSV data, writing analysis scripts, and generating interactive growth dashboards.
|
||||
|
||||
23
README_CN.md
23
README_CN.md
@@ -6,6 +6,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
|
||||
<!-- STATS_START -->
|
||||
## 📊 社区统计
|
||||
>
|
||||
> 
|
||||
|
||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||
@@ -16,18 +17,19 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
|  |  |  |  |  |
|
||||
|
||||
|
||||
### 🔥 热门插件 Top 6
|
||||
|
||||
| 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 |
|
||||
| :---: | :--- | :---: | :---: | :---: | :---: |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) |  |  |  |  |
|
||||
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) |  |  |  |  |
|
||||
| 4️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) |  |  |  |  |
|
||||
| 5️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) |  |  |  |  |
|
||||
| 6️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) |  |  |  |  |
|
||||
|
||||
### 📈 总下载量累计趋势
|
||||
|
||||

|
||||
|
||||
*完整统计与趋势图请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||
@@ -37,10 +39,23 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
|
||||
### 1. [GitHub Copilot SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) [](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4)
|
||||
|
||||
**OpenWebUI 终极 Agent 增强。** 支持原生代码执行(Python/Pandas)、原始文件直接分析以及交互式 Artifacts。
|
||||
**OpenWebUI 终极自主 Agent 增强。** 将 LLM 转化为具备 OS 级操作能力的专业工程师,支持原生代码执行、深度工具自治以及专业技能管理。
|
||||
|
||||
> [!TIP]
|
||||
> **无需 GitHub Copilot 订阅!** 支持 **BYOK (Bring Your Own Key)** 模式,使用你自己的 OpenAI/Anthropic API Key。
|
||||
|
||||
#### 🚀 核心进化 (v0.9.0+)
|
||||
|
||||
- **🧩 技能革命**: 原生支持 **SKILL 目录**(含脚本、模板与资源),并实现与 OpenWebUI **工作区 > Skills** 的深度双向桥接。
|
||||
- **🛡️ 安全沙箱**: 严格的用户/会话级 **工作区隔离** 与持久化配置环境。
|
||||
- **📊 交互交付**: 专业 **文件交付协议**,支持即时预览交互式 HTML Artifacts 与持久化结果下载。
|
||||
- **🛠️ 确定性工具链**: 内置 `manage_skills` 等专业工具,赋予 Agent 完整的技能生命周期管理能力。
|
||||
|
||||
#### 📺 演示:可视化技能与数据分析
|
||||
|
||||

|
||||
> *在此演示中,Agent 自动安装可视化增强技能,并根据世界杯表格数据瞬间生成交互式看板。*
|
||||
|
||||
#### 🌟 核心实战案例
|
||||
|
||||
- **[GitHub Star 增长预测](./docs/plugins/pipes/star-prediction-example.zh.md)**:自动解析 CSV 数据,编写 Python 分析脚本并生成动态增长看板。
|
||||
|
||||
BIN
docs/assets/videos/skill.gif
Normal file
BIN
docs/assets/videos/skill.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
BIN
docs/assets/videos/skill_visual_agents.mov
Normal file
BIN
docs/assets/videos/skill_visual_agents.mov
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "downloads",
|
||||
"message": "6.0k",
|
||||
"message": "6.4k",
|
||||
"color": "blue",
|
||||
"namedLogo": "openwebui"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "followers",
|
||||
"message": "287",
|
||||
"message": "295",
|
||||
"color": "blue"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "plugins",
|
||||
"message": "24",
|
||||
"message": "25",
|
||||
"color": "green"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "points",
|
||||
"message": "295",
|
||||
"message": "299",
|
||||
"color": "orange"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "upvotes",
|
||||
"message": "251",
|
||||
"message": "254",
|
||||
"color": "brightgreen"
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"total_posts": 24,
|
||||
"total_downloads": 6041,
|
||||
"total_views": 64841,
|
||||
"total_upvotes": 251,
|
||||
"total_posts": 25,
|
||||
"total_downloads": 6379,
|
||||
"total_views": 67827,
|
||||
"total_upvotes": 254,
|
||||
"total_downvotes": 3,
|
||||
"total_saves": 329,
|
||||
"total_saves": 337,
|
||||
"total_comments": 58,
|
||||
"by_type": {
|
||||
"post": 5,
|
||||
"post": 6,
|
||||
"pipe": 1,
|
||||
"filter": 4,
|
||||
"action": 12,
|
||||
"filter": 4,
|
||||
"prompt": 1,
|
||||
"review": 1
|
||||
},
|
||||
@@ -22,13 +22,13 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
||||
"downloads": 1270,
|
||||
"views": 10965,
|
||||
"downloads": 1328,
|
||||
"views": 11410,
|
||||
"upvotes": 23,
|
||||
"saves": 59,
|
||||
"comments": 15,
|
||||
"created_at": "2025-12-30",
|
||||
"updated_at": "2026-02-22",
|
||||
"updated_at": "2026-02-27",
|
||||
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
|
||||
},
|
||||
{
|
||||
@@ -38,9 +38,9 @@
|
||||
"version": "1.5.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
|
||||
"downloads": 1032,
|
||||
"views": 10202,
|
||||
"upvotes": 24,
|
||||
"downloads": 1076,
|
||||
"views": 10746,
|
||||
"upvotes": 25,
|
||||
"saves": 40,
|
||||
"comments": 10,
|
||||
"created_at": "2025-12-28",
|
||||
@@ -54,13 +54,13 @@
|
||||
"version": "1.2.7",
|
||||
"author": "Fu-Jie",
|
||||
"description": "A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting. Including LaTeX command protection.",
|
||||
"downloads": 570,
|
||||
"views": 6491,
|
||||
"downloads": 609,
|
||||
"views": 6795,
|
||||
"upvotes": 18,
|
||||
"saves": 37,
|
||||
"comments": 5,
|
||||
"created_at": "2026-01-12",
|
||||
"updated_at": "2026-02-24",
|
||||
"updated_at": "2026-02-27",
|
||||
"url": "https://openwebui.com/posts/markdown_normalizer_baaa8732"
|
||||
},
|
||||
{
|
||||
@@ -70,8 +70,8 @@
|
||||
"version": "0.4.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
|
||||
"downloads": 544,
|
||||
"views": 4391,
|
||||
"downloads": 578,
|
||||
"views": 4611,
|
||||
"upvotes": 16,
|
||||
"saves": 30,
|
||||
"comments": 5,
|
||||
@@ -86,10 +86,10 @@
|
||||
"version": "1.3.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||
"downloads": 528,
|
||||
"views": 5201,
|
||||
"downloads": 559,
|
||||
"views": 5452,
|
||||
"upvotes": 15,
|
||||
"saves": 40,
|
||||
"saves": 41,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-02-21",
|
||||
@@ -102,8 +102,8 @@
|
||||
"version": "0.3.7",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
||||
"downloads": 470,
|
||||
"views": 2542,
|
||||
"downloads": 492,
|
||||
"views": 2693,
|
||||
"upvotes": 10,
|
||||
"saves": 8,
|
||||
"comments": 0,
|
||||
@@ -118,10 +118,10 @@
|
||||
"version": "",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 427,
|
||||
"views": 5106,
|
||||
"downloads": 473,
|
||||
"views": 5498,
|
||||
"upvotes": 9,
|
||||
"saves": 13,
|
||||
"saves": 14,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-28",
|
||||
"updated_at": "2026-01-28",
|
||||
@@ -134,8 +134,8 @@
|
||||
"version": "0.2.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||
"downloads": 275,
|
||||
"views": 4038,
|
||||
"downloads": 285,
|
||||
"views": 4128,
|
||||
"upvotes": 13,
|
||||
"saves": 18,
|
||||
"comments": 2,
|
||||
@@ -147,16 +147,16 @@
|
||||
"title": "GitHub Copilot Official SDK Pipe",
|
||||
"slug": "github_copilot_official_sdk_pipe_ce96f7b4",
|
||||
"type": "pipe",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, and frontend debug logging.",
|
||||
"downloads": 240,
|
||||
"views": 3867,
|
||||
"description": "Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, bidirectional OpenWebUI Skills bridge, and manage_skills tool.",
|
||||
"downloads": 263,
|
||||
"views": 4106,
|
||||
"upvotes": 14,
|
||||
"saves": 9,
|
||||
"saves": 10,
|
||||
"comments": 6,
|
||||
"created_at": "2026-01-26",
|
||||
"updated_at": "2026-02-25",
|
||||
"updated_at": "2026-02-27",
|
||||
"url": "https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4"
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
|
||||
"downloads": 197,
|
||||
"views": 1589,
|
||||
"downloads": 204,
|
||||
"views": 1631,
|
||||
"upvotes": 6,
|
||||
"saves": 13,
|
||||
"comments": 0,
|
||||
@@ -182,8 +182,8 @@
|
||||
"version": "0.4.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||
"downloads": 148,
|
||||
"views": 2560,
|
||||
"downloads": 153,
|
||||
"views": 2631,
|
||||
"upvotes": 14,
|
||||
"saves": 7,
|
||||
"comments": 4,
|
||||
@@ -198,8 +198,8 @@
|
||||
"version": "0.1.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Automatically extracts project rules from conversations and injects them into the folder's system prompt.",
|
||||
"downloads": 96,
|
||||
"views": 1788,
|
||||
"downloads": 99,
|
||||
"views": 1839,
|
||||
"upvotes": 7,
|
||||
"saves": 11,
|
||||
"comments": 0,
|
||||
@@ -214,8 +214,8 @@
|
||||
"version": "1.5.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||
"downloads": 63,
|
||||
"views": 1271,
|
||||
"downloads": 65,
|
||||
"views": 1304,
|
||||
"upvotes": 10,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
@@ -230,10 +230,10 @@
|
||||
"version": "0.1.3",
|
||||
"author": "Fu-Jie",
|
||||
"description": "A specialized filter to bypass OpenWebUI's default RAG for GitHub Copilot SDK models. It moves uploaded files to a safe location ('copilot_files') so the Copilot Pipe can process them natively without interference.",
|
||||
"downloads": 48,
|
||||
"views": 2033,
|
||||
"downloads": 54,
|
||||
"views": 2098,
|
||||
"upvotes": 3,
|
||||
"saves": 0,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2026-02-09",
|
||||
"updated_at": "2026-02-25",
|
||||
@@ -246,8 +246,8 @@
|
||||
"version": "0.9.2",
|
||||
"author": "Fu-Jie",
|
||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||
"downloads": 41,
|
||||
"views": 666,
|
||||
"downloads": 45,
|
||||
"views": 691,
|
||||
"upvotes": 6,
|
||||
"saves": 2,
|
||||
"comments": 0,
|
||||
@@ -262,8 +262,8 @@
|
||||
"version": "1.2.2",
|
||||
"author": "Fu-Jie",
|
||||
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||
"downloads": 37,
|
||||
"views": 765,
|
||||
"downloads": 38,
|
||||
"views": 783,
|
||||
"upvotes": 7,
|
||||
"saves": 5,
|
||||
"comments": 0,
|
||||
@@ -278,9 +278,9 @@
|
||||
"version": "0.2.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
||||
"downloads": 30,
|
||||
"views": 806,
|
||||
"upvotes": 8,
|
||||
"downloads": 32,
|
||||
"views": 830,
|
||||
"upvotes": 7,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2025-12-30",
|
||||
@@ -294,8 +294,8 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
||||
"downloads": 25,
|
||||
"views": 560,
|
||||
"downloads": 26,
|
||||
"views": 581,
|
||||
"upvotes": 5,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
@@ -303,6 +303,22 @@
|
||||
"updated_at": "2026-01-08",
|
||||
"url": "https://openwebui.com/posts/精读_99830b0f"
|
||||
},
|
||||
{
|
||||
"title": "🚀 GitHub Copilot SDK Pipe v0.9.0: Copilot SDK Skills Core Capabilities & Extended Delivery",
|
||||
"slug": "github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452",
|
||||
"type": "post",
|
||||
"version": "",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 7,
|
||||
"upvotes": 0,
|
||||
"saves": 0,
|
||||
"comments": 0,
|
||||
"created_at": "2026-02-27",
|
||||
"updated_at": "2026-02-27",
|
||||
"url": "https://openwebui.com/posts/github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452"
|
||||
},
|
||||
{
|
||||
"title": "🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️",
|
||||
"slug": "github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d",
|
||||
@@ -311,9 +327,9 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 5,
|
||||
"upvotes": 0,
|
||||
"saves": 0,
|
||||
"views": 1059,
|
||||
"upvotes": 2,
|
||||
"saves": 2,
|
||||
"comments": 0,
|
||||
"created_at": "2026-02-25",
|
||||
"updated_at": "2026-02-25",
|
||||
@@ -327,7 +343,7 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 1826,
|
||||
"views": 2162,
|
||||
"upvotes": 7,
|
||||
"saves": 2,
|
||||
"comments": 1,
|
||||
@@ -343,9 +359,9 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 2215,
|
||||
"views": 2257,
|
||||
"upvotes": 7,
|
||||
"saves": 3,
|
||||
"saves": 4,
|
||||
"comments": 0,
|
||||
"created_at": "2026-02-10",
|
||||
"updated_at": "2026-02-10",
|
||||
@@ -359,7 +375,7 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 1811,
|
||||
"views": 1839,
|
||||
"upvotes": 12,
|
||||
"saves": 19,
|
||||
"comments": 8,
|
||||
@@ -375,7 +391,7 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 226,
|
||||
"views": 234,
|
||||
"upvotes": 2,
|
||||
"saves": 0,
|
||||
"comments": 0,
|
||||
@@ -391,9 +407,9 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 1488,
|
||||
"upvotes": 15,
|
||||
"saves": 10,
|
||||
"views": 1502,
|
||||
"upvotes": 16,
|
||||
"saves": 11,
|
||||
"comments": 2,
|
||||
"created_at": "2026-01-10",
|
||||
"updated_at": "2026-01-10",
|
||||
@@ -405,11 +421,11 @@
|
||||
"name": "Fu-Jie",
|
||||
"profile_url": "https://openwebui.com/u/Fu-Jie",
|
||||
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
|
||||
"followers": 287,
|
||||
"following": 5,
|
||||
"total_points": 295,
|
||||
"post_points": 248,
|
||||
"comment_points": 47,
|
||||
"contributions": 53
|
||||
"followers": 295,
|
||||
"following": 6,
|
||||
"total_points": 299,
|
||||
"post_points": 251,
|
||||
"comment_points": 48,
|
||||
"contributions": 54
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
> *Blue: Downloads | Purple: Views (Real-time dynamic)*
|
||||
|
||||
### 📂 Content Distribution
|
||||

|
||||

|
||||
|
||||
|
||||
## 📈 Overview
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
## 📂 By Type
|
||||
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
|
||||
@@ -36,15 +36,15 @@
|
||||
|
||||
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action |  |  |  |  |  | 2026-02-22 |
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action |  |  |  |  |  | 2026-02-27 |
|
||||
| 2 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 3 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | filter |  |  |  |  |  | 2026-02-24 |
|
||||
| 3 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | filter |  |  |  |  |  | 2026-02-27 |
|
||||
| 4 | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 5 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter |  |  |  |  |  | 2026-02-21 |
|
||||
| 6 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 7 | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | prompt |  |  |  |  |  | 2026-01-28 |
|
||||
| 8 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 9 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe |  |  |  |  |  | 2026-02-25 |
|
||||
| 9 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe |  |  |  |  |  | 2026-02-27 |
|
||||
| 10 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action |  |  |  |  |  | 2026-01-08 |
|
||||
| 11 | [导出为Word增强版](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 12 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter |  |  |  |  |  | 2026-01-20 |
|
||||
@@ -54,9 +54,10 @@
|
||||
| 16 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 17 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 18 | [精读](https://openwebui.com/posts/精读_99830b0f) | action |  |  |  |  |  | 2026-01-08 |
|
||||
| 19 | [🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️](https://openwebui.com/posts/github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d) | post |  |  |  |  |  | 2026-02-25 |
|
||||
| 20 | [🚀 GitHub Copilot SDK Pipe v0.7.0: Native Tool UI & Zero-Config CLI 🛠️](https://openwebui.com/posts/github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131) | post |  |  |  |  |  | 2026-02-22 |
|
||||
| 21 | [🚀 GitHub Copilot SDK Pipe: AI That Executes, Not Just Talks](https://openwebui.com/posts/github_copilot_sdk_for_openwebui_elevate_your_ai_t_a140f293) | post |  |  |  |  |  | 2026-02-10 |
|
||||
| 22 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | post |  |  |  |  |  | 2026-01-28 |
|
||||
| 23 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | review |  |  |  |  |  | 2026-01-14 |
|
||||
| 24 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | post |  |  |  |  |  | 2026-01-10 |
|
||||
| 19 | [🚀 GitHub Copilot SDK Pipe v0.9.0: Copilot SDK Skills Core Capabilities & Extended Delivery](https://openwebui.com/posts/github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452) | post |  |  |  |  |  | 2026-02-27 |
|
||||
| 20 | [🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️](https://openwebui.com/posts/github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d) | post |  |  |  |  |  | 2026-02-25 |
|
||||
| 21 | [🚀 GitHub Copilot SDK Pipe v0.7.0: Native Tool UI & Zero-Config CLI 🛠️](https://openwebui.com/posts/github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131) | post |  |  |  |  |  | 2026-02-22 |
|
||||
| 22 | [🚀 GitHub Copilot SDK Pipe: AI That Executes, Not Just Talks](https://openwebui.com/posts/github_copilot_sdk_for_openwebui_elevate_your_ai_t_a140f293) | post |  |  |  |  |  | 2026-02-10 |
|
||||
| 23 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | post |  |  |  |  |  | 2026-01-28 |
|
||||
| 24 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | review |  |  |  |  |  | 2026-01-14 |
|
||||
| 25 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | post |  |  |  |  |  | 2026-01-10 |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
> *蓝色: 总下载量 | 紫色: 总浏览量 (实时动态生成)*
|
||||
|
||||
### 📂 内容分类占比 (Distribution)
|
||||

|
||||

|
||||
|
||||
|
||||
## 📈 总览
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
## 📂 按类型分类
|
||||
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
|
||||
@@ -36,15 +36,15 @@
|
||||
|
||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action |  |  |  |  |  | 2026-02-22 |
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action |  |  |  |  |  | 2026-02-27 |
|
||||
| 2 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 3 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | filter |  |  |  |  |  | 2026-02-24 |
|
||||
| 3 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | filter |  |  |  |  |  | 2026-02-27 |
|
||||
| 4 | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 5 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter |  |  |  |  |  | 2026-02-21 |
|
||||
| 6 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 7 | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | prompt |  |  |  |  |  | 2026-01-28 |
|
||||
| 8 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 9 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe |  |  |  |  |  | 2026-02-25 |
|
||||
| 9 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe |  |  |  |  |  | 2026-02-27 |
|
||||
| 10 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action |  |  |  |  |  | 2026-01-08 |
|
||||
| 11 | [导出为Word增强版](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 12 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter |  |  |  |  |  | 2026-01-20 |
|
||||
@@ -54,9 +54,10 @@
|
||||
| 16 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 17 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action |  |  |  |  |  | 2026-02-13 |
|
||||
| 18 | [精读](https://openwebui.com/posts/精读_99830b0f) | action |  |  |  |  |  | 2026-01-08 |
|
||||
| 19 | [🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️](https://openwebui.com/posts/github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d) | post |  |  |  |  |  | 2026-02-25 |
|
||||
| 20 | [🚀 GitHub Copilot SDK Pipe v0.7.0: Native Tool UI & Zero-Config CLI 🛠️](https://openwebui.com/posts/github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131) | post |  |  |  |  |  | 2026-02-22 |
|
||||
| 21 | [🚀 GitHub Copilot SDK Pipe: AI That Executes, Not Just Talks](https://openwebui.com/posts/github_copilot_sdk_for_openwebui_elevate_your_ai_t_a140f293) | post |  |  |  |  |  | 2026-02-10 |
|
||||
| 22 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | post |  |  |  |  |  | 2026-01-28 |
|
||||
| 23 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | review |  |  |  |  |  | 2026-01-14 |
|
||||
| 24 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | post |  |  |  |  |  | 2026-01-10 |
|
||||
| 19 | [🚀 GitHub Copilot SDK Pipe v0.9.0: Copilot SDK Skills Core Capabilities & Extended Delivery](https://openwebui.com/posts/github_copilot_sdk_pipe_v090_copilot_sdk_skills_co_99a42452) | post |  |  |  |  |  | 2026-02-27 |
|
||||
| 20 | [🚀 GitHub Copilot SDK Pipe v0.8.0: Conditional Tool Filtering & Publish Reliability 🎛️](https://openwebui.com/posts/github_copilot_sdk_pipe_v080_conditional_tool_filt_a5a3322d) | post |  |  |  |  |  | 2026-02-25 |
|
||||
| 21 | [🚀 GitHub Copilot SDK Pipe v0.7.0: Native Tool UI & Zero-Config CLI 🛠️](https://openwebui.com/posts/github_copilot_sdk_pipe_v070_native_tool_ui_zero_c_4af38131) | post |  |  |  |  |  | 2026-02-22 |
|
||||
| 22 | [🚀 GitHub Copilot SDK Pipe: AI That Executes, Not Just Talks](https://openwebui.com/posts/github_copilot_sdk_for_openwebui_elevate_your_ai_t_a140f293) | post |  |  |  |  |  | 2026-02-10 |
|
||||
| 23 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | post |  |  |  |  |  | 2026-01-28 |
|
||||
| 24 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | review |  |  |  |  |  | 2026-01-14 |
|
||||
| 25 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | post |  |  |  |  |  | 2026-01-10 |
|
||||
|
||||
7
docs/plugins/tools/index.md
Normal file
7
docs/plugins/tools/index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Tools
|
||||
|
||||
OpenWebUI native Tool plugins that can be used across models.
|
||||
|
||||
## Available Tool Plugins
|
||||
|
||||
- [OpenWebUI Skills Manager Tool](openwebui-skills-manager-tool.md) (v0.2.1) - Simple native skill management (`list/show/install/create/update/delete`).
|
||||
7
docs/plugins/tools/index.zh.md
Normal file
7
docs/plugins/tools/index.zh.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Tools(工具)
|
||||
|
||||
可跨模型使用的 OpenWebUI 原生 Tool 插件。
|
||||
|
||||
## 可用 Tool 插件
|
||||
|
||||
- [OpenWebUI Skills 管理工具](openwebui-skills-manager-tool.zh.md) (v0.2.1) - 简化技能管理(`list/show/install/create/update/delete`)。
|
||||
32
docs/plugins/tools/openwebui-skills-manager-tool.md
Normal file
32
docs/plugins/tools/openwebui-skills-manager-tool.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OpenWebUI Skills Manager Tool
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
A standalone OpenWebUI Tool plugin for managing native Workspace Skills across models.
|
||||
|
||||
## What's New
|
||||
|
||||
- Added GitHub skills-directory auto-discovery for `install_skill` (e.g., `.../tree/main/skills`) to install all child skills in one request.
|
||||
- Fixed language detection with robust frontend-first fallback (`__event_call__` + timeout), request header fallback, and profile fallback.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Native skill management
|
||||
- User-scoped list/show/install/create/update/delete operations
|
||||
- Status-bar feedback for each operation
|
||||
|
||||
## Methods
|
||||
|
||||
- `list_skills`
|
||||
- `show_skill`
|
||||
- `install_skill`
|
||||
- `create_skill`
|
||||
- `update_skill`
|
||||
- `delete_skill`
|
||||
|
||||
## Installation
|
||||
|
||||
1. Open OpenWebUI → Workspace → Tools
|
||||
2. Create Tool and paste:
|
||||
- `plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py`
|
||||
3. Save and enable for your chat/model
|
||||
32
docs/plugins/tools/openwebui-skills-manager-tool.zh.md
Normal file
32
docs/plugins/tools/openwebui-skills-manager-tool.zh.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OpenWebUI Skills 管理工具
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
一个可跨模型使用的 OpenWebUI 原生 Tool 插件,用于管理 Workspace Skills。
|
||||
|
||||
## 最新更新
|
||||
|
||||
- `install_skill` 新增 GitHub 技能目录自动发现(例如 `.../tree/main/skills`),可一键安装目录下所有子技能。
|
||||
- 修复语言获取逻辑:前端优先(`__event_call__` + 超时保护),并回退到请求头与用户资料。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- 原生技能管理
|
||||
- 用户范围内的 list/show/install/create/update/delete
|
||||
- 每步操作提供状态栏反馈
|
||||
|
||||
## 方法
|
||||
|
||||
- `list_skills`
|
||||
- `show_skill`
|
||||
- `install_skill`
|
||||
- `create_skill`
|
||||
- `update_skill`
|
||||
- `delete_skill`
|
||||
|
||||
## 安装方式
|
||||
|
||||
1. 打开 OpenWebUI → Workspace → Tools
|
||||
2. 新建 Tool 并粘贴:
|
||||
- `plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py`
|
||||
3. 保存并在模型/聊天中启用
|
||||
@@ -1,359 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
======================================================================
|
||||
Staged README Synchronizer to OpenWebUI Community
|
||||
暂存 README 文件同步到 OpenWebUI 社区工具
|
||||
======================================================================
|
||||
|
||||
PURPOSE / 用途:
|
||||
--------------
|
||||
This script synchronizes staged README.md/README_CN.md files to their
|
||||
corresponding OpenWebUI Community posts automatically. It's designed for
|
||||
batch updating documentation content without modifying plugin versions
|
||||
or media attachments.
|
||||
|
||||
本脚本自动将暂存的 README.md/README_CN.md 文件同步到对应的 OpenWebUI
|
||||
社区帖子。专为批量更新文档内容设计,不修改插件版本或媒体附件。
|
||||
|
||||
USAGE / 使用方法:
|
||||
----------------
|
||||
1. Set up environment:
|
||||
配置环境:
|
||||
|
||||
Create a .env file in the repository root with:
|
||||
在仓库根目录创建 .env 文件,包含:
|
||||
|
||||
OPENWEBUI_API_KEY=your_api_key_here
|
||||
|
||||
2. Stage README files to sync:
|
||||
暂存需要同步的 README 文件:
|
||||
|
||||
git add plugins/actions/my_plugin/README.md
|
||||
git add plugins/actions/my_plugin/README_CN.md
|
||||
|
||||
3. Run the script:
|
||||
运行脚本:
|
||||
|
||||
python plugins/debug/common_tools/update_readmes_to_market.py
|
||||
|
||||
WORKFLOW / 工作流程:
|
||||
-------------------
|
||||
1. Load OPENWEBUI_API_KEY from .env file
|
||||
从 .env 文件加载 OPENWEBUI_API_KEY
|
||||
|
||||
2. Get list of staged README.md/README_CN.md files via git
|
||||
通过 git 获取暂存的 README.md/README_CN.md 文件列表
|
||||
|
||||
3. For each staged README:
|
||||
对于每个暂存的 README:
|
||||
|
||||
a. Locate the corresponding plugin .py file
|
||||
定位对应的插件 .py 文件
|
||||
|
||||
b. Extract openwebui_id/post_id from plugin frontmatter
|
||||
从插件前置信息中提取 openwebui_id/post_id
|
||||
|
||||
c. Fetch existing post data from OpenWebUI Community API
|
||||
从 OpenWebUI 社区 API 获取现有帖子数据
|
||||
|
||||
d. Update post content with new README content
|
||||
用新的 README 内容更新帖子内容
|
||||
|
||||
e. Push changes via API (preserves version & media)
|
||||
通过 API 推送更改(保留版本和媒体)
|
||||
|
||||
REQUIREMENTS / 依赖要求:
|
||||
-----------------------
|
||||
- python-dotenv: For loading .env configuration
|
||||
用于加载 .env 配置文件
|
||||
- Git repository: Must be run from a git-tracked workspace
|
||||
必须在 git 跟踪的工作区中运行
|
||||
|
||||
KEY FEATURES / 关键特性:
|
||||
-----------------------
|
||||
✅ Only updates content field (不仅更新内容字段)
|
||||
✅ Skips files without openwebui_id (跳过没有 openwebui_id 的文件)
|
||||
✅ Automatically matches CN/EN plugin files (自动匹配中英文插件文件)
|
||||
✅ Supports staged plugin source code updates (支持暂存插件源码更新)
|
||||
✅ Safe: Won't modify version or media fields (安全:不会修改版本或媒体字段)
|
||||
|
||||
NOTES / 注意事项:
|
||||
---------------
|
||||
- This is a DEBUG/DEVELOPMENT tool, not for production workflows
|
||||
这是一个调试/开发工具,不用于生产工作流
|
||||
|
||||
- Always verify changes in OpenWebUI Community after sync
|
||||
同步后务必在 OpenWebUI 社区中验证更改
|
||||
|
||||
- Requires valid API key with update permissions
|
||||
需要具有更新权限的有效 API 密钥
|
||||
|
||||
AUTHOR / 作者:
|
||||
-------------
|
||||
Fu-Jie
|
||||
GitHub: https://github.com/Fu-Jie/openwebui-extensions
|
||||
|
||||
======================================================================
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
|
||||
def _load_dotenv(repo_root: Path) -> None:
|
||||
try:
|
||||
from dotenv import load_dotenv # type: ignore
|
||||
except Exception as exc: # pragma: no cover
|
||||
print("Missing dependency: python-dotenv. Please install it and retry.")
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
env_path = repo_root / ".env"
|
||||
load_dotenv(env_path)
|
||||
|
||||
|
||||
def _get_repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def _get_staged_readmes(repo_root: Path) -> List[Path]:
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo_root),
|
||||
"diff",
|
||||
"--cached",
|
||||
"--name-only",
|
||||
"--",
|
||||
"*.md",
|
||||
],
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Failed to read staged files: {exc}")
|
||||
return []
|
||||
|
||||
paths = []
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.endswith("README.md") or line.endswith("README_CN.md"):
|
||||
paths.append(repo_root / line)
|
||||
return paths
|
||||
|
||||
|
||||
def _get_staged_plugin_files(repo_root: Path) -> List[Path]:
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo_root),
|
||||
"diff",
|
||||
"--cached",
|
||||
"--name-only",
|
||||
"--",
|
||||
"*.py",
|
||||
],
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Failed to read staged files: {exc}")
|
||||
return []
|
||||
|
||||
paths = []
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if "/plugins/" not in line:
|
||||
continue
|
||||
if line.endswith("__init__.py") or os.path.basename(line).startswith("test_"):
|
||||
continue
|
||||
paths.append(repo_root / line)
|
||||
return paths
|
||||
|
||||
|
||||
def _parse_frontmatter(content: str) -> Dict[str, str]:
|
||||
match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL)
|
||||
if not match:
|
||||
match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL)
|
||||
if not match:
|
||||
return {}
|
||||
|
||||
frontmatter = match.group(1)
|
||||
meta: Dict[str, str] = {}
|
||||
for line in frontmatter.split("\n"):
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
meta[key.strip()] = value.strip()
|
||||
return meta
|
||||
|
||||
|
||||
def _find_plugin_file(readme_path: Path) -> Optional[Path]:
|
||||
plugin_dir = readme_path.parent
|
||||
is_cn = readme_path.name.lower().endswith("readme_cn.md")
|
||||
|
||||
py_files = [
|
||||
p
|
||||
for p in plugin_dir.glob("*.py")
|
||||
if p.name != "__init__.py" and not p.name.startswith("test_")
|
||||
]
|
||||
if not py_files:
|
||||
return None
|
||||
|
||||
cn_files = [p for p in py_files if p.stem.endswith("_cn")]
|
||||
en_files = [p for p in py_files if not p.stem.endswith("_cn")]
|
||||
|
||||
candidates = cn_files + en_files if is_cn else en_files + cn_files
|
||||
|
||||
# Prefer files that contain openwebui_id/post_id in frontmatter
|
||||
for candidate in candidates:
|
||||
post_id = _get_post_id(candidate)
|
||||
if post_id:
|
||||
return candidate
|
||||
|
||||
return candidates[0] if candidates else None
|
||||
|
||||
|
||||
def _get_post_id(plugin_file: Path) -> Optional[str]:
|
||||
try:
|
||||
content = plugin_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
meta = _parse_frontmatter(content)
|
||||
return meta.get("openwebui_id") or meta.get("post_id")
|
||||
|
||||
|
||||
def _get_plugin_metadata(plugin_file: Path) -> Dict[str, str]:
|
||||
try:
|
||||
content = plugin_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return {}
|
||||
return _parse_frontmatter(content)
|
||||
|
||||
|
||||
def _find_readme_for_plugin(plugin_file: Path) -> Optional[str]:
|
||||
plugin_dir = plugin_file.parent
|
||||
is_cn = plugin_file.stem.endswith("_cn")
|
||||
readme_candidates = ["README_CN.md", "README.md"] if is_cn else ["README.md", "README_CN.md"]
|
||||
for name in readme_candidates:
|
||||
readme_path = plugin_dir / name
|
||||
if readme_path.exists():
|
||||
return readme_path.read_text(encoding="utf-8")
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
repo_root = _get_repo_root()
|
||||
_load_dotenv(repo_root)
|
||||
|
||||
api_key = os.environ.get("OPENWEBUI_API_KEY")
|
||||
if not api_key:
|
||||
print("OPENWEBUI_API_KEY is not set in environment.")
|
||||
return 1
|
||||
|
||||
client_module_path = repo_root / "scripts" / "openwebui_community_client.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"openwebui_community_client", client_module_path
|
||||
)
|
||||
if not spec or not spec.loader:
|
||||
print("Failed to load openwebui_community_client module.")
|
||||
return 1
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
client = module.get_client(api_key)
|
||||
|
||||
staged_plugins = _get_staged_plugin_files(repo_root)
|
||||
staged_readmes = _get_staged_readmes(repo_root)
|
||||
if not staged_plugins and not staged_readmes:
|
||||
print("No staged README or plugin files found.")
|
||||
return 0
|
||||
|
||||
updated_post_ids: set[str] = set()
|
||||
|
||||
for plugin_file in staged_plugins:
|
||||
if not plugin_file.exists():
|
||||
print(f"Skipped (missing): {plugin_file}")
|
||||
continue
|
||||
|
||||
post_id = _get_post_id(plugin_file)
|
||||
if not post_id:
|
||||
print(f"Skipped (no openwebui_id): {plugin_file}")
|
||||
continue
|
||||
|
||||
try:
|
||||
post_data = client.get_post(post_id)
|
||||
if not post_data:
|
||||
print(f"Skipped (post not found): {plugin_file}")
|
||||
continue
|
||||
|
||||
source_code = plugin_file.read_text(encoding="utf-8")
|
||||
metadata = _get_plugin_metadata(plugin_file)
|
||||
readme_content = _find_readme_for_plugin(plugin_file)
|
||||
|
||||
ok = client.update_plugin(
|
||||
post_id=post_id,
|
||||
source_code=source_code,
|
||||
readme_content=readme_content or metadata.get("description", ""),
|
||||
metadata=metadata,
|
||||
media_urls=None,
|
||||
)
|
||||
if ok:
|
||||
updated_post_ids.add(post_id)
|
||||
print(f"Updated plugin -> {plugin_file} (post_id: {post_id})")
|
||||
except Exception as exc:
|
||||
print(f"Failed: {plugin_file} ({exc})")
|
||||
|
||||
for readme_path in staged_readmes:
|
||||
if not readme_path.exists():
|
||||
print(f"Skipped (missing): {readme_path}")
|
||||
continue
|
||||
|
||||
plugin_file = _find_plugin_file(readme_path)
|
||||
if not plugin_file:
|
||||
print(f"Skipped (no plugin file): {readme_path}")
|
||||
continue
|
||||
|
||||
post_id = _get_post_id(plugin_file)
|
||||
if not post_id:
|
||||
print(f"Skipped (no openwebui_id): {readme_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
if post_id in updated_post_ids:
|
||||
print(f"Skipped (already updated via plugin): {readme_path}")
|
||||
continue
|
||||
|
||||
post_data = client.get_post(post_id)
|
||||
if not post_data:
|
||||
print(f"Skipped (post not found): {readme_path}")
|
||||
continue
|
||||
|
||||
readme_content = readme_path.read_text(encoding="utf-8")
|
||||
|
||||
# Update README content only, keep other fields unchanged.
|
||||
post_data["content"] = readme_content
|
||||
|
||||
ok = client.update_post(post_id, post_data)
|
||||
if ok:
|
||||
print(f"Updated README -> {readme_path} (post_id: {post_id})")
|
||||
except Exception as exc:
|
||||
print(f"Failed: {readme_path} ({exc})")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,98 +0,0 @@
|
||||
# 开发指南勘误与更新
|
||||
|
||||
## 权限控制章节修正(第 2.2 节)
|
||||
|
||||
### ⚠️ 关键勘误
|
||||
|
||||
在实际测试中发现,Shell 权限请求使用的是 **`fullCommandText`** 字段,而非文档中提到的 `command` 字段。
|
||||
|
||||
### 需要修改的代码行
|
||||
|
||||
**第 89 行(错误):**
|
||||
|
||||
```python
|
||||
command = request.get("command", "")
|
||||
```
|
||||
|
||||
**应改为(正确):**
|
||||
|
||||
```python
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
```
|
||||
|
||||
### 完整的正确实现
|
||||
|
||||
```python
|
||||
async def on_user_permission_request(request, context):
|
||||
"""
|
||||
统一权限审批网关
|
||||
"""
|
||||
kind = request.get("kind") # shell, write, mcp, read, url
|
||||
# ✅ 正确:使用 fullCommandText(shell)或 command(其他)
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
|
||||
# 1. 超级模式:全部允许
|
||||
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 2. 默认安全:始终允许 "读" 和 "Web浏览"
|
||||
if kind in ["read", "url"]:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 3. 细粒度控制
|
||||
if kind == "shell":
|
||||
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||
if pattern and command:
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
return {"kind": "approved"}
|
||||
except re.error:
|
||||
print(f"[Config Error] Invalid Regex: {pattern}")
|
||||
|
||||
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 4. 默认拒绝
|
||||
print(f"[Permission Denied] Blocked: {kind} {command}")
|
||||
return {
|
||||
"kind": "denied-by-rules",
|
||||
"rules": [{"kind": "check-openwebui-valves"}]
|
||||
}
|
||||
```
|
||||
|
||||
### Shell 权限请求的完整结构
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "shell",
|
||||
"toolCallId": "call_xxx",
|
||||
"fullCommandText": "ls -la", // ← 关键字段
|
||||
"intention": "List all files and directories",
|
||||
"commands": [
|
||||
{
|
||||
"identifier": "ls -la",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"possiblePaths": [],
|
||||
"possibleUrls": [],
|
||||
"hasWriteFileRedirection": false,
|
||||
"canOfferSessionApproval": false
|
||||
}
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
已通过完整测试套件验证(8/8 通过),详见 [PERMISSION_TEST_REPORT.md](./PERMISSION_TEST_REPORT.md)。
|
||||
|
||||
---
|
||||
|
||||
**更新日期**: 2026-01-30
|
||||
**验证状态**: ✅ 已测试
|
||||
**影响范围**: 2.2 权限与确认章节
|
||||
@@ -1,173 +0,0 @@
|
||||
# GitHub Copilot SDK 权限控制测试报告
|
||||
|
||||
## 测试日期
|
||||
|
||||
2026-01-30
|
||||
|
||||
## 测试环境
|
||||
|
||||
- **Model**: gpt-4.1
|
||||
- **Python**: 3.12
|
||||
- **Copilot SDK**: Latest
|
||||
|
||||
## 关键发现
|
||||
|
||||
### 1. Shell 权限请求结构
|
||||
|
||||
Shell 类型的权限请求使用 **`fullCommandText`** 字段,而非 `command` 字段。
|
||||
|
||||
**完整请求示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "shell",
|
||||
"toolCallId": "call_JKLi7tz3uSDQWE3LgzCpvSVy",
|
||||
"fullCommandText": "ls -la",
|
||||
"intention": "List all files and directories with details in the current directory",
|
||||
"commands": [
|
||||
{
|
||||
"identifier": "ls -la",
|
||||
"readOnly": false
|
||||
}
|
||||
],
|
||||
"possiblePaths": [],
|
||||
"possibleUrls": [],
|
||||
"hasWriteFileRedirection": false,
|
||||
"canOfferSessionApproval": false
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 正则匹配模式验证
|
||||
|
||||
正则白名单模式已验证有效,必须使用 `fullCommandText` 字段:
|
||||
|
||||
```python
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||
if pattern and command:
|
||||
if re.match(pattern, command):
|
||||
return {"kind": "approved"}
|
||||
```
|
||||
|
||||
## 测试结果
|
||||
|
||||
### 完整测试套件(8/8 通过 ✅)
|
||||
|
||||
| # | 测试用例 | 配置 | 提示词 | 预期 | 结果 | 状态 |
|
||||
|---|---------|------|--------|------|------|------|
|
||||
| 1 | Default Deny Shell | 默认 | 请执行: ls -la | ❌ Denied | ❌ Denied | ✅ |
|
||||
| 2 | Allow All | allow_all=True | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ |
|
||||
| 3 | Allow Shell | allow_shell=True | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ |
|
||||
| 4 | Regex Match: ^ls | pattern='^ls' | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ |
|
||||
| 5 | Regex No Match | pattern='^ls' | 请执行: pwd | ❌ Denied | ❌ Denied | ✅ |
|
||||
| 6 | Regex Complex | pattern='^(ls\|pwd\|echo)' | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ |
|
||||
| 7 | Regex No Match: git | pattern='^(ls\|pwd\|echo)' | 请执行: git status | ❌ Denied | ❌ Denied | ✅ |
|
||||
| 8 | Read Permission | 默认 | Read: README.md | ✅ Approved | ✅ Approved | ✅ |
|
||||
|
||||
**总体通过率: 100%** 🎉
|
||||
|
||||
## 推荐配置示例
|
||||
|
||||
### 1. 安全模式(推荐生产环境)
|
||||
|
||||
```python
|
||||
PERMISSIONS_ALLOW_ALL: bool = False
|
||||
PERMISSIONS_ALLOW_SHELL: bool = False
|
||||
PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat).*"
|
||||
PERMISSIONS_ALLOW_WRITE: bool = False
|
||||
PERMISSIONS_ALLOW_MCP: bool = True
|
||||
```
|
||||
|
||||
### 2. 开发模式
|
||||
|
||||
```python
|
||||
PERMISSIONS_ALLOW_ALL: bool = False
|
||||
PERMISSIONS_ALLOW_SHELL: bool = False
|
||||
PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat|grep|git status|npm test).*"
|
||||
PERMISSIONS_ALLOW_WRITE: bool = False
|
||||
PERMISSIONS_ALLOW_MCP: bool = True
|
||||
```
|
||||
|
||||
### 3. 完全信任模式(仅限受控环境)
|
||||
|
||||
```python
|
||||
PERMISSIONS_ALLOW_ALL: bool = True
|
||||
```
|
||||
|
||||
## 实现建议
|
||||
|
||||
### 正确的权限处理代码
|
||||
|
||||
```python
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
async def on_user_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||
"""
|
||||
统一权限审批网关
|
||||
"""
|
||||
kind = request.get("kind")
|
||||
# 关键:使用 fullCommandText 而非 command
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
|
||||
# 1. 超级模式
|
||||
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 2. 默认安全(read、url)
|
||||
if kind in ["read", "url"]:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 3. Shell 细粒度控制
|
||||
if kind == "shell":
|
||||
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||
if pattern and command:
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
return {"kind": "approved"}
|
||||
except re.error as e:
|
||||
logger.error(f"Invalid regex: {pattern} - {e}")
|
||||
|
||||
# 4. Write 权限
|
||||
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 5. MCP 权限
|
||||
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 6. 默认拒绝
|
||||
logger.warning(f"Permission Denied: {kind} {command}")
|
||||
return {
|
||||
"kind": "denied-by-rules",
|
||||
"rules": [{"kind": "security-policy"}]
|
||||
}
|
||||
```
|
||||
|
||||
## 常见正则模式示例
|
||||
|
||||
| 用途 | 正则表达式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| 只读命令 | `^(ls|pwd|cat|echo|grep).*` | 允许常见只读命令 |
|
||||
| Git 只读 | `^git (status\|log\|diff\|show).*` | 允许 Git 只读操作 |
|
||||
| npm/yarn 测试 | `^(npm\|yarn) (test\|run).*` | 允许测试脚本 |
|
||||
| 完全 shell | `.*` | ⚠️ 危险:允许所有命令 |
|
||||
|
||||
## 测试脚本位置
|
||||
|
||||
- 基础测试: [test_shell_permission_pattern.py](./test_shell_permission_pattern.py)
|
||||
- 完整测试套件: [test_permission_comprehensive.py](./test_permission_comprehensive.py)
|
||||
|
||||
## 结论
|
||||
|
||||
✅ **GitHub Copilot SDK 的权限控制机制完全有效**
|
||||
✅ **正则白名单模式已验证可用**
|
||||
⚠️ **必须使用 `fullCommandText` 字段获取命令内容**
|
||||
|
||||
---
|
||||
|
||||
**测试执行者**: GitHub Copilot
|
||||
**审核状态**: ✅ 已验证
|
||||
@@ -1,238 +0,0 @@
|
||||
# OpenWebUI GitHub Copilot Pipe Enhancement Guide
|
||||
|
||||
基于 Copilot SDK 源码级研究的深度技术总结,旨在指导 OpenWebUI Pipe 的功能增强开发。
|
||||
|
||||
## 1. 认证机制 (Authentication)
|
||||
|
||||
官方支持通过环境变量传递 Token。在 Pipe 中,只要确保 `GH_TOKEN` 或 `GITHUB_TOKEN` 存在于环境变量中,Copilot CLI 即可自动识别,无需在 `CopilotClient` 构造函数中重复注入。
|
||||
|
||||
### 核心实现
|
||||
|
||||
Pipe 应确保将 Token(来自 Valve 或 Env)正确设置到当前进程的环境变量中。
|
||||
|
||||
```python
|
||||
import os
|
||||
from copilot import CopilotClient
|
||||
|
||||
# 1. 设置环境变量 (如果从 Valve 获取)
|
||||
if self.valves.GH_TOKEN:
|
||||
os.environ["GH_TOKEN"] = self.valves.GH_TOKEN
|
||||
|
||||
# 2. 初始化客户端
|
||||
# CopilotClient 启动的 CLI 子进程会自动继承当前环境中的 GH_TOKEN
|
||||
client = CopilotClient({
|
||||
# "cli_path": ...,
|
||||
# 注意:无需在此处重复传入 github_token,CLI 会自动读取环境变量
|
||||
})
|
||||
|
||||
# 3. 启动前检查 (建议)
|
||||
# status = await client.get_auth_status()
|
||||
# if not status.isAuthenticated: ...
|
||||
```
|
||||
|
||||
## 2. 权限与确认 (Permissions & Tools) - 核心控制点
|
||||
|
||||
这是用户最关心的部分:如何知道有哪些工具,以及如何控制它们的执行。
|
||||
|
||||
### 2.1 内置工具 (Built-in Tools)
|
||||
|
||||
Copilot CLI 内部管理了一组标准工具,**Python SDK 目前没有直接的 API (`client.list_tools()`) 来列出这些工具**。
|
||||
|
||||
但是,根据 SDK 的 `PermissionRequest` 类型定义 (`copilot/types.py`),我们可以反推其能力类别:
|
||||
|
||||
* **`shell`**: 执行终端命令 (对应 `run_terminal_command` 等)
|
||||
* **`filesystem`** (对应 `read/write`): 文件读写 (对应 `read_file`, `edit_file`, `delete_file` 等)
|
||||
* **`url`**: 网络访问 (对应 `fetch_url` 等)
|
||||
* **`mcp`**: 连接的 MCP 服务器工具
|
||||
|
||||
> **提示**: `available_tools` 参数可以用来“隐藏”工具,让 Agent 根本不知道它有一把锤子。而 `on_permission_request` 是用来拦截 Agent 挥舞锤子的动作。通常我们建议**能力全开 (不设置 available_tools 限制)**,而在**权限层 (on_permission_request) 做拦截**。
|
||||
|
||||
### 2.2 实现“全部允许”与“按需允许”
|
||||
|
||||
建议在 Valves 中增加权限控制字段,并在 `on_permission_request` 中实现逻辑。
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
class Valves(BaseModel):
|
||||
# ... 其他 Valve ...
|
||||
# 权限控制开关
|
||||
PERMISSIONS_ALLOW_ALL: bool = Field(default=False, description="DANGER: Auto-approve ALL actions (shell, write, etc).")
|
||||
PERMISSIONS_ALLOW_SHELL: bool = Field(default=False, description="Auto-approve shell commands.")
|
||||
PERMISSIONS_SHELL_ALLOW_PATTERN: str = Field(default="", description="Regex for approved shell commands (e.g., '^ls|^grep').")
|
||||
PERMISSIONS_ALLOW_WRITE: bool = Field(default=False, description="Auto-approve file write/edit/delete.")
|
||||
PERMISSIONS_ALLOW_MCP: bool = Field(default=True, description="Auto-approve MCP tool execution.")
|
||||
|
||||
# 权限处理 Hook 实现
|
||||
async def on_user_permission_request(request, context):
|
||||
"""
|
||||
统一权限审批网关
|
||||
request keys: kind, toolCallId, ... (shell requests have 'command')
|
||||
"""
|
||||
kind = request.get("kind") # shell, write, mcp, read, url
|
||||
|
||||
# 1. 超级模式:全部允许
|
||||
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 2. 默认安全:始终允许 "读" 和 "Web浏览" (根据需求调整)
|
||||
if kind in ["read", "url"]:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 3. 细粒度控制
|
||||
if kind == "shell":
|
||||
# 3.1 完全允许 Shell
|
||||
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 3.2 基于正则允许特定命令
|
||||
command = request.get("command", "")
|
||||
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||
if pattern and command:
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
return {"kind": "approved"}
|
||||
except re.error:
|
||||
print(f"[Config Error] Invalid Regex: {pattern}")
|
||||
|
||||
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||
return {"kind": "approved"}
|
||||
|
||||
# 4. 默认拒绝
|
||||
print(f"[Permission Denied] Blocked request for: {kind} {request.get('command', '')}")
|
||||
return {
|
||||
"kind": "denied-by-rules",
|
||||
"rules": [{"kind": "check-openwebui-valves"}]
|
||||
}
|
||||
|
||||
# 注册 Hook
|
||||
session = await client.create_session({
|
||||
# ...
|
||||
"on_permission_request": on_user_permission_request
|
||||
})
|
||||
```
|
||||
|
||||
## 3. Agent 与 MCP 集成 (Agents & MCP)
|
||||
|
||||
SDK 中的 Agent 和 MCP 并非独立文件,而是会话配置 (`SessionConfig`) 的一部分。Pipe 可以通过 Valves 动态构建这些配置。
|
||||
|
||||
### 关键映射关系
|
||||
|
||||
| SDK 概念 | OpenWebUI 对应 | 实现位置 | 关键参数 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Custom Agent** | 自定义模型 / Persona | `create_session(custom_agents=[...])` | `name`, `prompt`, `tools` (仅名称) |
|
||||
| **Agent Tools** | Valve 开关 / 预置工具 | `create_session(tools=[func1, func2])` | 必须先在 `tools` 注册函数,Agent 才能引用 |
|
||||
| **MCP Server** | Valve 配置 (JSON) | `create_session(mcp_servers={...})` | `command`, `args`, `env` (本地) |
|
||||
|
||||
### 代码范式:动态构建 Agent
|
||||
|
||||
```python
|
||||
async def create_agent_session(client, user_prompt, model_name):
|
||||
# 1. 定义工具 (必须是函数引用)
|
||||
# 假设已从 OpenWebUI Tools 转换或内置
|
||||
available_tools = [tool_web_search, tool_run_script]
|
||||
|
||||
# 2. 构建 Agent Manifest (针对当前请求的虚拟 Agent)
|
||||
agent_manifest = {
|
||||
"name": "openwebui_agent",
|
||||
"description": "Dynamic agent from OpenWebUI",
|
||||
"prompt": "You are a helpful assistant...", # 这里注入 System Prompt
|
||||
"tools": ["web_search", "run_script"], # 引用上方工具的 name
|
||||
"mcp_servers": {
|
||||
# 可以在这里为特定 Agent 绑定 MCP
|
||||
}
|
||||
}
|
||||
|
||||
# 3. 创建会话
|
||||
session = await client.create_session({
|
||||
"model": "gpt-4", # 底层模型
|
||||
"custom_agents": [agent_manifest],
|
||||
"tools": available_tools, # 注册实际代码
|
||||
"available_tools": ["web_search"], # 白名单控制当前可用工具
|
||||
# ... 权限配置
|
||||
})
|
||||
```
|
||||
|
||||
## 4. MCP 服务器配置 (Native MCP Support)
|
||||
|
||||
Pipe 可以直接支持标准 MCP 协议(Stdio)。不需要额外的 MCP 客户端代理,SDK 原生支持。
|
||||
|
||||
### Valve 配置结构建议
|
||||
|
||||
建议在 Pipe 的 Valves 中增加一个 `MCP_CONFIG` 字段(JSON 字符串),解析后直接传给 SDK。
|
||||
|
||||
```python
|
||||
# Valve 输入示例 (JSON)
|
||||
# {
|
||||
# "brave_search": {
|
||||
# "type": "local",
|
||||
# "command": "npx",
|
||||
# "args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
# "env": {"BRAVE_API_KEY": "..."}
|
||||
# }
|
||||
# }
|
||||
|
||||
# 代码实现
|
||||
mcp_config = json.loads(self.valves.MCP_CONFIG)
|
||||
session = await client.create_session({
|
||||
# ...
|
||||
"mcp_servers": mcp_config,
|
||||
# 注意:必须配合权限自动审批,否则 MCP 工具无法调用
|
||||
"on_permission_request": auto_approve_policy
|
||||
})
|
||||
```
|
||||
|
||||
## 5. 会话管理:持久化 vs 重放 (Persistence)
|
||||
|
||||
OpenWebUI 是无状态的,但 Copilot SDK 是有状态的(保留上下文窗口优化)。
|
||||
|
||||
### 最佳实践:以 `chat_id` 为锚点
|
||||
|
||||
利用 OpenWebUI 提供的 `chat_id` 来决定是 `resume` 还是 `start`。
|
||||
|
||||
1. **Map**: 维护 `Dict[chat_id, session_id]` (内存或数据库)。
|
||||
2. **Flow**:
|
||||
* 请求进来 -> 检查 `chat_id` 是否有对应的 `session_id`。
|
||||
* **有**: 尝试 `client.resume_session(session_id)`。
|
||||
* *注意*:Resume 时必须重新传入 `tools`, `hooks`, `on_permission_request`,因为这些 Python 对象不会被序列化保存。
|
||||
* **无/失败**: 调用 `client.create_session()`,并将新 `session_id` 存入 Map。
|
||||
3. **Fallback**: 如果 Resume 失败(例如后端重启 SDK 进程丢失),回退到 Create 新会话,并可选地将 OpenWebUI 传来的 `messages` 历史以 System Message 或历史插入的方式“重放”进去(虽然 SDK 不直接支持 insert history,但可以通过连续的 `send` 模拟,但这很慢)。
|
||||
* *简易方案*:Resume 失败就作为新对话开始,只带入 System Prompt。
|
||||
|
||||
## 6. 高级 Hook:提示词增强
|
||||
|
||||
利用 `on_user_prompt_submitted` 钩子,可以在不修改用户可见内容的情况下,向 Copilot 注入隐式上下文(例如当前文件内容、Pipe 的元指令)。
|
||||
|
||||
```python
|
||||
async def inject_context_hook(input_data, ctx):
|
||||
user_prompt = input_data["prompt"]
|
||||
|
||||
# 比如:检测到用户在问代码,自动附加上下文
|
||||
additional_context = "Current Language: Python. Framework: OpenWebUI."
|
||||
|
||||
return {
|
||||
"modifiedPrompt": user_prompt, # 可以在这里改写提示词
|
||||
"additionalContext": additional_context # 注入隐藏上下文
|
||||
}
|
||||
|
||||
session = await client.create_session({
|
||||
# ...
|
||||
"hooks": {
|
||||
"on_user_prompt_submitted": inject_context_hook
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**总结开发清单:**
|
||||
|
||||
1. [ ] **Env Auth**: 读取环境变量 -> `CopilotClient`。
|
||||
2. [ ] **Permission Valve**: 实现 `PERMISSIONS_ALLOW_ALL/SHELL` 等 Valves。
|
||||
3. [ ] **Auto-Approve Hook**: 实现 `on_permission_request` 逻辑。
|
||||
4. [ ] **MCP Valve**: 添加 JSON Valve -> `session.mcp_servers`。
|
||||
5. [ ] **Session Map**: 实现 `chat_id` <-> `session_id` 的简单的内存映射。
|
||||
6. [ ] **Resume Logic**: 优先 `resume_session`,并记得并在 resume 时重传 Hook 和 Tools。
|
||||
@@ -1,620 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from copilot import CopilotClient
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger("copilot_sdk_guide")
|
||||
|
||||
DEFAULT_CONTEXT_URLS = [
|
||||
"https://raw.githubusercontent.com/github/copilot-sdk/main/README.md",
|
||||
"https://raw.githubusercontent.com/github/copilot-sdk/main/python/README.md",
|
||||
"https://raw.githubusercontent.com/github/copilot-sdk/main/docs/getting-started.md",
|
||||
"https://raw.githubusercontent.com/github/copilot-cli/main/README.md",
|
||||
"https://raw.githubusercontent.com/github/copilot-cli/main/changelog.md",
|
||||
"https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli",
|
||||
"https://docs.github.com/en/copilot/concepts/agents/about-agent-skills",
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main/README.md",
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main/skills/copilot-sdk/SKILL.md",
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main/instructions/agent-skills.instructions.md",
|
||||
]
|
||||
|
||||
AWESOME_COPILOT_REPO = "github/awesome-copilot"
|
||||
AWESOME_COPILOT_BRANCH = "main"
|
||||
AWESOME_COPILOT_DOC_DIRS = ["docs/", "instructions/"]
|
||||
|
||||
TOPICS = [
|
||||
"MCP Server Integration: JSON-RPC config and SDK hooks",
|
||||
"Agent Manifests: Defining capabilities and permissions programmatically",
|
||||
"Headless Auth: Device Code Flow and credential persistence",
|
||||
"Session Replay vs Resume: Handling stateless frontend history",
|
||||
"Advanced Session Hooks: Intercepting and modifying user prompts",
|
||||
"Workspace Virtualization: Handling CWD for remote/virtual files",
|
||||
"Error Recovery: Handling session disconnects and re-auth",
|
||||
"Confirmation Events: programmatic handling of 'confirmation_required'",
|
||||
"Skills: Conflict resolution and precedence defaults",
|
||||
"Debugging: Tracing JSON-RPC traffic in the SDK",
|
||||
"Billing & Policies: How seat management affects SDK features",
|
||||
]
|
||||
|
||||
QUESTION_TEMPLATES = [
|
||||
"Give a concise overview of {topic}.",
|
||||
"Provide best practices and common pitfalls for {topic}.",
|
||||
"Show a minimal example snippet for {topic}.",
|
||||
"List recommended configuration defaults for {topic}.",
|
||||
"How does {topic} relate to building a custom Agent?",
|
||||
]
|
||||
|
||||
CLI_FOCUS_QUESTIONS = [
|
||||
"How to configure MCP servers in ~/.copilot/config.json for SDK usage?",
|
||||
"What CLI environment variables force 'Agent' mode vs 'Generic' mode?",
|
||||
"Explain the 'confirmation' flow in CLI and how it maps to SDK events.",
|
||||
"Does the CLI support 'dry-run' permission checks for tools?",
|
||||
"What are the undocumented requirements for 'workspace' context updates?",
|
||||
"How does the CLI handle 'device code' re-authentication automatically?",
|
||||
]
|
||||
|
||||
|
||||
def build_questions(max_questions: int) -> List[str]:
|
||||
questions: List[str] = []
|
||||
|
||||
for topic in TOPICS:
|
||||
for template in QUESTION_TEMPLATES:
|
||||
questions.append(template.format(topic=topic))
|
||||
|
||||
questions.extend(CLI_FOCUS_QUESTIONS)
|
||||
|
||||
# De-duplicate while preserving order
|
||||
seen = set()
|
||||
uniq: List[str] = []
|
||||
for q in questions:
|
||||
if q in seen:
|
||||
continue
|
||||
seen.add(q)
|
||||
uniq.append(q)
|
||||
|
||||
return uniq[:max_questions]
|
||||
|
||||
|
||||
def build_deep_dive_prompts() -> List[str]:
|
||||
return [
|
||||
"Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.",
|
||||
"Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.",
|
||||
"Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.",
|
||||
"Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.",
|
||||
"Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.",
|
||||
"Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.",
|
||||
"List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.",
|
||||
]
|
||||
|
||||
|
||||
def load_questions(path: str) -> List[str]:
|
||||
if path.lower().endswith(".json"):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
return [str(x).strip() for x in data if str(x).strip()]
|
||||
raise ValueError("JSON must be an array of strings")
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f.readlines()]
|
||||
return [line for line in lines if line]
|
||||
|
||||
|
||||
def fetch_url(url: str, headers: Optional[dict] = None) -> str:
|
||||
import urllib.request
|
||||
import time
|
||||
|
||||
retries = 3
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
|
||||
for i in range(retries):
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as response:
|
||||
return response.read().decode("utf-8", errors="replace")
|
||||
except Exception as exc:
|
||||
if i == retries - 1:
|
||||
logger.warning(
|
||||
"Failed to fetch %s after %d attempts: %s", url, retries, exc
|
||||
)
|
||||
return ""
|
||||
time.sleep(1 * (i + 1))
|
||||
return ""
|
||||
|
||||
|
||||
def list_repo_markdown_urls(
|
||||
repo: str,
|
||||
branch: str,
|
||||
dir_prefixes: List[str],
|
||||
) -> List[str]:
|
||||
api_url = f"https://api.github.com/repos/{repo}/git/trees/{branch}?recursive=1"
|
||||
headers = {}
|
||||
if os.environ.get("GITHUB_TOKEN"):
|
||||
headers["Authorization"] = f"token {os.environ.get('GITHUB_TOKEN')}"
|
||||
|
||||
try:
|
||||
content = fetch_url(api_url, headers=headers)
|
||||
if not content:
|
||||
return []
|
||||
data = json.loads(content)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to list repo tree: %s", exc)
|
||||
return []
|
||||
|
||||
tree = data.get("tree", []) if isinstance(data, dict) else []
|
||||
urls: List[str] = []
|
||||
for item in tree:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
path = item.get("path", "")
|
||||
if not path or not path.endswith(".md"):
|
||||
continue
|
||||
if any(path.startswith(prefix) for prefix in dir_prefixes):
|
||||
raw = f"https://raw.githubusercontent.com/{repo}/{branch}/{path}"
|
||||
urls.append(raw)
|
||||
return urls
|
||||
|
||||
|
||||
def read_local_sdk_source(max_chars: int = 300000) -> str:
|
||||
"""
|
||||
Locates the installed 'copilot' package and reads its source code.
|
||||
This ensures analysis is based on the actual installed version, not just docs.
|
||||
"""
|
||||
try:
|
||||
import copilot
|
||||
except ImportError:
|
||||
logger.error("Could not import 'copilot' SDK. Is it installed?")
|
||||
return ""
|
||||
|
||||
package_dir = os.path.dirname(copilot.__file__)
|
||||
logger.info(f"Reading SDK source from: {package_dir}")
|
||||
|
||||
source_chunks = []
|
||||
total_chars = 0
|
||||
|
||||
# Prioritize key files that define core logic
|
||||
priority_files = ["client.py", "session.py", "types.py", "events.py", "__init__.py"]
|
||||
|
||||
# First pass: Recursively find all .py files
|
||||
all_py_files = []
|
||||
for root, dirs, files in os.walk(package_dir):
|
||||
if "__pycache__" in root:
|
||||
continue
|
||||
for file in files:
|
||||
if file.endswith(".py"):
|
||||
all_py_files.append(os.path.join(root, file))
|
||||
|
||||
# Sort files: priority files first, then alphabetical
|
||||
def sort_key(path):
|
||||
fname = os.path.basename(path)
|
||||
if fname in priority_files:
|
||||
return (0, priority_files.index(fname))
|
||||
return (1, path)
|
||||
|
||||
all_py_files.sort(key=sort_key)
|
||||
|
||||
for path in all_py_files:
|
||||
rel_path = os.path.relpath(path, os.path.dirname(package_dir))
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Add file delimiter for the model
|
||||
header = f"\n\n# ==================================================\n# SOURCE CODE FILE: {rel_path}\n# ==================================================\n"
|
||||
chunk = header + content
|
||||
|
||||
if total_chars + len(chunk) > max_chars:
|
||||
remaining = max_chars - total_chars
|
||||
if remaining > len(header) + 100:
|
||||
source_chunks.append(
|
||||
chunk[:remaining] + "\n# [TRUNCATED DUE TO LENGTH LIMIT]"
|
||||
)
|
||||
logger.warning(f"Context limit reached. Stopping at {rel_path}")
|
||||
break
|
||||
|
||||
source_chunks.append(chunk)
|
||||
total_chars += len(chunk)
|
||||
logger.info(f"Loaded source file: {rel_path} ({len(content)} chars)")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read source file {path}: {e}")
|
||||
|
||||
return "".join(source_chunks)
|
||||
|
||||
|
||||
def build_context(urls: Iterable[str], max_chars: int) -> str:
|
||||
chunks: List[str] = []
|
||||
remaining = max_chars
|
||||
|
||||
for url in urls:
|
||||
if remaining <= 0:
|
||||
break
|
||||
try:
|
||||
content = fetch_url(url)
|
||||
header = f"[Source: {url}]\n"
|
||||
if len(header) >= remaining:
|
||||
break
|
||||
remaining -= len(header)
|
||||
|
||||
if len(content) > remaining:
|
||||
content = content[:remaining] + "\n[TRUNCATED]\n"
|
||||
remaining = 0
|
||||
else:
|
||||
remaining -= len(content)
|
||||
|
||||
chunks.append(header + content)
|
||||
logger.info("Fetched context: %s", url)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to fetch %s: %s", url, exc)
|
||||
|
||||
return "\n\n".join(chunks)
|
||||
|
||||
|
||||
def write_jsonl(path: str, item: dict) -> None:
|
||||
with open(path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(item, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def write_markdown_header(path: str, title: str, meta: dict) -> None:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(f"# {title}\n\n")
|
||||
for k, v in meta.items():
|
||||
f.write(f"- **{k}**: {v}\n")
|
||||
f.write("\n---\n\n")
|
||||
|
||||
|
||||
def append_markdown_qa(path: str, question: str, answer: str) -> None:
|
||||
with open(path, "a", encoding="utf-8") as f:
|
||||
f.write(f"## Q: {question}\n\n")
|
||||
f.write(f"{answer}\n\n")
|
||||
|
||||
|
||||
def clamp_questions(questions: List[str], max_questions: int) -> List[str]:
|
||||
return questions[: max(1, min(max_questions, 400))]
|
||||
|
||||
|
||||
def print_progress_bar(
|
||||
iteration,
|
||||
total,
|
||||
prefix="",
|
||||
suffix="",
|
||||
decimals=1,
|
||||
length=50,
|
||||
fill="█",
|
||||
printEnd="\r",
|
||||
):
|
||||
"""
|
||||
Call in a loop to create terminal progress bar
|
||||
"""
|
||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||
filledLength = int(length * iteration // total)
|
||||
bar = fill * filledLength + "-" * (length - filledLength)
|
||||
# Clear line extension to handle shrinking suffixes
|
||||
print(f"\r{prefix} |{bar}| {percent}% {suffix}\033[K", end=printEnd)
|
||||
# Print New Line on Complete
|
||||
if iteration == total:
|
||||
print()
|
||||
|
||||
|
||||
async def run_session(
|
||||
model: str,
|
||||
questions: List[str],
|
||||
output_dir: str,
|
||||
context: str,
|
||||
session_id: Optional[str],
|
||||
delay: float,
|
||||
output_lang: str,
|
||||
enable_infinite_sessions: bool,
|
||||
timeout: int,
|
||||
) -> None:
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session_config = {"model": model}
|
||||
if session_id:
|
||||
session_config["session_id"] = session_id
|
||||
if enable_infinite_sessions:
|
||||
session_config["infinite_sessions"] = {
|
||||
"enabled": True,
|
||||
"background_compaction_threshold": 0.8,
|
||||
"buffer_exhaustion_threshold": 0.95,
|
||||
}
|
||||
|
||||
session = await client.create_session(session_config)
|
||||
|
||||
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
jsonl_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.jsonl")
|
||||
md_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.md")
|
||||
|
||||
write_markdown_header(
|
||||
md_path,
|
||||
"GitHub Copilot SDK & CLI 研究报告",
|
||||
{
|
||||
"model": model,
|
||||
"questions": len(questions),
|
||||
"timestamp": timestamp,
|
||||
"language": output_lang,
|
||||
},
|
||||
)
|
||||
|
||||
lang_instruction = "Chinese" if "zh" in output_lang.lower() else "English"
|
||||
|
||||
system_prompt = textwrap.dedent(
|
||||
f"""
|
||||
You are an expert assistant. Focus on GitHub Copilot SDK and GitHub Copilot CLI.
|
||||
|
||||
CRITICAL INSTRUCTION: SOURCE CODE FIRST.
|
||||
You have been provided with the ACTUAL PYTHON SOURCE CODE of the `copilot` SDK in the context.
|
||||
When answering questions:
|
||||
1. FIRST, analyze the provided source code (look for class definitions, type hints, methods).
|
||||
2. THEN, refer to documentation if source code is ambiguous.
|
||||
3. Do NOT hallucinate methods that do not exist in the source code.
|
||||
4. If a feature (like MCP) is not explicitly in the code, explain how to implement it using the available primitives (low-level hooks/events).
|
||||
|
||||
Provide accurate, concise answers in {lang_instruction}. When relevant, include command names,
|
||||
configuration keys, and pitfalls. Use bullet points where useful.
|
||||
|
||||
Output requirements:
|
||||
- Write in {lang_instruction}.
|
||||
- Provide practical code snippets (Python/TypeScript/CLI) when helpful.
|
||||
- Include a short "建议/落地" section for integration into a pipe.
|
||||
- If citing facts from provided context, briefly mention the source URL.
|
||||
"""
|
||||
).strip()
|
||||
|
||||
if context:
|
||||
system_prompt += "\n\nAdditional context:\n" + context
|
||||
|
||||
await session.send_and_wait({"prompt": system_prompt}, timeout=timeout)
|
||||
|
||||
total_q = len(questions)
|
||||
print_progress_bar(0, total_q, prefix="Progress:", suffix="Starting...", length=30)
|
||||
|
||||
for idx, question in enumerate(questions, start=1):
|
||||
# Update progress bar (Asking...)
|
||||
q_short = (question[:40] + "...") if len(question) > 40 else question.ljust(43)
|
||||
print_progress_bar(
|
||||
idx - 1, total_q, prefix="Progress:", suffix=f"Asking: {q_short}", length=30
|
||||
)
|
||||
|
||||
# Log to file/debug only
|
||||
logger.debug("[%s/%s] Asking: %s", idx, total_q, question)
|
||||
|
||||
answer = ""
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = await session.send_and_wait(
|
||||
{"prompt": question}, timeout=timeout
|
||||
)
|
||||
answer = response.data.content if response and response.data else ""
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error asking question (Attempt {attempt+1}/{max_retries}): {e}"
|
||||
)
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
answer = f"Error retrieving answer: {e}"
|
||||
|
||||
write_jsonl(
|
||||
jsonl_path,
|
||||
{
|
||||
"index": idx,
|
||||
"question": question,
|
||||
"answer": answer,
|
||||
"model": model,
|
||||
},
|
||||
)
|
||||
append_markdown_qa(md_path, question, answer)
|
||||
|
||||
# Update progress bar (Done...)
|
||||
print_progress_bar(
|
||||
idx, total_q, prefix="Progress:", suffix=f"Done: {q_short}", length=30
|
||||
)
|
||||
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
await session.destroy()
|
||||
await client.stop()
|
||||
|
||||
logger.info("Saved output to %s and %s", jsonl_path, md_path)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Ask up to 100 Copilot SDK questions via GitHub Copilot SDK",
|
||||
)
|
||||
parser.add_argument("--model", default="gpt-5.2-codex", help="Model to use")
|
||||
parser.add_argument(
|
||||
"--max-questions",
|
||||
type=int,
|
||||
default=100,
|
||||
help="Max number of questions (1-400)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--questions-file",
|
||||
default="",
|
||||
help="Path to .txt or .json list of questions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--context-url",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Additional context URL (repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-default-context",
|
||||
action="store_true",
|
||||
help="Disable default Copilot SDK context URLs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-awesome-copilot-docs",
|
||||
action="store_true",
|
||||
help="Include all markdown files from awesome-copilot/docs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-awesome-copilot-instructions",
|
||||
action="store_true",
|
||||
help="Include all markdown files from awesome-copilot/instructions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-sdk-source",
|
||||
action="store_true",
|
||||
help="Do NOT read local SDK source code (default: reads source)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--session-id",
|
||||
default="",
|
||||
help="Optional custom session ID",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default="",
|
||||
help="Directory to save outputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delay",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Delay between questions (seconds)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-context-chars",
|
||||
type=int,
|
||||
default=400000,
|
||||
help="Max characters of aggregated context (default: 400000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-infinite-sessions",
|
||||
action="store_true",
|
||||
help="Disable infinite sessions (default: enabled)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-lang",
|
||||
default="zh-CN",
|
||||
help="Output language (default: zh-CN)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deep-dive",
|
||||
action="store_true",
|
||||
help="Append deep-dive prompts for more detailed research",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=3600,
|
||||
help="Session request timeout in seconds (default: 3600)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
if args.questions_file:
|
||||
questions = load_questions(args.questions_file)
|
||||
else:
|
||||
# Generate enough questions to cover everything
|
||||
questions = build_questions(9999)
|
||||
|
||||
if args.deep_dive:
|
||||
# Prepend deep dive questions to ensure they are prioritized
|
||||
questions = build_deep_dive_prompts() + questions
|
||||
|
||||
questions = clamp_questions(questions, args.max_questions)
|
||||
if not questions:
|
||||
logger.error("No questions to ask")
|
||||
sys.exit(1)
|
||||
|
||||
context_urls = [] if args.no_default_context else list(DEFAULT_CONTEXT_URLS)
|
||||
|
||||
if args.include_awesome_copilot_docs:
|
||||
context_urls.extend(
|
||||
list_repo_markdown_urls(
|
||||
AWESOME_COPILOT_REPO,
|
||||
AWESOME_COPILOT_BRANCH,
|
||||
["docs/"],
|
||||
)
|
||||
)
|
||||
|
||||
if args.include_awesome_copilot_instructions:
|
||||
context_urls.extend(
|
||||
list_repo_markdown_urls(
|
||||
AWESOME_COPILOT_REPO,
|
||||
AWESOME_COPILOT_BRANCH,
|
||||
["instructions/"],
|
||||
)
|
||||
)
|
||||
|
||||
context_urls.extend(args.context_url or [])
|
||||
|
||||
# 1. Read local source code first (Priority: High)
|
||||
# We allocate up to max_context_chars to source code initially.
|
||||
# The actual usage will likely be less for a typical SDK.
|
||||
source_context = ""
|
||||
source_chars_count = 0
|
||||
if not args.no_sdk_source:
|
||||
source_context = read_local_sdk_source(args.max_context_chars)
|
||||
source_chars_count = len(source_context)
|
||||
logger.info(f"Source context usage: {source_chars_count} chars")
|
||||
|
||||
# 2. Calculate remaining budget for Web Docs (Priority: Secondary)
|
||||
# We ensure we don't exceed the global limit.
|
||||
remaining_chars = max(10000, args.max_context_chars - source_chars_count)
|
||||
logger.info(f"Remaining budget for web docs: {remaining_chars} chars")
|
||||
|
||||
# 3. Fetch remote docs
|
||||
web_context = build_context(context_urls, remaining_chars)
|
||||
|
||||
combined_context = ""
|
||||
# Assemble context in order of authority (Source > Docs)
|
||||
if source_context:
|
||||
combined_context += (
|
||||
"# PRIMARY SOURCE: LOCAL SDK CODE (AUTHORITATIVE)\n"
|
||||
+ source_context
|
||||
+ "\n\n"
|
||||
)
|
||||
if web_context:
|
||||
combined_context += (
|
||||
"# SECONDARY SOURCE: WEB DOCUMENTATION & AWESOME-COPILOT\n" + web_context
|
||||
)
|
||||
|
||||
output_dir = args.output_dir or os.path.join(
|
||||
os.getcwd(), "plugins", "debug", "copilot_sdk_research", "outputs"
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
asyncio.run(
|
||||
run_session(
|
||||
model=args.model,
|
||||
questions=questions,
|
||||
output_dir=output_dir,
|
||||
context=combined_context,
|
||||
session_id=args.session_id or None,
|
||||
delay=args.delay,
|
||||
output_lang=args.output_lang,
|
||||
enable_infinite_sessions=not args.disable_infinite_sessions,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
This is a dummy test file for verifying permission controls.
|
||||
You can delete this file safely.
|
||||
@@ -1,26 +0,0 @@
|
||||
import inspect
|
||||
import json
|
||||
|
||||
try:
|
||||
import copilot
|
||||
from copilot import types
|
||||
|
||||
print(f"Copilot location: {copilot.__file__}")
|
||||
|
||||
print("\n=== Inspecting PermissionRequest types ===")
|
||||
# Look for definition of PermissionRequest or similar
|
||||
|
||||
# In the absence of direct access to the CLI output structure documentation,
|
||||
# we can check if there are type hints or typed dicts in copilot.types
|
||||
|
||||
for name, obj in inspect.getmembers(types):
|
||||
if "Permission" in name or "Request" in name:
|
||||
print(f"\nType: {name}")
|
||||
try:
|
||||
if hasattr(obj, "__annotations__"):
|
||||
print(obj.__annotations__)
|
||||
except:
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
print("copilot module not installed")
|
||||
@@ -1,68 +0,0 @@
|
||||
{"index": 1, "question": "Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.", "answer": "SDK 源码显示 MCP 服务器通过 `create_session`/`resume_session` 的 `mcp_servers` 配置传入并映射为 `mcpServers`,不是 CopilotClient 构造参数(源:本地源码 copilot/types.py / copilot/client.py)。下面是本地 stdio MCP(以 Brave Search 为例,命令/密钥请替换)的 Python 示例。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave_search\": {\n \"type\": \"local\", # 或 \"stdio\"\n \"command\": \"/path/to/brave-search-mcp\", # 替换为你的 MCP server 启动命令\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000, # 毫秒\n }\n },\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 OpenAI 最新公告\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑:\n- `tools`: `[\"*\"]` 表示全量,`[]` 表示不启用\n- `timeout`: 毫秒\n- `command/args/env`: 按你的 MCP server 实际启动方式替换\n\n建议/落地:\n- pipe 内复用同一 session,结束时统一 `client.stop()`", "model": "gpt-5.2-codex"}
|
||||
{"index": 2, "question": "Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.", "answer": "SDK 源码未定义 `tool.confirmation_required` 事件,权限确认由 JSON‑RPC `permission.request` 触发并交给 `on_permission_request` 回调处理,`session.on()` 只能订阅 `tool.user_requested`/`tool.execution_start` 等事件做记录关联(来源:本地源码 copilot/client.py、copilot/session.py、copilot/types.py)。 \n在非交互流里必须提供 `on_permission_request` 自动审批/拒绝,否则默认拒绝且回调异常也会被拒绝。\n\n要点/坑 \n- 配置键: `on_permission_request`(SessionConfig) \n- 权限类型: `shell` / `write` / `mcp` / `read` / `url` \n- 回调异常或缺失 -> `denied-no-approval-rule-and-could-not-request-from-user`\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n def on_event(ev):\n if ev.type == SessionEventType.TOOL_USER_REQUESTED:\n print(f\"tool requested: {ev.data.tool_name} {ev.data.tool_call_id}\")\n elif ev.type == SessionEventType.TOOL_EXECUTION_START:\n print(f\"tool start: {ev.data.tool_name}\")\n\n async def on_permission_request(req, ctx):\n kind = req.get(\"kind\")\n if kind in (\"read\", \"mcp\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny-non-read\"}]}\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": on_permission_request,\n })\n session.on(on_event)\n\n await session.send_and_wait({\"prompt\": \"请调用需要权限的工具\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- pipe 启动时注册审批策略 + `session.on` 记录 `tool_call_id` 做审计", "model": "gpt-5.2-codex"}
|
||||
{"index": 3, "question": "Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.", "answer": "SDK 本身没有 device‑flow 登录 API;可用鉴权原语只有 CopilotClient 的 `github_token`/`use_logged_in_user` 与 `get_auth_status`(源:本地源码 copilot/client.py、copilot/types.py)。 \n下面用 GitHub OAuth Device Flow 原始 HTTP 获取 token、持久化本地并注入 `github_token`(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。\n\n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\n\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nSCOPE = \"read:user\" # 按你的 OAuth App 需求调整\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef http_post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef load_token():\n if TOKEN_PATH.exists():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\")\n return None\n\ndef save_token(token):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(token))\n os.chmod(TOKEN_PATH, 0o600)\n\ndef device_flow():\n code = http_post(\n \"https://github.com/login/device/code\",\n {\"client_id\": CLIENT_ID, \"scope\": SCOPE},\n )\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n\n while True:\n token = http_post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in token:\n return token\n if token.get(\"error\") == \"slow_down\":\n interval += 5\n elif token.get(\"error\") not in (\"authorization_pending\",):\n raise RuntimeError(token)\n time.sleep(interval)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n token = device_flow()\n save_token(token)\n access_token = token[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```\n\n要点/坑 \n- `cli_url` 与 `github_token` 互斥 \n- SDK 不保存凭据 需自行持久化 \n- 轮询需处理 `authorization_pending` / `slow_down`\n\n建议/落地 \n- pipe 启动时读取 token 文件 不存在则走 device flow \n- 用 `github_token` 注入 保持非交互运行", "model": "gpt-5.2-codex"}
|
||||
{"index": 4, "question": "Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.", "answer": "对无状态后端(OpenWebUI)而言,SDK 只支持通过 `resume_session(session_id)` 的状态续接;所谓“Session Replay”需要你自行重放历史 prompt(来源:本地 SDK 源码 copilot/client.py、copilot/session.py)。 \n对比要点(短语): \n- Replay:无状态/易扩展;缺点是耗时耗 tokens、易截断/丢工具状态、可能重复副作用 \n- Resume:省 tokens、保留工具/compaction/`workspace_path` 状态;缺点是需持久化 session_id、依赖会话存活/跨实例一致性。 \n建议/落地:优先持久化 chat_id→session_id 并 resume_session,失效时回退为重放最近 N 轮并重建工具上下文。", "model": "gpt-5.2-codex"}
|
||||
{"index": 5, "question": "Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.", "answer": "SDK 源码里没有“Virtual Workspace”协议或文件系统 provider 接口;会话只识别真实路径(`SessionConfig.working_directory`、`MessageOptions.attachments`)。要在无物理文件情况下供给内容,只能用自定义工具或 MCP 服务器把“读写文件”变成 tool 调用(来源:本地 SDK 源码 copilot/types.py / copilot/client.py)。\n\n可用原语(源码)\n- `SessionConfig.tools` / `available_tools`:自定义工具与白名单\n- `MessageOptions.attachments`:仅支持真实路径文件/目录\n\n方案 A:自定义工具实现 VFS(推荐)\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nVFS = {\"README.md\": \"# Virtual README\\n内容来自内存\\n\"}\n\nclass VfsReadParams(BaseModel):\n path: str = Field(description=\"virtual path\")\n\n@define_tool(name=\"vfs_read\", description=\"Read a virtual file\")\ndef vfs_read(params: VfsReadParams) -> str:\n return VFS.get(params.path, \"\")\n\n@define_tool(name=\"vfs_list\", description=\"List virtual files\")\ndef vfs_list() -> list[str]:\n return sorted(VFS.keys())\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [vfs_read, vfs_list],\n \"available_tools\": [\"vfs_read\", \"vfs_list\"],\n \"system_message\": {\"content\": \"Use vfs_* tools for all file access.\"},\n })\n await session.send_and_wait({\"prompt\": \"打开 README.md\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n方案 B:MCP 服务器(本地 stdio,VFS 由你实现)\n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"vfs\": {\n \"type\": \"local\",\n \"command\": \"python\",\n \"args\": [\"-m\", \"my_vfs_mcp_server\"],\n \"tools\": [\"*\"],\n }\n },\n})\n```\n\n注意/坑 \n- `available_tools` 只保留你的 VFS 工具以强制使用 \n- 内置文件工具只读真实文件;要兼容只能落地临时文件 \n- 大文件需分块返回,避免上下文爆炸 \n\n建议/落地 \n- pipe 内维护 VFS 存储(DB/Redis/内存) \n- 启动 session 时注入 `system_message` + `available_tools` 强制 VFS 入口", "model": "gpt-5.2-codex"}
|
||||
{"index": 6, "question": "Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.", "answer": "SDK 没有独立“Agent Manifest”文件格式,等价于 `create_session` 的 `custom_agents`(CustomAgentConfig),并用 `tools`/`available_tools` 约束高权限工具,权限由 `on_permission_request` 处理(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n示例如下(manifest 为 Python dict,可序列化为 JSON 配置)。\n\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass AdminShellParams(BaseModel):\n cmd: str = Field(description=\"approved shell command\")\n\n@define_tool(name=\"admin_shell\", description=\"Run an approved shell command\")\ndef admin_shell(params: AdminShellParams) -> str:\n # TODO: 执行受控命令(白名单/沙箱)\n return f\"approved: {params.cmd}\"\n\nclass AdminWriteParams(BaseModel):\n path: str = Field(description=\"target path\")\n content: str = Field(description=\"content\")\n\n@define_tool(name=\"admin_write\", description=\"Write to privileged store\")\ndef admin_write(params: AdminWriteParams) -> str:\n # TODO: 受控写入\n return \"ok\"\n\nasync def on_permission_request(req, _ctx):\n if req.get(\"kind\") in (\"shell\", \"write\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny\"}]}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"ops-admin\",\n \"display_name\": \"Ops Admin\",\n \"description\": \"Privileged ops agent\",\n \"prompt\": \"Use only admin_* tools for privileged ops.\",\n \"tools\": [\"admin_shell\", \"admin_write\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [admin_shell, admin_write],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"admin_shell\", \"admin_write\"],\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"使用 ops-admin 执行受控操作\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑 \n- session.tools 未注册 -> agent.tools 不可用 \n- custom_agents.tools 仅写名称 \n- 非交互缺少 on_permission_request -> 默认拒绝 \n- SDK 无显式 agent 选择参数 -> 需在 prompt 指定 \n\n建议/落地 \n- pipe 启动时加载 manifest 注入 custom_agents + available_tools + on_permission_request", "model": "gpt-5.2-codex"}
|
||||
{"index": 7, "question": "List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.", "answer": "基于本地 SDK 源码 `copilot/types.py` 的 SessionConfig,影响 Agent 行为/人格的相关参数如下(来源:本地 SDK 源码)。 \n- `system_message`: `append`/`replace`;`replace` 会移除 SDK guardrails(风险) \n- `custom_agents`: `prompt`/`description`/`display_name`/`tools`/`mcp_servers`/`infer`;`tools` 仅名称且必须在 `tools` 注册 \n- `tools` / `available_tools` / `excluded_tools`: 能力与行为边界;`available_tools` 优先于 `excluded_tools` \n- `hooks`: `on_user_prompt_submitted`/`on_pre_tool_use`/`on_post_tool_use`/`on_session_start`/`on_session_end`/`on_error_occurred`,可改写 prompt/工具参数/结果 \n- `on_permission_request`: 非交互审批策略;缺失或异常 => 拒绝 \n- `on_user_input_request`: 启用/处理 `ask_user` \n- `mcp_servers`: 注入外部工具集 \n- `skill_directories` / `disabled_skills`: 技能启用/禁用 \n- `model` / `provider`: 模型与 BYOK 影响输出风格与能力 \n- `working_directory`: 上下文范围 \n- `infinite_sessions`: compaction 阈值影响记忆保留 \n- `config_dir`: CLI 配置/状态目录,行为取决于 CLI 侧配置 \n\n建议/落地:在 pipe 启动时固定 `system_message` + `available_tools/excluded_tools` + `hooks`,并持久化 session_id 以便 `resume_session`。", "model": "gpt-5.2-codex"}
|
||||
{"index": 8, "question": "Give a concise overview of MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "SDK 中 MCP 通过 create_session/resume_session 的 mcp_servers 传入(映射到 JSON‑RPC 的 mcpServers),支持 local/stdio 与 http/sse,两类配置字段分别为 type/command/args/env/cwd/timeout/tools 或 type/url/headers/timeout/tools(来源:本地 SDK 源码)。 \nSDK hooks 使用 hooks(on_pre_tool_use/on_post_tool_use/on_user_prompt_submitted/on_session_start/on_session_end/on_error_occurred) 进行拦截与附加上下文,权限需 on_permission_request 对 kind='mcp' 明确批准,且 session.on 可审计 tool.execution_* 事件中的 mcp_server_name/mcp_tool_name。 \n示例见下,建议/落地:pipe 中启用 on_permission_request、限制 tools 白名单并记录 MCP 事件。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_permission_request(req, _):\n return {\"kind\": \"approved\"} if req.get(\"kind\") == \"mcp\" else {\n \"kind\": \"denied-by-rules\",\n \"rules\": [{\"kind\": \"deny\"}],\n }\n\nasync def on_pre_tool_use(input, _):\n if input.get(\"toolName\", \"\").startswith(\"mcp.\"):\n return {\"additionalContext\": \"MCP tool call observed\"}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000,\n }\n },\n \"hooks\": {\"on_pre_tool_use\": on_pre_tool_use},\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 Copilot SDK 新闻\"})\n await client.stop()\n\nasyncio.run(main())\n```", "model": "gpt-5.2-codex"}
|
||||
{"index": 9, "question": "Provide best practices and common pitfalls for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 配置通过 `create_session`/`resume_session` 的 `mcp_servers` 进入 JSON‑RPC `mcpServers`,并通过 `on_permission_request` 与 `hooks` 控制工具审批与行为(来源:本地 SDK 源码) \n- 最佳实践:local/stdio 用 `type/command/args/env/cwd/timeout/tools`;remote http/sse 用 `type/url/headers/timeout/tools`;`tools` 尽量白名单、必要时 `[\"*\"]`;`timeout` 毫秒;`hooks.on_pre_tool_use` 做改参/拒绝,`session.on` 记录 `tool.execution_*` 与 `mcp_server_name`/`mcp_tool_name` \n- 常见坑:把 MCP 配置放在 CopilotClient;缺少 `on_permission_request`(kind=\"mcp\")导致默认拒绝;`tools: []` 等于禁用全部;设置 `available_tools` 却未包含 MCP 工具名导致被过滤 \n\n建议/落地:pipe 中固定审批策略 + 统一 MCP 配置模板 + 审计日志", "model": "gpt-5.2-codex"}
|
||||
{"index": 10, "question": "Show a minimal example snippet for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 通过 `mcp_servers` 传入并用 `hooks` 处理工具调用(来源:本地 SDK 源码)。最小示例: \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n \"hooks\": {\n \"on_pre_tool_use\": lambda input, _: {\"additionalContext\": \"MCP tool call\"},\n },\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 搜索 Copilot SDK\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:固定 `on_permission_request`(kind=\"mcp\")并限制 `tools` 白名单。", "model": "gpt-5.2-codex"}
|
||||
{"index": 11, "question": "List recommended configuration defaults for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "推荐默认值(MCP JSON‑RPC 配置,来源:本地 SDK 源码):local/stdio 服务器用 `type=\"local\"/\"stdio\"`(远程用 `http`/`sse`)、`tools` 白名单(无则 `\"*\"`)、`timeout=30000` 毫秒、`env` 仅放密钥、`cwd` 指向受控目录。 \nHooks 默认:`on_pre_tool_use` 追加上下文并可设 `permissionDecision`(ask/allow)、必要时改参,`on_post_tool_use` 记录结果,`on_error_occurred` 设置 retry/skip。 \n建议/落地:非交互场景配置 `on_permission_request` 仅放行 `kind=\"mcp\"`,否则拒绝;工具名不确定时先不设 `available_tools`,避免误过滤。", "model": "gpt-5.2-codex"}
|
||||
{"index": 12, "question": "How does MCP Server Integration: JSON-RPC config and SDK hooks relate to building a custom Agent?", "answer": "SDK 源码中,自定义 Agent 通过 `SessionConfig.custom_agents` 定义,`CustomAgentConfig` 支持 `mcp_servers`,因此“Agent 绑定 MCP”就是在 agent config 里声明 `mcp_servers`,底层在 `create_session` 时转成 JSON‑RPC 的 `mcpServers`(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \nHooks 是会话级拦截器(`on_pre_tool_use`/`on_post_tool_use` 等),对 MCP 工具同样生效;权限确认需 `on_permission_request` 审批 `kind=\"mcp\"`。 \n建议/落地:pipe 中为目标 Agent 绑定 MCP + 审批钩子,示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"custom_agents\": [{\n \"name\": \"search\",\n \"prompt\": \"Use MCP for search.\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n }],\n \"hooks\": {\"on_pre_tool_use\": lambda i, _: {\"additionalContext\": \"mcp\"}},\n \"on_permission_request\": lambda r, _: {\"kind\": \"approved\"},\n})\n```", "model": "gpt-5.2-codex"}
|
||||
{"index": 13, "question": "Give a concise overview of Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立的 Agent Manifest 文件,等价做法是用 `SessionConfig.custom_agents` 描述 agent,并用 `tools`/`available_tools`/`excluded_tools` 限定能力。 \n权限通过 `on_permission_request` 与 `hooks`(如 `on_pre_tool_use`)在会话级控制,且 `custom_agents.tools` 只能写工具名并需在 `SessionConfig.tools` 注册。 \n建议/落地:pipe 启动时加载 manifest→注入 `custom_agents` + `available_tools` + `on_permission_request` 固化权限边界。", "model": "gpt-5.2-codex"}
|
||||
{"index": 14, "question": "Provide best practices and common pitfalls for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立 Manifest 文件,等价于 `SessionConfig.custom_agents` + `tools/available_tools/excluded_tools` + `on_permission_request` + `hooks` 来定义能力与权限(来源:本地 SDK 源码)。 \n最佳实践:`custom_agents.tools` 只写已在 `SessionConfig.tools` 注册的名称,配合 `available_tools` 白名单与 `on_permission_request` 精准放行,必要时用 `hooks.on_pre_tool_use` 做参数校验/审计,`system_message` 选 append 以保留 guardrails。 \n常见坑:工具未注册或名称不一致、缺少 `on_permission_request` 导致默认拒绝、误用 `system_message` replace 去掉安全限制、期待 agent 自动被选中或跨实例不持久 `session_id`;建议/落地:pipe 启动时加载 manifest→注入 `custom_agents/available_tools/on_permission_request` 并持久化 `session_id`。", "model": "gpt-5.2-codex"}
|
||||
{"index": 15, "question": "Show a minimal example snippet for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "最小示例(本地 SDK 源码:`custom_agents` + `tools` + `available_tools` + `on_permission_request`): \n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass PingParams(BaseModel):\n text: str = Field(description=\"text to echo\")\n\n@define_tool(name=\"priv_ping\", description=\"Privileged echo\")\ndef priv_ping(params: PingParams) -> str:\n return f\"ok: {params.text}\"\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"priv-agent\",\n \"prompt\": \"Only use priv_ping for actions.\",\n \"tools\": [\"priv_ping\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [priv_ping],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"priv_ping\"],\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"调用 priv_ping\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:保持工具名一致并白名单化,非交互务必提供 `on_permission_request`。", "model": "gpt-5.2-codex"}
|
||||
{"index": 16, "question": "List recommended configuration defaults for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "基于本地 SDK 源码(copilot/types.py / copilot/client.py),推荐默认值如下: \n- custom_agents:name 必填、prompt 必填、tools 仅名称、infer=True,display_name/description 可选 \n- available_tools=白名单、excluded_tools=空、system_message=append \n- on_permission_request:除明确允许的 kind 其余拒绝;hooks.on_pre_tool_use 做审计/参数校验(其余 hooks 按需) \n建议/落地:pipe 启动时加载 manifest→注册 tools + available_tools + on_permission_request,并持久化 session_id。", "model": "gpt-5.2-codex"}
|
||||
{"index": 17, "question": "How does Agent Manifests: Defining capabilities and permissions programmatically relate to building a custom Agent?", "answer": "“Agent Manifest”在 SDK 里就是 `SessionConfig.custom_agents` 的配置对象,用它定义自定义 Agent 的 `name/prompt/tools/mcp_servers/infer`,并在同一 session 里用 `tools` 注册实际工具、用 `available_tools/excluded_tools` 限定能力(来源:本地 SDK 源码)。 \n权限不在 manifest 内,而是由会话级 `on_permission_request` 和 hooks(如 `on_pre_tool_use`)统一控制,且对所有 Agent 生效。 \n建议/落地:pipe 启动时把 manifest 转成 `custom_agents` 并同步注册工具 + 白名单,再配置 `on_permission_request` 作为权限策略。", "model": "gpt-5.2-codex"}
|
||||
{"index": 18, "question": "Give a concise overview of Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 仅提供 `CopilotClient` 的 `github_token/use_logged_in_user` 与 `get_auth_status()`,没有内置 device‑flow API(源码:copilot/client.py、copilot/types.py)。 \nHeadless Device Flow 需走 GitHub OAuth Device Flow HTTP 端点获取 token(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),将 token 持久化(权限 600)并通过 `CopilotClient({\"github_token\": token})` 注入,且与 `cli_url` 互斥。 \n建议/落地:启动时读 token 文件,不存在则执行 device flow 并保存,再启动客户端。\n\n```python\ntoken = load_token() or device_flow() # 调用 device code + access_token 端点\nsave_token(token) # chmod 600\nclient = CopilotClient({\"github_token\": token[\"access_token\"]})\nawait client.start()\n```", "model": "gpt-5.2-codex"}
|
||||
{"index": 19, "question": "Provide best practices and common pitfalls for Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 不内置 device flow,只能用 GitHub OAuth Device Flow HTTP 换取 token 并通过 CopilotClient({github_token}) 注入,且与 cli_url 互斥(源码:copilot/client.py、copilot/types.py)。 \n最佳实践/坑:- token 文件 chmod 600 或 OS keychain;- 记录到期与刷新策略,启动时用 get_auth_status 验证;- 轮询需处理 authorization_pending/slow_down;- 未持久化、use_logged_in_user 与 github_token 同时使用或与 cli_url 混用会失败(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。 \n建议/落地:pipe 启动时读 token 文件,不存在则走 device flow 并保存,失败时回退重新授权。", "model": "gpt-5.2-codex"}
|
||||
{"index": 20, "question": "Show a minimal example snippet for Headless Auth: Device Code Flow and credential persistence.", "answer": "最小示例:用 GitHub Device Flow(端点说明见 https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)拿 token,持久化后注入 CopilotClient({github_token}). \n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef device_flow():\n code = post(\"https://github.com/login/device/code\", {\"client_id\": CLIENT_ID, \"scope\": \"read:user\"})\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n while True:\n tok = post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in tok:\n return tok\n if tok.get(\"error\") == \"slow_down\":\n interval += 5\n elif tok.get(\"error\") != \"authorization_pending\":\n raise RuntimeError(tok)\n time.sleep(interval)\n\ndef load_token():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\") if TOKEN_PATH.exists() else None\n\ndef save_token(tok):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(tok))\n os.chmod(TOKEN_PATH, 0o600)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n tok = device_flow()\n save_token(tok)\n access_token = tok[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- 启动先读 token 文件;缺失再 device flow \n- token 文件权限 600 \n- 失效则重新授权", "model": "gpt-5.2-codex"}
|
||||
{"index": 21, "question": "List recommended configuration defaults for Headless Auth: Device Code Flow and credential persistence.", "answer": "基于本地 SDK 源码(`github_token` 与 `cli_url` 互斥)与 GitHub Device Flow 文档(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),推荐默认值: \n- Token 持久化:`~/.config/<app>/copilot_token.json` 或系统密钥链,文件权限 600 \n- Client 选项:仅 `github_token`,`use_logged_in_user` 留空/False,禁用 `cli_url` \n- 轮询:使用 device code 返回的 `interval`(缺省 5s),处理 `authorization_pending`/`slow_down` \n- 启动校验:`get_auth_status()` 失败即重新授权 \n\n建议/落地:启动先读 token 文件 → 无则 device flow → 保存 → `client.start()`", "model": "gpt-5.2-codex"}
|
||||
{"index": 22, "question": "How does Headless Auth: Device Code Flow and credential persistence relate to building a custom Agent?", "answer": "Headless Auth 只负责拿到并持久化 `github_token`,让 `CopilotClient` 能在无交互环境启动会话并加载 `custom_agents`;因此它是自定义 Agent 能稳定运行的前置条件,而不是 Agent 定义本身(来源:本地 SDK 源码)。 \n建议/落地:在无状态后端启动时读取 token 文件(不存在就走 device flow)→ `CopilotClient({\"github_token\": token})` → `create_session(custom_agents=...)`。", "model": "gpt-5.2-codex"}
|
||||
{"index": 23, "question": "Give a concise overview of Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码(copilot/client.py、copilot/session.py),Resume 是 `resume_session(session_id)` 保留会话状态,Replay 只能由应用重放历史 prompt。优缺点:Resume 省 tokens/保工具与 compaction 状态但依赖持久化 session_id;Replay 无状态易扩展但成本高且易丢副作用控制。建议/落地:无状态前端请保存 session_id 并优先 Resume,失效时回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||
{"index": 24, "question": "Provide best practices and common pitfalls for Session Replay vs Resume: Handling stateless frontend history.", "answer": "最佳实践:在无状态前端每次请求携带 session_id 并优先 `resume_session`,同时恢复 tools/hooks/on_permission_request/system_message 以保持行为一致(来源:本地 SDK 源码)。 \n常见坑:Replay 会重复执行工具且耗 tokens;Resume 未重新注册工具或权限处理器会出现 tool not supported/默认拒绝;会话失效未回退。 \n建议/落地:持久化 chat_id→session_id,resume 失败时仅回放最近 N 轮并禁用/幂等化有副作用的工具。", "model": "gpt-5.2-codex"}
|
||||
{"index": 25, "question": "Show a minimal example snippet for Session Replay vs Resume: Handling stateless frontend history.", "answer": "来源:本地 SDK 源码(copilot/client.py, copilot/session.py),最小示例如下(优先 Resume,失败则 Replay)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def handle_request(session_id: str | None, history: list[str], latest: str):\n client = CopilotClient()\n await client.start()\n\n try:\n # Resume: 复用会话状态\n if not session_id:\n raise RuntimeError(\"no session_id\")\n session = await client.resume_session(session_id)\n except Exception:\n # Replay: 逐条重放历史\n session = await client.create_session({\"model\": \"gpt-5\"})\n session_id = session.session_id\n for prompt in history:\n await session.send_and_wait({\"prompt\": prompt})\n\n response = await session.send_and_wait({\"prompt\": latest})\n await client.stop()\n return response.data.content, session_id\n\n# asyncio.run(handle_request(session_id, history, latest_prompt))\n```\n建议/落地:持久化 session_id,resume 失败时仅回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||
{"index": 26, "question": "List recommended configuration defaults for Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码,推荐默认配置如下(Resume / Replay): \n- Resume:持久化 session_id;`resume_session({...})` 传回 tools、hooks、on_permission_request、working_directory、streaming、provider、mcp_servers、custom_agents、skill_directories、disabled_skills;`disable_resume` 保持默认 False \n- Replay:`create_session({...})` 传回 model、system_message、tools、available_tools/excluded_tools、hooks、on_permission_request、working_directory、provider、mcp_servers、custom_agents、infinite_sessions(默认启用);仅回放最近 N 轮;默认拒绝 write/shell 或用只读白名单", "model": "gpt-5.2-codex"}
|
||||
{"index": 27, "question": "How does Session Replay vs Resume: Handling stateless frontend history relate to building a custom Agent?", "answer": "Resume 会保留 session_id 关联的 custom_agents/工具状态,Replay 需要每次 create_session 时重新注入 custom_agents + tools,否则 persona/能力会丢失(来源:本地 SDK 源码)。对于无状态前端,Resume 更可靠且省 token,但要持久化 session_id 并在 resume 时重新注册 tools/hooks;Replay 则需回放历史并保证相同 custom_agents 配置。建议/落地:pipe 优先 resume_session,失败再 replay 最近 N 轮并重建 custom_agents/tools/权限策略。", "model": "gpt-5.2-codex"}
|
||||
{"index": 28, "question": "Give a concise overview of Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "拦截/修改用户提示要用 `SessionHooks.on_user_prompt_submitted`(不是 `session.on` 事件订阅),`input.prompt` 可改为 `output.modifiedPrompt` 并附加 `additionalContext`/`suppressOutput`,且需在 `create_session`/`resume_session` 传入 `hooks` 才会触发 `hooks.invoke`(来源:本地 SDK 源码 copilot/types.py、copilot/session.py、copilot/client.py)。 \n```python\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(input, _ctx):\n return {\"modifiedPrompt\": input[\"prompt\"].strip(), \"additionalContext\": \"normalize whitespace\"}\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n})\n```\n建议/落地:在 pipe 入口统一做 prompt 清洗/拼接上下文,并记录修改前后用于审计。", "model": "gpt-5.2-codex"}
|
||||
{"index": 29, "question": "Provide best practices and common pitfalls for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最佳实践:在 create_session/resume_session 传入 hooks.on_user_prompt_submitted,返回 {modifiedPrompt, additionalContext, suppressOutput},修改保持幂等、最小化、可审计(来源:本地 SDK 源码 copilot/types.py、copilot/client.py、copilot/session.py)。 \n常见坑:忘记传 hooks 或 resume 时未重新传、handler 抛异常会被吞并返回 None、返回非 dict 或过度改写提示导致无效/偏航。 \n建议/落地:在 pipe 入口统一注册该 hook 并记录修改前后,保持 system_message 为 append。", "model": "gpt-5.2-codex"}
|
||||
{"index": 30, "question": "Show a minimal example snippet for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最小示例(来源:本地 SDK 源码): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(hook_input, _ctx):\n return {\n \"modifiedPrompt\": hook_input[\"prompt\"].strip(),\n \"additionalContext\": \"normalized\",\n }\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n })\n await session.send_and_wait({\"prompt\": \" hello \"})\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:在 pipe 入口统一注册该 hook,并记录修改前后以便审计。", "model": "gpt-5.2-codex"}
|
||||
{"index": 31, "question": "List recommended configuration defaults for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "- 推荐默认值(来源:本地 SDK 源码 copilot/types.py):`hooks={\"on_user_prompt_submitted\": handler}`,handler 默认返回 `{\"modifiedPrompt\": input[\"prompt\"], \"additionalContext\": \"\", \"suppressOutput\": False}` 或 `None`(不改写)。 \n- `create_session`/`resume_session` 都要传同一 hooks;`system_message` 保持 append 以保留 guardrails。 \n- 建议/落地:pipe 入口统一注册该 hook 并记录原/改提示用于审计。", "model": "gpt-5.2-codex"}
|
||||
{"index": 32, "question": "How does Advanced Session Hooks: Intercepting and modifying user prompts relate to building a custom Agent?", "answer": "Hooks 是会话级拦截器,对所有自定义 Agent 都生效;因此它是定义 Agent 行为边界和输入规范化的“外层控制”,而不是 Agent Manifest 本身(来源:本地 SDK 源码 copilot/types.py、copilot/session.py)。 \n在自定义 Agent 场景下,`on_user_prompt_submitted` 可统一清洗/增强用户指令,再由 custom_agents 的 prompt 决定具体风格。 \n建议/落地:pipe 启动时同时注入 custom_agents + hooks,确保 resume 时同样注册 hooks。", "model": "gpt-5.2-codex"}
|
||||
{"index": 33, "question": "Give a concise overview of Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 只有 `SessionConfig.working_directory` 来约束 CWD,`attachments` 也要求真实路径,没有“虚拟工作区/FS provider”协议(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。要处理远程/虚拟文件,需用自定义工具或 MCP 服务器实现 VFS 读写,或把内容落地到 working_directory 下的临时文件再交给模型。建议/落地:pipe 固定 working_directory 映射到会话工作区,统一走 vfs_* 工具/MCP,且在 on_permission_request 限制 write/shell。", "model": "gpt-5.2-codex"}
|
||||
{"index": 34, "question": "Provide best practices and common pitfalls for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 没有虚拟工作区/FS provider,仅支持 `SessionConfig.working_directory` 与真实路径 `attachments`(来源:本地 SDK 源码),因此远程/虚拟文件需落地或用自定义工具/MCP 提供内容。 \n最佳实践:固定 working_directory 到可控目录/会话 workspace,VFS 工具分块返回,配合 `available_tools`/`on_permission_request` 限权;常见坑:直接传虚拟路径/URL、working_directory 不存在、resume 未重注册工具/权限、一次性返回过大内容导致上下文截断。 \n建议/落地:pipe 入口做 remote→临时文件或 vfs_* 映射,并持久化路径索引。", "model": "gpt-5.2-codex"}
|
||||
{"index": 35, "question": "Show a minimal example snippet for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 仅提供 `SessionConfig.working_directory` 与真实路径附件,没有虚拟 FS/provider(来源:本地 SDK 源码 copilot/types.py)。最小做法是把远程内容落地到 CWD 下再作为附件传入: \n```python\nimport asyncio\nimport pathlib\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n workdir = \"/tmp/owui\"\n pathlib.Path(workdir).mkdir(parents=True, exist_ok=True)\n remote_content = \"...\"\n path = f\"{workdir}/remote.txt\"\n pathlib.Path(path).write_text(remote_content)\n\n session = await client.create_session({\"model\": \"gpt-5\", \"working_directory\": workdir})\n await session.send_and_wait({\n \"prompt\": \"Summarize remote.txt\",\n \"attachments\": [{\"type\": \"file\", \"path\": path}],\n })\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 入口固定 working_directory,落地临时文件后立即处理,并用 `on_permission_request` 限制 write/shell。", "model": "gpt-5.2-codex"}
|
||||
{"index": 36, "question": "List recommended configuration defaults for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "推荐默认值(来源:本地 SDK 源码 copilot/types.py): \n- `working_directory`: 指向可控的会话目录(如 session.workspace_path 或 /tmp/<app>/<session_id>) \n- `available_tools`: 只允许 vfs_* 或只读工具,避免任意 shell/write \n- `on_permission_request`: 默认拒绝 write/shell,仅放行 read/mcp \n- `attachments`: 仅真实路径文件;远程内容先落地 \n\n建议/落地:pipe 入口统一映射 remote→临时文件或 VFS 工具,并持久化映射表。", "model": "gpt-5.2-codex"}
|
||||
{"index": 37, "question": "How does Workspace Virtualization: Handling CWD for remote/virtual files relate to building a custom Agent?", "answer": "SDK 没有虚拟文件系统协议,custom agent 只能依赖 `working_directory` 和真实路径附件,因此处理远程/虚拟文件需通过自定义工具/MCP 并在 agent prompt/available_tools 中强制使用(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n这使“工作区虚拟化”成为 agent 能力与权限设计的一部分,需要 `available_tools`/`on_permission_request`/hooks 来限定访问范围。 \n建议/落地:pipe 中固定 working_directory 到会话目录,注入 vfs_* 工具白名单并在 prompt 明确使用 VFS。", "model": "gpt-5.2-codex"}
|
||||
{"index": 38, "question": "Give a concise overview of Error Recovery: Handling session disconnects and re-auth.", "answer": "本地 SDK 源码中没有显式“重连”API,断线通常表现为请求异常或 `SessionEventType.SESSION_ERROR`,需要 `stop()/force_stop()` 后重新 `start()` 并 `resume_session`(失败则 replay)(来源:本地 SDK 源码)。 \n认证可用 `get_auth_status()` 检测,未认证时重新创建 `CopilotClient({\"github_token\": token})`;若 `cli_url` 连接外部服务器则认证由外部处理(来源:本地 SDK 源码)。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def safe_send(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n session.on(lambda ev: ev.type == SessionEventType.SESSION_ERROR and print(ev.data.message))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n finally:\n await client.stop()\n```\n\n建议/落地:持久化 session_id 与 token,优先 resume,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||
{"index": 39, "question": "Provide best practices and common pitfalls for Error Recovery: Handling session disconnects and re-auth.", "answer": "最佳实践(基于本地 SDK 源码):监听 `SESSION_ERROR`/捕获请求异常后 `stop()/force_stop()`→重新 `start()`→`resume_session`(失败再 replay),启动时用 `get_auth_status()` 校验并在失效时重建 `CopilotClient({\"github_token\": ...})`,同时在 resume 时重新注册 tools/hooks/权限处理器。 \n常见坑:把 `cli_url` 与 `github_token` 混用、忘记重注册工具导致 `tool not supported`、缺少 `on_permission_request` 导致默认拒绝、以及不回退到 replay 造成会话中断。 \n建议/落地:无状态后端持久化 `session_id+token`、实现幂等的重试/回放策略并记录错误上下文用于告警。", "model": "gpt-5.2-codex"}
|
||||
{"index": 40, "question": "Show a minimal example snippet for Error Recovery: Handling session disconnects and re-auth.", "answer": "最小示例(本地 SDK 源码:copilot/client.py、copilot/session.py): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def send_with_recovery(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n finally:\n await client.stop()\n```\n建议/落地:持久化 token+session_id,resume 失败再重建并回放必要历史。", "model": "gpt-5.2-codex"}
|
||||
{"index": 41, "question": "List recommended configuration defaults for Error Recovery: Handling session disconnects and re-auth.", "answer": "推荐默认值(字段来源:本地 SDK 源码):CopilotClientOptions—auto_start=True、log_level='info'、use_stdio=True(非 cli_url)、github_token=持久化 token;SessionConfig/ResumeSessionConfig—hooks.on_error_occurred=handler、on_permission_request=非交互审批、working_directory 固定、mcp_servers/custom_agents/streaming 与首次一致、可固定 config_dir 并持久化 session_id。 \n常见坑:cli_url 与 github_token 互斥、resume 时未重传 tools/hooks 导致 tool not supported 或默认拒绝。 \n建议/落地:pipe 保存 token+session_id,断线后先 resume_session,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||
{"index": 42, "question": "How does Error Recovery: Handling session disconnects and re-auth relate to building a custom Agent?", "answer": "错误恢复与自定义 Agent 强相关:Agent 定义与工具/权限处理器都是 session 级配置,断线或重认证后必须重建客户端并重新注入 `custom_agents`、`tools`、`hooks`、`on_permission_request`,否则能力/人格会丢失(来源:本地 SDK 源码)。 \n建议/落地:持久化 session_id+token,重连优先 `resume_session` 并重新注册上述配置,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||
{"index": 43, "question": "Give a concise overview of Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码里没有独立的 `confirmation_required` 事件;权限确认是 JSON‑RPC `permission.request`,通过 `SessionConfig.on_permission_request` 回调处理(缺失或异常会默认拒绝)。 \n`session.on()` 只能用于审计/关联工具事件(如 `tool.user_requested`、`tool.execution_start`),不能替代确认逻辑。", "model": "gpt-5.2-codex"}
|
||||
{"index": 44, "question": "Provide best practices and common pitfalls for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码未定义 `confirmation_required` 事件,确认流程必须用 `SessionConfig.on_permission_request` 处理,缺失或异常会默认拒绝(来源:本地 SDK 源码)。 \n最佳实践/坑: \n- ✅ 对 `kind` 建立白名单策略(如仅允许 `read/mcp`),记录决策并用 `session.on` 仅做审计关联 `tool.user_requested` \n- ✅ `resume_session` 时重新注册 `on_permission_request`/hooks \n- ❌ 误以为 `session.on()` 可完成确认、返回非规范 dict 或抛异常导致全部拒绝", "model": "gpt-5.2-codex"}
|
||||
{"index": 45, "question": "Show a minimal example snippet for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 里没有 `confirmation_required` 事件,需用 `SessionConfig.on_permission_request` 处理权限确认。 \n最小示例: \n```python\nfrom copilot import CopilotClient\n\nclient = CopilotClient()\nawait client.start()\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\n```\n建议/落地:在 pipe 中统一注册 on_permission_request,并在 resume_session 时重复注册。", "model": "gpt-5.2-codex"}
|
||||
{"index": 46, "question": "List recommended configuration defaults for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "推荐默认值(SDK 无 confirmation_required 事件,改用 on_permission_request,来源:本地 SDK 源码):- 对 kind 建白名单(常见只放行 read/mcp)- 未命中默认返回 denied-by-rules 并给 rules - resume_session 时重注册 on_permission_request/hooks。 \n建议/落地:pipe 启动即注入 on_permission_request 并记录审批日志。", "model": "gpt-5.2-codex"}
|
||||
{"index": 47, "question": "How does Confirmation Events: programmatic handling of 'confirmation_required' relate to building a custom Agent?", "answer": "SDK 没有 `confirmation_required` 事件,权限确认由 `create_session`/`resume_session` 的 `on_permission_request` 处理,且对同一 session 的所有 custom agent 生效(来源:本地 SDK 源码)。 \n建议/落地:在 pipe 启动时注入 `on_permission_request` 白名单策略并在 resume 时重注册,以约束高权限工具。", "model": "gpt-5.2-codex"}
|
||||
{"index": 48, "question": "Give a concise overview of Skills: Conflict resolution and precedence defaults.", "answer": "在本地 SDK 源码中,Skills 只通过 SessionConfig 的 `skill_directories` 与 `disabled_skills` 暴露,未定义冲突解析/优先级规则,因此行为取决于 CLI 运行时实现(来源:本地 SDK 源码 copilot/types.py)。 \n为可控性,建议确保技能名唯一、仅配置单一目录或由你自行按顺序拼接目录,并用 `disabled_skills` 明确屏蔽同名/不应启用的技能。 \n建议/落地:在无状态后端每次 `create_session`/`resume_session` 时显式传入同一份 `skill_directories` 与 `disabled_skills`,避免因默认加载顺序差异导致行为漂移。", "model": "gpt-5.2-codex"}
|
||||
{"index": 49, "question": "Provide best practices and common pitfalls for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 源码仅暴露 SessionConfig/ResumeSessionConfig 的 `skill_directories` 和 `disabled_skills`,未定义冲突解析或优先级,行为由 CLI 侧决定(来源:本地 SDK 源码 copilot/types.py)。 \n最佳实践:技能名全局唯一、只配置单一目录或显式排序,并用 `disabled_skills` 禁用冲突项;常见坑是依赖默认加载顺序或 resume 未重传这两个字段导致行为漂移。 \n建议/落地:在 pipe 启动与 resume 时统一注入同一份 `skill_directories`/`disabled_skills` 并记录变更。", "model": "gpt-5.2-codex"}
|
||||
{"index": 50, "question": "Show a minimal example snippet for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 里技能只通过 `skill_directories` 与 `disabled_skills` 配置,冲突/优先级未在源码定义,需自行避免同名。 \n最小示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"skill_directories\": [\"/opt/skills\"],\n \"disabled_skills\": [\"legacy_skill\"],\n})\n``` \n建议:技能名唯一并在 `resume_session` 时重传这两项。", "model": "gpt-5.2-codex"}
|
||||
{"index": 51, "question": "List recommended configuration defaults for Skills: Conflict resolution and precedence defaults.", "answer": "推荐默认值(基于 SDK 仅暴露 `skill_directories`/`disabled_skills`): \n- `skill_directories`: 明确设置为单一目录(或你自己控制顺序的列表),不要依赖 CLI 默认搜索顺序 \n- `disabled_skills`: 默认空列表;如存在同名/旧技能则显式禁用 \n- Resume/Replay:始终复用同一份配置以避免“隐式优先级”漂移", "model": "gpt-5.2-codex"}
|
||||
{"index": 52, "question": "How does Skills: Conflict resolution and precedence defaults relate to building a custom Agent?", "answer": "Skills 是会话级配置(`skill_directories`/`disabled_skills`),会影响所有自定义 Agent 的可用能力与提示行为;SDK 不定义冲突优先级,因此需要你在构建自定义 Agent 时确保技能命名唯一并显式禁用冲突项(来源:本地 SDK 源码)。 \n建议/落地:为 custom agent 固定同一份 skills 配置,并在 resume 时重复传入,避免行为漂移。", "model": "gpt-5.2-codex"}
|
||||
{"index": 53, "question": "Give a concise overview of Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "本地 SDK 源码里 JsonRpcClient 没有内置 JSON‑RPC trace,只能通过 `CopilotClient({\"log_level\":\"debug\"})` 打开 CLI 侧日志(非原始包)(来源:本地 SDK 源码)。 \n要抓实际 JSON‑RPC 流量,可 monkey‑patch `JsonRpcClient._send_message`/`_read_message` 或包装 stdio 进行 tee,最小示例如下。 \n建议/落地:在 pipe 中开启 patch 并对日志做脱敏与采样。 \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, message):\n print(\">>\", message)\n return await orig_send(self, message)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```", "model": "gpt-5.2-codex"}
|
||||
{"index": 54, "question": "Provide best practices and common pitfalls for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "源码里 JsonRpcClient 没有内置 trace,只能用 `CopilotClient({\"log_level\":\"debug\"})` 查看 CLI 侧日志或 monkey‑patch `JsonRpcClient._send_message/_read_message` 做 tee(来源:本地 SDK 源码)。 \n最佳实践:仅在调试启用、对 github_token/API key/内容脱敏、采样/限流、只读不改消息并注意线程安全;常见坑:记录完整 prompt/工具参数导致泄露,或同步打印阻塞 IO 造成超时/断连。", "model": "gpt-5.2-codex"}
|
||||
{"index": 55, "question": "Show a minimal example snippet for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "最小示例(monkey‑patch `JsonRpcClient` 打印收发): \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, msg):\n print(\">>\", msg)\n return await orig_send(self, msg)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```\n建议:仅在调试启用并脱敏日志。", "model": "gpt-5.2-codex"}
|
||||
{"index": 56, "question": "List recommended configuration defaults for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "推荐默认值(基于 SDK 源码):- 生产环境关闭 trace;- 调试时 `CopilotClient({\"log_level\":\"debug\"})`;- 如需 JSON‑RPC 报文,采用临时 monkey‑patch 并启用脱敏/采样;- 不修改消息内容,避免线程阻塞。 \n建议/落地:pipe 中通过环境变量开关调试与采样率。", "model": "gpt-5.2-codex"}
|
||||
{"index": 57, "question": "How does Debugging: Tracing JSON-RPC traffic in the SDK relate to building a custom Agent?", "answer": "JSON‑RPC trace 能看到 custom_agents 在 `session.create/session.resume` 的实际 payload,以及后续 `tool.call`/`permission.request`/`hooks.invoke` 是否按预期触发,从而验证自定义 Agent 的能力与权限边界(来源:本地 SDK 源码)。 \n建议/落地:仅在开发环境开启并脱敏日志;用它核对 customAgents/tools/权限决策与 MCP 配置。", "model": "gpt-5.2-codex"}
|
||||
{"index": 58, "question": "Give a concise overview of Billing & Policies: How seat management affects SDK features.", "answer": "- 访问与政策:SDK 依赖 Copilot CLI,需有效 Copilot 订阅且组织/企业未禁用 CLI,否则无法使用(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- 计费与认证:每次 prompt 计入 Copilot premium requests 配额,PAT 需 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)\n\n建议/落地:启动先 `get_auth_status()`,失败时提示分配席位/启用 CLI 或更新 token", "model": "gpt-5.2-codex"}
|
||||
{"index": 59, "question": "Provide best practices and common pitfalls for Billing & Policies: How seat management affects SDK features.", "answer": "最佳实践:确认用户/组织席位允许 Copilot CLI/SDK 使用,启动前用 `get_auth_status()` 预检,使用 PAT 时确保具备 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n常见坑:未分配席位或组织/企业策略禁用 CLI 导致 SDK 失效、PAT 权限不足或 premium requests 用尽引发认证/配额错误(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在 pipe 启动时做席位与鉴权检测并给出明确提示/自助链接,记录每次请求计费与配额告警阈值以便限流。", "model": "gpt-5.2-codex"}
|
||||
{"index": 60, "question": "Show a minimal example snippet for Billing & Policies: How seat management affects SDK features.", "answer": "基于 SDK README,最小示例是在启动时用 `get_auth_status()` 检查是否已分配席位/可用认证(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n auth = await client.get_auth_status()\n if not auth.isAuthenticated:\n raise RuntimeError(\"Copilot 未认证/未分配席位,请先登录或分配权限\")\n\n # ...正常创建 session...\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 启动先做该检查,失败时提示分配席位或更新 PAT 权限。", "model": "gpt-5.2-codex"}
|
||||
{"index": 61, "question": "List recommended configuration defaults for Billing & Policies: How seat management affects SDK features.", "answer": "推荐默认值:CopilotClient 用 `use_logged_in_user=True`、`auto_start=True`、`log_level=\"info\"`;如用 PAT 则用 `github_token` 注入并确保具备 Copilot Requests 权限,且启动前 `get_auth_status()` 失败即按席位/策略限制处理(来源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:pipe 启动先做 auth 预检并给出续订/分配席位提示,避免会话中途才触发权限或配额错误。", "model": "gpt-5.2-codex"}
|
||||
{"index": 62, "question": "How does Billing & Policies: How seat management affects SDK features relate to building a custom Agent?", "answer": "席位与组织策略决定 Copilot CLI/SDK 是否可用,因此自定义 Agent 只是同一会话上的配置,前提仍是有有效订阅与允许的策略,否则 Agent 无法运行(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在创建自定义 Agent 前先 `get_auth_status()` 预检并对配额/政策限制做降级提示。", "model": "gpt-5.2-codex"}
|
||||
{"index": 63, "question": "How to configure MCP servers in ~/.copilot/config.json for SDK usage?", "answer": "我在 CLI 文档里未看到 `~/.copilot/config.json` 的 MCP 配置结构;CLI 仅提供 `/mcp add|edit|delete|show` 命令管理配置(来源:Copilot CLI 文档)。 \n因此建议用 CLI 写入配置,并在 SDK 中用 `config_dir` 指向该配置目录;或直接在 SDK 的 `create_session(mcp_servers=...)` 传入。 \n如果你确认存在 `~/.copilot/config.json` 的具体 schema,请提供示例或路径,我可按你的文件格式给出精确配置。", "model": "gpt-5.2-codex"}
|
||||
{"index": 64, "question": "What CLI environment variables force 'Agent' mode vs 'Generic' mode?", "answer": "CLI 文档未提到任何“通过环境变量强制 Agent 模式/Generic 模式”的开关(源:Copilot CLI 文档)。 \n已知与模式相关的是命令/参数,例如 `--agent` 选择自定义 agent,或交互内 `/agent` 切换;但不是环境变量。 \n如果你指的是内部实验开关,请提供具体变量名或版本线索,我可继续追索。", "model": "gpt-5.2-codex"}
|
||||
{"index": 65, "question": "Explain the 'confirmation' flow in CLI and how it maps to SDK events.", "answer": "- CLI:工具权限弹窗确认,可用 `/allow-all` 或 `/yolo` 自动批准。(源: https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- SDK:对应 JSON‑RPC `permission.request`→`on_permission_request` 返回 `approved/denied-*`,`session.on()` 只用于审计 tool.execution_*,无独立 confirmation 事件。(源: 本地 SDK 源码 copilot/client.py、copilot/session.py、copilot/types.py)\n\n示例:\n```python\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nclient = CopilotClient()\nawait client.start()\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\nsession.on(lambda ev: ev.type == SessionEventType.TOOL_EXECUTION_START and print(ev.data.tool_name))\n```\n\n建议/落地:非交互模式实现 `on_permission_request` 并记录 `tool.execution_*` 事件。", "model": "gpt-5.2-codex"}
|
||||
{"index": 66, "question": "Does the CLI support 'dry-run' permission checks for tools?", "answer": "CLI 文档未提到“dry-run”权限检查,只有正常确认与 `/allow-all`、`/yolo` 自动批准(源:https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。SDK 侧仅有 `permission.request`→`on_permission_request` 与 `hooks.on_pre_tool_use` 的拦截/拒绝(本地 SDK 源码),不提供不执行的试运行;建议/落地:pipe 中用 `on_permission_request` 记录/拒绝高风险工具并在 hooks 做审计。", "model": "gpt-5.2-codex"}
|
||||
{"index": 67, "question": "What are the undocumented requirements for 'workspace' context updates?", "answer": "CLI 文档与本地 SDK 源码都未提到“workspace context updates”的未公开要求;SDK 只暴露 `working_directory` 与 `session.workspace_path`,以及 `attachments` 真实路径(来源:本地 SDK 源码)。 \n如果你指的是 CLI 的“工作区/会话文件”更新规则,请提供具体页面或日志片段,我可以进一步定位。", "model": "gpt-5.2-codex"}
|
||||
{"index": 68, "question": "How does the CLI handle 'device code' re-authentication automatically?", "answer": "CLI 文档未说明“自动 device code 重新认证”的具体机制;只提到未登录时使用 `/login`,PAT 可通过 `GH_TOKEN/GITHUB_TOKEN` 提供(源:Copilot CLI 文档)。因此无法确认是否有自动 re-auth 或缓存刷新策略。 \n若需无交互场景,请使用 SDK 的 `github_token` 注入并自行实现 device flow 轮询与持久化。", "model": "gpt-5.2-codex"}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,186 +0,0 @@
|
||||
"""
|
||||
Test multi-rule permission control pattern (VSCode-style)
|
||||
Tests ordered rule matching like VSCode's chat.tools.terminal.autoApprove
|
||||
SAFE VERSION: Uses harmless commands (echo/ls) only. No rm, no git.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from copilot import CopilotClient
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_multi_rule_handler(rules_json: str):
|
||||
"""
|
||||
Build permission handler with ordered rules (VSCode-style)
|
||||
"""
|
||||
try:
|
||||
rules = json.loads(rules_json) if rules_json else {}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("Invalid rules JSON: %s", e)
|
||||
rules = {}
|
||||
|
||||
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||
kind = request.get("kind")
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
|
||||
# Always approve read and url
|
||||
if kind in ("read", "url"):
|
||||
return {"kind": "approved"}
|
||||
|
||||
# For shell commands, apply ordered rules
|
||||
if kind == "shell" and command:
|
||||
for pattern, approved in rules.items():
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
if approved:
|
||||
logger.info(
|
||||
"✅ Approved (rule match): pattern=%r command=%r",
|
||||
pattern,
|
||||
command,
|
||||
)
|
||||
return {"kind": "approved"}
|
||||
else:
|
||||
logger.warning(
|
||||
"❌ Denied (rule match): pattern=%r command=%r",
|
||||
pattern,
|
||||
command,
|
||||
)
|
||||
return {
|
||||
"kind": "denied-by-rules",
|
||||
"rules": [
|
||||
{"kind": "multi-rule-deny", "pattern": pattern}
|
||||
],
|
||||
}
|
||||
except re.error as exc:
|
||||
logger.error("Invalid pattern %r: %s", pattern, exc)
|
||||
continue
|
||||
|
||||
# Default deny for shell without matching rule
|
||||
logger.warning("❌ Denied (no matching rule): command=%r", command)
|
||||
return {"kind": "denied-by-rules", "rules": [{"kind": "no-rule-match"}]}
|
||||
|
||||
return on_permission_request
|
||||
|
||||
|
||||
async def run_test(model: str, rules_json: str, prompt: str) -> Tuple[bool, str]:
|
||||
"""Run a single test and return (approved, response)"""
|
||||
try:
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = await client.create_session(
|
||||
{
|
||||
"model": model,
|
||||
"on_permission_request": build_multi_rule_handler(rules_json),
|
||||
}
|
||||
)
|
||||
|
||||
# Set a short timeout
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
session.send_and_wait({"prompt": prompt}), timeout=15.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("Test Timed Out")
|
||||
return (False, "Timeout")
|
||||
finally:
|
||||
await client.stop()
|
||||
|
||||
content = response.data.content
|
||||
# Heuristics to detect denial in response
|
||||
denied_keywords = [
|
||||
"不允许",
|
||||
"无法",
|
||||
"对不起",
|
||||
"Sorry",
|
||||
"can't",
|
||||
"cannot",
|
||||
"not have permission",
|
||||
"denied",
|
||||
]
|
||||
is_denied = any(kw in content for kw in denied_keywords)
|
||||
|
||||
return (not is_denied, content)
|
||||
except Exception as e:
|
||||
logger.error("Test failed: %s", e)
|
||||
return (False, str(e))
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", default="gpt-4.1", help="Model ID")
|
||||
args = parser.parse_args()
|
||||
|
||||
# LOGIC TEST RULES
|
||||
# 1. Deny "echo secret" explicitly (Specific Deny)
|
||||
# 2. Allow "echo" anything else (General Allow)
|
||||
# 3. Allow "ls" (General Allow)
|
||||
# 4. Deny everything else (Default Deny)
|
||||
logic_test_rules = {
|
||||
"^echo\\s+secret": False, # Higher priority: Deny specific echo
|
||||
"^echo": True, # Lower priority: Allow general echo
|
||||
"^ls": True, # Allow ls
|
||||
".*": False, # Deny everything else (e.g. whoami)
|
||||
}
|
||||
|
||||
rules_json = json.dumps(logic_test_rules)
|
||||
|
||||
test_cases = [
|
||||
# 1. Matches Rule 2 (^echo) -> Should be Approved
|
||||
("Allowed: Normal Echo", "请执行: echo 'hello world'", True),
|
||||
# 2. Matches Rule 3 (^ls) -> Should be Approved
|
||||
("Allowed: LS", "请执行: ls -la", True),
|
||||
# 3. Matches Rule 1 (^echo\s+secret) -> Should be DENIED
|
||||
# This proves the ORDER matters. If it matched Rule 2 first, it would be allowed.
|
||||
("Denied: Restricted Echo", "请执行: echo secret data", False),
|
||||
# 4. Matches Rule 4 (.*) -> Should be DENIED
|
||||
("Denied: Unknown Command", "请执行: whoami", False),
|
||||
]
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info("Safe Multi-Rule Logic Test (Proving Precedence)")
|
||||
logger.info("Rules: %s", json.dumps(logic_test_rules, indent=2))
|
||||
logger.info("=" * 80)
|
||||
|
||||
results = []
|
||||
for i, (name, prompt, expected) in enumerate(test_cases, 1):
|
||||
logger.info("\n[Test %d/%d] %s", i, len(test_cases), name)
|
||||
logger.info(" Prompt: %s", prompt)
|
||||
|
||||
approved, response = await run_test(args.model, rules_json, prompt)
|
||||
passed = approved == expected
|
||||
|
||||
status = "✅ PASS" if passed else "❌ FAIL"
|
||||
results.append((name, passed))
|
||||
|
||||
logger.info(
|
||||
" Expected: %s, Got: %s - %s",
|
||||
"Approved" if expected else "Denied",
|
||||
"Approved" if approved else "Denied",
|
||||
status,
|
||||
)
|
||||
logger.info(" Response: %s", response[:100].replace("\n", " "))
|
||||
|
||||
# Summary
|
||||
logger.info("\n" + "=" * 80)
|
||||
logger.info("Test Summary")
|
||||
logger.info("=" * 80)
|
||||
passed_count = sum(1 for _, passed in results if passed)
|
||||
for name, passed in results:
|
||||
logger.info("%s %s", "✅" if passed else "❌", name)
|
||||
logger.info("Total: %d/%d tests passed", passed_count, len(results))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,202 +0,0 @@
|
||||
"""
|
||||
Comprehensive Permission Control Test Suite
|
||||
Tests all permission control scenarios for GitHub Copilot SDK
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from copilot import CopilotClient
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str):
|
||||
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||
kind = request.get("kind")
|
||||
# Shell requests use 'fullCommandText' not 'command'
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
|
||||
if allow_all:
|
||||
logger.info("✅ Approved (allow-all): kind=%s command=%r", kind, command)
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind in ("read", "url"):
|
||||
logger.info("✅ Approved (safe): kind=%s", kind)
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind == "shell":
|
||||
if allow_shell:
|
||||
logger.info("✅ Approved (allow-shell): command=%r", command)
|
||||
return {"kind": "approved"}
|
||||
|
||||
if pattern and command:
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
logger.info(
|
||||
"✅ Approved (regex match): pattern=%r command=%r",
|
||||
pattern,
|
||||
command,
|
||||
)
|
||||
return {"kind": "approved"}
|
||||
except re.error as exc:
|
||||
logger.error("Invalid regex pattern: %s (%s)", pattern, exc)
|
||||
|
||||
logger.warning("❌ Denied: kind=%s command=%r", kind, command)
|
||||
return {"kind": "denied-by-rules", "rules": [{"kind": "test-suite"}]}
|
||||
|
||||
return on_permission_request
|
||||
|
||||
|
||||
async def run_test(
|
||||
model: str, allow_all: bool, allow_shell: bool, pattern: str, prompt: str
|
||||
) -> Tuple[bool, str]:
|
||||
"""Run a single test and return (success, response)"""
|
||||
try:
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = await client.create_session(
|
||||
{
|
||||
"model": model,
|
||||
"on_permission_request": build_permission_handler(
|
||||
allow_all=allow_all,
|
||||
allow_shell=allow_shell,
|
||||
pattern=pattern,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
response = await session.send_and_wait({"prompt": prompt})
|
||||
await client.stop()
|
||||
|
||||
content = response.data.content
|
||||
# Check if response indicates success or denial
|
||||
denied_keywords = [
|
||||
"不允许",
|
||||
"无法",
|
||||
"对不起",
|
||||
"Sorry",
|
||||
"can't",
|
||||
"cannot",
|
||||
"not have permission",
|
||||
]
|
||||
is_denied = any(kw in content for kw in denied_keywords)
|
||||
|
||||
return (not is_denied, content)
|
||||
except Exception as e:
|
||||
logger.error("Test failed with exception: %s", e)
|
||||
return (False, str(e))
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Comprehensive permission control test suite."
|
||||
)
|
||||
parser.add_argument("--model", default="gpt-4.1", help="Model ID for testing.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Test cases: (name, allow_all, allow_shell, pattern, prompt, expected_approved)
|
||||
test_cases = [
|
||||
("Default Deny Shell", False, False, "", "请执行: ls -la", False),
|
||||
("Allow All", True, False, "", "请执行: ls -la", True),
|
||||
("Allow Shell", False, True, "", "请执行: pwd", True),
|
||||
("Regex Match: ^ls", False, False, "^ls", "请执行: ls -la", True),
|
||||
("Regex No Match: ^ls vs pwd", False, False, "^ls", "请执行: pwd", False),
|
||||
(
|
||||
"Regex Complex: ^(ls|pwd|echo)",
|
||||
False,
|
||||
False,
|
||||
"^(ls|pwd|echo)",
|
||||
"请执行: pwd",
|
||||
True,
|
||||
),
|
||||
(
|
||||
"Regex Complex No Match: git",
|
||||
False,
|
||||
False,
|
||||
"^(ls|pwd|echo)",
|
||||
"请执行: git status",
|
||||
False,
|
||||
),
|
||||
(
|
||||
"Read Permission (Always Allow)",
|
||||
False,
|
||||
False,
|
||||
"",
|
||||
"Read the file: README.md",
|
||||
True,
|
||||
),
|
||||
]
|
||||
|
||||
results = []
|
||||
logger.info("=" * 80)
|
||||
logger.info("Starting Comprehensive Permission Control Test Suite")
|
||||
logger.info("Model: %s", args.model)
|
||||
logger.info("=" * 80)
|
||||
|
||||
for i, (name, allow_all, allow_shell, pattern, prompt, expected) in enumerate(
|
||||
test_cases, 1
|
||||
):
|
||||
logger.info("\n[Test %d/%d] %s", i, len(test_cases), name)
|
||||
logger.info(
|
||||
" Config: allow_all=%s, allow_shell=%s, pattern=%r",
|
||||
allow_all,
|
||||
allow_shell,
|
||||
pattern,
|
||||
)
|
||||
logger.info(" Prompt: %s", prompt)
|
||||
|
||||
approved, response = await run_test(
|
||||
args.model, allow_all, allow_shell, pattern, prompt
|
||||
)
|
||||
passed = approved == expected
|
||||
|
||||
status = "✅ PASS" if passed else "❌ FAIL"
|
||||
results.append((name, passed))
|
||||
|
||||
logger.info(
|
||||
" Expected: %s, Got: %s - %s",
|
||||
"Approved" if expected else "Denied",
|
||||
"Approved" if approved else "Denied",
|
||||
status,
|
||||
)
|
||||
logger.info(
|
||||
" Response: %s",
|
||||
response[:100] + "..." if len(response) > 100 else response,
|
||||
)
|
||||
|
||||
# Summary
|
||||
logger.info("\n" + "=" * 80)
|
||||
logger.info("Test Summary")
|
||||
logger.info("=" * 80)
|
||||
passed_count = sum(1 for _, passed in results if passed)
|
||||
total_count = len(results)
|
||||
|
||||
for name, passed in results:
|
||||
logger.info("%s %s", "✅" if passed else "❌", name)
|
||||
|
||||
logger.info("-" * 80)
|
||||
logger.info(
|
||||
"Total: %d/%d tests passed (%.1f%%)",
|
||||
passed_count,
|
||||
total_count,
|
||||
100 * passed_count / total_count,
|
||||
)
|
||||
|
||||
if passed_count == total_count:
|
||||
logger.info("🎉 All tests passed!")
|
||||
else:
|
||||
logger.warning("⚠️ Some tests failed. Please review the logs.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,90 +0,0 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from copilot import CopilotClient
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str):
|
||||
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||
kind = request.get("kind")
|
||||
# Shell requests use 'fullCommandText' not 'command'
|
||||
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||
logger.info("permission.request FULL: %s", request)
|
||||
logger.info("permission.request kind=%s command=%r", kind, command)
|
||||
|
||||
if allow_all:
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind in ("read", "url"):
|
||||
return {"kind": "approved"}
|
||||
|
||||
if kind == "shell":
|
||||
if allow_shell:
|
||||
return {"kind": "approved"}
|
||||
|
||||
if pattern and command:
|
||||
try:
|
||||
if re.match(pattern, command):
|
||||
return {"kind": "approved"}
|
||||
except re.error as exc:
|
||||
logger.error("Invalid regex pattern: %s (%s)", pattern, exc)
|
||||
|
||||
return {"kind": "denied-by-rules", "rules": [{"kind": "debug-shell-pattern"}]}
|
||||
|
||||
return on_permission_request
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Test shell permission regex with GitHub Copilot SDK."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pattern", default="", help="Regex pattern for auto-approving shell commands."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-shell", action="store_true", help="Auto-approve all shell commands."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-all", action="store_true", help="Auto-approve all permission requests."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt",
|
||||
default="请执行: ls -la",
|
||||
help="Prompt to trigger a shell tool request.",
|
||||
)
|
||||
parser.add_argument("--model", default="gpt-5-mini", help="Model ID for testing.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
session = await client.create_session(
|
||||
{
|
||||
"model": args.model,
|
||||
"on_permission_request": build_permission_handler(
|
||||
allow_all=args.allow_all,
|
||||
allow_shell=args.allow_shell,
|
||||
pattern=args.pattern,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
logger.info("Sending prompt: %s", args.prompt)
|
||||
response = await session.send_and_wait({"prompt": args.prompt})
|
||||
logger.info("Response: %s", response.data.content)
|
||||
|
||||
await client.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,359 +0,0 @@
|
||||
"""
|
||||
title: UI Language Debugger
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/openwebui-extensions
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.1.0
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxwYXRoIGQ9Im01IDggNiA2Ii8+CiAgPHBhdGggZD0ibTQgMTQgNi02IDItMiIvPgogIDxwYXRoIGQ9Ik0yIDVoMTIiLz4KICA8cGF0aCBkPSJNNyAyaDEiLz4KICA8cGF0aCBkPSJtMjIgMjItNS0xMC01IDEwIi8+CiAgPHBhdGggZD0iTTE0IDE4aDYiLz4KPC9zdmc+Cg==
|
||||
description: Debug UI language detection in the browser console and on-page panel.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HTML_WRAPPER_TEMPLATE = """
|
||||
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="{user_language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
#main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* STYLES_INSERTION_POINT */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<!-- CONTENT_INSERTION_POINT -->
|
||||
</div>
|
||||
<!-- SCRIPTS_INSERTION_POINT -->
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CONTENT_TEMPLATE = """
|
||||
<div class="lang-debug-card" id="lang-debug-card-{unique_id}">
|
||||
<div class="lang-debug-header">
|
||||
🧭 UI Language Debugger
|
||||
</div>
|
||||
<div class="lang-debug-body">
|
||||
<div class="lang-debug-row"><span>python.ui_language</span><code id="lang-py-{unique_id}">{python_language}</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.lang</span><code id="lang-html-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.getAttribute('lang')</span><code id="lang-attr-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dir</span><code id="lang-dir-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.body.lang</span><code id="lang-body-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.language</span><code id="lang-nav-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.languages</span><code id="lang-navs-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.language</span><code id="lang-store-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.locale</span><code id="lang-locale-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.i18n</span><code id="lang-i18n-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.settings</span><code id="lang-settings-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dataset</span><code id="lang-dataset-{unique_id}">-</code></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
STYLE_TEMPLATE = """
|
||||
.lang-debug-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.lang-debug-header {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.lang-debug-body {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.lang-debug-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.9em;
|
||||
color: #1f2937;
|
||||
}
|
||||
.lang-debug-row code {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
color: #0f172a;
|
||||
}
|
||||
"""
|
||||
|
||||
SCRIPT_TEMPLATE = """
|
||||
<script>
|
||||
(function() {{
|
||||
const uniqueId = "{unique_id}";
|
||||
const get = (id) => document.getElementById(id + '-' + uniqueId);
|
||||
|
||||
const safe = (value) => {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
if (Array.isArray(value)) return value.join(", ");
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const safeJson = (value) => {
|
||||
try {
|
||||
return value ? JSON.stringify(JSON.parse(value)) : "-";
|
||||
} catch (e) {
|
||||
return value ? String(value) : "-";
|
||||
}
|
||||
};
|
||||
|
||||
const settingsRaw = localStorage.getItem('settings');
|
||||
const i18nRaw = localStorage.getItem('i18n');
|
||||
const localeRaw = localStorage.getItem('locale');
|
||||
|
||||
const payload = {{
|
||||
htmlLang: document.documentElement.lang,
|
||||
htmlAttr: document.documentElement.getAttribute('lang'),
|
||||
htmlDir: document.documentElement.dir,
|
||||
bodyLang: document.body ? document.body.lang : "",
|
||||
navigatorLanguage: navigator.language,
|
||||
navigatorLanguages: navigator.languages,
|
||||
localStorageLanguage: localStorage.getItem('language'),
|
||||
localStorageLocale: localeRaw,
|
||||
localStorageI18n: i18nRaw,
|
||||
localStorageSettings: settingsRaw,
|
||||
htmlDataset: document.documentElement.dataset,
|
||||
}};
|
||||
|
||||
get('lang-html').textContent = safe(payload.htmlLang);
|
||||
get('lang-attr').textContent = safe(payload.htmlAttr);
|
||||
get('lang-dir').textContent = safe(payload.htmlDir);
|
||||
get('lang-body').textContent = safe(payload.bodyLang);
|
||||
get('lang-nav').textContent = safe(payload.navigatorLanguage);
|
||||
get('lang-navs').textContent = safe(payload.navigatorLanguages);
|
||||
get('lang-store').textContent = safe(payload.localStorageLanguage);
|
||||
get('lang-locale').textContent = safe(payload.localStorageLocale);
|
||||
get('lang-i18n').textContent = safeJson(payload.localStorageI18n);
|
||||
get('lang-settings').textContent = safeJson(payload.localStorageSettings);
|
||||
get('lang-dataset').textContent = safe(payload.htmlDataset);
|
||||
|
||||
console.group('🧭 UI Language Debugger');
|
||||
console.log(payload);
|
||||
console.groupEnd();
|
||||
}})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
SHOW_STATUS: bool = Field(
|
||||
default=True,
|
||||
description="Whether to show operation status updates.",
|
||||
)
|
||||
SHOW_DEBUG_LOG: bool = Field(
|
||||
default=True,
|
||||
description="Whether to print debug logs in the browser console.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
elif isinstance(__user__, dict):
|
||||
user_data = __user__
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", ""),
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
self, body: dict, __metadata__: Optional[dict] = None
|
||||
) -> Dict[str, str]:
|
||||
chat_id = ""
|
||||
message_id = ""
|
||||
|
||||
if isinstance(body, dict):
|
||||
chat_id = body.get("chat_id", "")
|
||||
message_id = body.get("id", "")
|
||||
|
||||
if not chat_id or not message_id:
|
||||
body_metadata = body.get("metadata", {})
|
||||
if isinstance(body_metadata, dict):
|
||||
if not chat_id:
|
||||
chat_id = body_metadata.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = body_metadata.get("message_id", "")
|
||||
|
||||
if __metadata__ and isinstance(__metadata__, dict):
|
||||
if not chat_id:
|
||||
chat_id = __metadata__.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = __metadata__.get("message_id", "")
|
||||
|
||||
return {
|
||||
"chat_id": str(chat_id).strip(),
|
||||
"message_id": str(message_id).strip(),
|
||||
}
|
||||
|
||||
async def _emit_status(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
description: str,
|
||||
done: bool = False,
|
||||
):
|
||||
if self.valves.SHOW_STATUS and emitter:
|
||||
await emitter(
|
||||
{"type": "status", "data": {"description": description, "done": done}}
|
||||
)
|
||||
|
||||
async def _emit_debug_log(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
title: str,
|
||||
data: dict,
|
||||
):
|
||||
if not self.valves.SHOW_DEBUG_LOG or not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
js_code = f"""
|
||||
(async function() {{
|
||||
console.group("🛠️ {title}");
|
||||
console.log({json.dumps(data, ensure_ascii=False)});
|
||||
console.groupEnd();
|
||||
}})();
|
||||
"""
|
||||
|
||||
await emitter({"type": "execute", "data": {"code": js_code}})
|
||||
except Exception as e:
|
||||
logger.error("Error emitting debug log: %s", e, exc_info=True)
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html: str,
|
||||
new_content: str,
|
||||
new_styles: str = "",
|
||||
new_scripts: str = "",
|
||||
user_language: str = "en-US",
|
||||
) -> str:
|
||||
if not existing_html:
|
||||
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||
else:
|
||||
base_html = existing_html
|
||||
|
||||
if "<!-- CONTENT_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- CONTENT_INSERTION_POINT -->",
|
||||
f"{new_content}\n <!-- CONTENT_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
if new_styles and "/* STYLES_INSERTION_POINT */" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"/* STYLES_INSERTION_POINT */",
|
||||
f"{new_styles}\n /* STYLES_INSERTION_POINT */",
|
||||
)
|
||||
|
||||
if new_scripts and "<!-- SCRIPTS_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- SCRIPTS_INSERTION_POINT -->",
|
||||
f"{new_scripts}\n <!-- SCRIPTS_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
return base_html
|
||||
|
||||
async def action(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[Dict[str, Any]] = None,
|
||||
__event_emitter__: Optional[Any] = None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
) -> Optional[dict]:
|
||||
await self._emit_status(__event_emitter__, "Detecting UI language...", False)
|
||||
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: user context",
|
||||
user_ctx,
|
||||
)
|
||||
|
||||
ui_language = ""
|
||||
if __event_call__:
|
||||
try:
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": "return (localStorage.getItem('locale') || localStorage.getItem('language') || (navigator.languages && navigator.languages[0]) || navigator.language || document.documentElement.lang || '')",
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: execute response",
|
||||
{"response": response},
|
||||
)
|
||||
if isinstance(response, dict) and "value" in response:
|
||||
ui_language = response.get("value", "") or ""
|
||||
elif isinstance(response, str):
|
||||
ui_language = response
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to read UI language from frontend: %s", e, exc_info=True
|
||||
)
|
||||
|
||||
unique_id = f"lang_{int(__import__('time').time() * 1000)}"
|
||||
content_html = CONTENT_TEMPLATE.replace("{unique_id}", unique_id).replace(
|
||||
"{python_language}", ui_language or "-"
|
||||
)
|
||||
script_html = SCRIPT_TEMPLATE.replace("{unique_id}", unique_id)
|
||||
script_html = script_html.replace("{{", "{").replace("}}", "}")
|
||||
|
||||
final_html = self._merge_html(
|
||||
"",
|
||||
content_html,
|
||||
STYLE_TEMPLATE,
|
||||
script_html,
|
||||
"en",
|
||||
)
|
||||
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] = (
|
||||
body["messages"][-1].get("content", "") + "\n\n" + html_embed_tag
|
||||
)
|
||||
|
||||
await self._emit_status(__event_emitter__, "UI language captured.", True)
|
||||
return body
|
||||
@@ -1,568 +0,0 @@
|
||||
# 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
|
||||
@@ -1,480 +0,0 @@
|
||||
# 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
|
||||
@@ -1,480 +0,0 @@
|
||||
# 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
|
||||
**状态:** 分析完成 - 实施等待测试
|
||||
@@ -1,182 +0,0 @@
|
||||
# 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/openwebui-extensions/issues)
|
||||
|
||||
Include:
|
||||
|
||||
- OpenWebUI version
|
||||
- Browser and version
|
||||
- Error messages from console
|
||||
- Steps to reproduce
|
||||
|
||||
---
|
||||
|
||||
**Author:** Fu-Jie | **Version:** 0.2.0 | **License:** MIT
|
||||
@@ -1,182 +0,0 @@
|
||||
# 原生工具显示使用指南
|
||||
|
||||
## 🎨 什么是原生工具显示?
|
||||
|
||||
原生工具显示是一项实验性功能,与 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/openwebui-extensions/issues)
|
||||
|
||||
包含:
|
||||
|
||||
- OpenWebUI 版本
|
||||
- 浏览器和版本
|
||||
- 控制台的错误消息
|
||||
- 复现步骤
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie | **版本:** 0.2.0 | **许可证:** MIT
|
||||
@@ -1,509 +0,0 @@
|
||||
# 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
|
||||
@@ -1,708 +0,0 @@
|
||||
# SessionConfig 完整功能集成指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档详细说明如何将 GitHub Copilot SDK 的 `SessionConfig` 所有功能集成到 OpenWebUI Pipe 中。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能清单与集成状态
|
||||
|
||||
| 功能 | 状态 | 优先级 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `session_id` | ✅ 已实现 | 高 | 使用 OpenWebUI chat_id |
|
||||
| `model` | ✅ 已实现 | 高 | 从 body 动态获取 |
|
||||
| `tools` | ✅ 已实现 | 高 | v0.2.0 新增示例工具 |
|
||||
| `streaming` | ✅ 已实现 | 高 | 支持流式输出 |
|
||||
| `infinite_sessions` | ✅ 已实现 | 高 | 自动上下文压缩 |
|
||||
| `system_message` | ⚠️ 部分支持 | 中 | 可通过 Valves 添加 |
|
||||
| `available_tools` | ⚠️ 部分支持 | 中 | 已有 AVAILABLE_TOOLS |
|
||||
| `excluded_tools` | 🔲 未实现 | 低 | 可添加到 Valves |
|
||||
| `on_permission_request` | 🔲 未实现 | 中 | 需要 UI 交互支持 |
|
||||
| `provider` (BYOK) | 🔲 未实现 | 低 | 高级功能 |
|
||||
| `mcp_servers` | 🔲 未实现 | 低 | MCP 协议支持 |
|
||||
| `custom_agents` | 🔲 未实现 | 低 | 自定义代理 |
|
||||
| `config_dir` | 🔲 未实现 | 低 | 可通过 WORKSPACE_DIR |
|
||||
| `skill_directories` | 🔲 未实现 | 低 | 技能系统 |
|
||||
| `disabled_skills` | 🔲 未实现 | 低 | 技能过滤 |
|
||||
|
||||
---
|
||||
|
||||
## 📖 详细集成方案
|
||||
|
||||
### 1. ✅ session_id(已实现)
|
||||
|
||||
**功能:** 持久化会话 ID
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id if chat_id else None, # 使用 OpenWebUI 的 chat_id
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
- OpenWebUI 的 `chat_id` 直接映射为 Copilot 的 `session_id`
|
||||
- 会话状态持久化到磁盘
|
||||
- 支持跨重启恢复对话
|
||||
|
||||
---
|
||||
|
||||
### 2. ✅ model(已实现)
|
||||
|
||||
**功能:** 选择 Copilot 模型
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
# 从用户选择的模型中提取
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):]
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
MODEL_ID: str = Field(
|
||||
default="claude-sonnet-4.5",
|
||||
description="默认模型(动态获取失败时使用)"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ✅ tools(已实现 - v0.2.0)
|
||||
|
||||
**功能:** 自定义工具/函数调用
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
custom_tools = self._initialize_custom_tools()
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
ENABLE_TOOLS: bool = Field(default=False)
|
||||
AVAILABLE_TOOLS: str = Field(default="all")
|
||||
```
|
||||
|
||||
**内置示例工具:**
|
||||
|
||||
- `get_current_time` - 获取当前时间
|
||||
- `calculate` - 数学计算
|
||||
- `generate_random_number` - 随机数生成
|
||||
|
||||
**扩展方法:** 参考 [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
|
||||
---
|
||||
|
||||
### 4. ⚠️ system_message(部分支持)
|
||||
|
||||
**功能:** 自定义系统提示词
|
||||
|
||||
**集成方案:**
|
||||
|
||||
#### 方案 A:通过 Valves 添加(推荐)
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SYSTEM_MESSAGE: str = Field(
|
||||
default="",
|
||||
description="Custom system message (append mode)"
|
||||
)
|
||||
SYSTEM_MESSAGE_MODE: str = Field(
|
||||
default="append",
|
||||
description="System message mode: 'append' or 'replace'"
|
||||
)
|
||||
```
|
||||
|
||||
**实现:**
|
||||
|
||||
```python
|
||||
async def pipe(self, body, ...):
|
||||
system_message_config = None
|
||||
|
||||
if self.valves.SYSTEM_MESSAGE:
|
||||
if self.valves.SYSTEM_MESSAGE_MODE == "replace":
|
||||
system_message_config = {
|
||||
"mode": "replace",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
else:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:从 OpenWebUI 系统提示词读取
|
||||
|
||||
```python
|
||||
# 从 body 中获取系统提示词
|
||||
system_prompt = body.get("system", "")
|
||||
if system_prompt:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": system_prompt
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- `append` 模式:在默认系统提示词后追加
|
||||
- `replace` 模式:完全替换(移除 SDK 安全保护)
|
||||
|
||||
---
|
||||
|
||||
### 5. ⚠️ available_tools / excluded_tools
|
||||
|
||||
**功能:** 工具白名单/黑名单
|
||||
|
||||
**当前部分支持:**
|
||||
|
||||
```python
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="'all' or comma-separated list"
|
||||
)
|
||||
```
|
||||
|
||||
**增强实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="Available tools (comma-separated or 'all')"
|
||||
)
|
||||
EXCLUDED_TOOLS: str = Field(
|
||||
default="",
|
||||
description="Excluded tools (comma-separated)"
|
||||
)
|
||||
```
|
||||
|
||||
**应用到 SessionConfig:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
available_tools=self._parse_tool_list(self.valves.AVAILABLE_TOOLS),
|
||||
excluded_tools=self._parse_tool_list(self.valves.EXCLUDED_TOOLS),
|
||||
...
|
||||
)
|
||||
|
||||
def _parse_tool_list(self, value: str) -> list[str]:
|
||||
"""解析工具列表"""
|
||||
if not value or value == "all":
|
||||
return []
|
||||
return [t.strip() for t in value.split(",") if t.strip()]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 🔲 on_permission_request(未实现)
|
||||
|
||||
**功能:** 处理权限请求(shell 命令、文件写入等)
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- Copilot 需要执行 shell 命令
|
||||
- 需要写入文件
|
||||
- 需要访问 URL
|
||||
|
||||
**集成挑战:**
|
||||
|
||||
- 需要 OpenWebUI 前端支持实时权限弹窗
|
||||
- 需要异步处理用户确认
|
||||
|
||||
**推荐方案:**
|
||||
|
||||
#### 方案 A:自动批准(开发/测试环境)
|
||||
|
||||
```python
|
||||
async def auto_approve_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""自动批准所有权限请求(危险!)"""
|
||||
return {
|
||||
"kind": "approved",
|
||||
"rules": []
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=auto_approve_permission_handler,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:基于规则的批准
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
async def rule_based_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
kind = request.get("kind")
|
||||
|
||||
if kind == "shell" and not self.valves.ALLOW_SHELL_COMMANDS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "write" and not self.valves.ALLOW_FILE_WRITE:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "url" and not self.valves.ALLOW_URL_ACCESS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
return {"kind": "approved", "rules": []}
|
||||
```
|
||||
|
||||
#### 方案 C:通过 Event Emitter 请求用户确认(理想)
|
||||
|
||||
```python
|
||||
async def interactive_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""通过前端请求用户确认"""
|
||||
if not __event_emitter__:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
|
||||
# 发送权限请求到前端
|
||||
response_queue = asyncio.Queue()
|
||||
await __event_emitter__({
|
||||
"type": "permission_request",
|
||||
"data": {
|
||||
"kind": request.get("kind"),
|
||||
"description": request.get("description"),
|
||||
"response_queue": response_queue
|
||||
}
|
||||
})
|
||||
|
||||
# 等待用户响应(带超时)
|
||||
try:
|
||||
user_response = await asyncio.wait_for(
|
||||
response_queue.get(),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if user_response.get("approved"):
|
||||
return {"kind": "approved", "rules": []}
|
||||
else:
|
||||
return {"kind": "denied-interactively-by-user"}
|
||||
except asyncio.TimeoutError:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 🔲 provider(BYOK - Bring Your Own Key)
|
||||
|
||||
**功能:** 使用自己的 API 密钥连接 OpenAI/Azure/Anthropic
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 不使用 GitHub Copilot 配额
|
||||
- 直接连接云服务提供商
|
||||
- 使用 Azure OpenAI 部署
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(
|
||||
default="openai",
|
||||
description="Provider type: openai, azure, anthropic"
|
||||
)
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
PROVIDER_BEARER_TOKEN: str = Field(default="")
|
||||
AZURE_API_VERSION: str = Field(default="2024-10-21")
|
||||
|
||||
def _build_provider_config(self) -> dict | None:
|
||||
"""构建 Provider 配置"""
|
||||
if not self.valves.USE_CUSTOM_PROVIDER:
|
||||
return None
|
||||
|
||||
config = {
|
||||
"type": self.valves.PROVIDER_TYPE,
|
||||
"base_url": self.valves.PROVIDER_BASE_URL,
|
||||
}
|
||||
|
||||
if self.valves.PROVIDER_API_KEY:
|
||||
config["api_key"] = self.valves.PROVIDER_API_KEY
|
||||
|
||||
if self.valves.PROVIDER_BEARER_TOKEN:
|
||||
config["bearer_token"] = self.valves.PROVIDER_BEARER_TOKEN
|
||||
|
||||
if self.valves.PROVIDER_TYPE == "azure":
|
||||
config["azure"] = {
|
||||
"api_version": self.valves.AZURE_API_VERSION
|
||||
}
|
||||
|
||||
# 自动推断 wire_api
|
||||
if self.valves.PROVIDER_TYPE == "anthropic":
|
||||
config["wire_api"] = "responses"
|
||||
else:
|
||||
config["wire_api"] = "completions"
|
||||
|
||||
return config
|
||||
```
|
||||
|
||||
**应用:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. ✅ streaming(已实现)
|
||||
|
||||
**功能:** 流式输出
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
streaming=body.get("stream", False),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 🔲 mcp_servers(MCP 协议)
|
||||
|
||||
**功能:** Model Context Protocol 服务器集成
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 连接外部数据源(数据库、API)
|
||||
- 集成第三方服务
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
MCP_SERVERS_CONFIG: str = Field(
|
||||
default="{}",
|
||||
description="MCP servers configuration (JSON format)"
|
||||
)
|
||||
|
||||
def _parse_mcp_servers(self) -> dict | None:
|
||||
"""解析 MCP 服务器配置"""
|
||||
if not self.valves.MCP_SERVERS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.MCP_SERVERS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"type": "local",
|
||||
"command": "mcp-server-sqlite",
|
||||
"args": ["--db", "/path/to/db.sqlite"],
|
||||
"tools": ["*"]
|
||||
},
|
||||
"weather": {
|
||||
"type": "http",
|
||||
"url": "https://weather-api.example.com/mcp",
|
||||
"tools": ["get_weather", "get_forecast"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. 🔲 custom_agents
|
||||
|
||||
**功能:** 自定义 AI 代理
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 专门化的子代理(如代码审查、文档编写)
|
||||
- 不同的提示词策略
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CUSTOM_AGENTS_CONFIG: str = Field(
|
||||
default="[]",
|
||||
description="Custom agents configuration (JSON array)"
|
||||
)
|
||||
|
||||
def _parse_custom_agents(self) -> list | None:
|
||||
"""解析自定义代理配置"""
|
||||
if not self.valves.CUSTOM_AGENTS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.CUSTOM_AGENTS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "code_reviewer",
|
||||
"display_name": "Code Reviewer",
|
||||
"description": "Reviews code for best practices",
|
||||
"prompt": "You are an expert code reviewer. Focus on security, performance, and maintainability.",
|
||||
"tools": ["read_file", "write_file"],
|
||||
"infer": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. 🔲 config_dir
|
||||
|
||||
**功能:** 自定义配置目录
|
||||
|
||||
**当前支持:**
|
||||
|
||||
- 已有 `WORKSPACE_DIR` 控制工作目录
|
||||
|
||||
**增强方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CONFIG_DIR: str = Field(
|
||||
default="",
|
||||
description="Custom config directory for session state"
|
||||
)
|
||||
|
||||
session_config = SessionConfig(
|
||||
config_dir=self.valves.CONFIG_DIR if self.valves.CONFIG_DIR else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12. 🔲 skill_directories / disabled_skills
|
||||
|
||||
**功能:** Copilot Skills 系统
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 加载自定义技能包
|
||||
- 禁用特定技能
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SKILL_DIRECTORIES: str = Field(
|
||||
default="",
|
||||
description="Comma-separated skill directories"
|
||||
)
|
||||
DISABLED_SKILLS: str = Field(
|
||||
default="",
|
||||
description="Comma-separated disabled skills"
|
||||
)
|
||||
|
||||
def _parse_skills_config(self):
|
||||
"""解析技能配置"""
|
||||
skill_dirs = []
|
||||
if self.valves.SKILL_DIRECTORIES:
|
||||
skill_dirs = [
|
||||
d.strip()
|
||||
for d in self.valves.SKILL_DIRECTORIES.split(",")
|
||||
if d.strip()
|
||||
]
|
||||
|
||||
disabled = []
|
||||
if self.valves.DISABLED_SKILLS:
|
||||
disabled = [
|
||||
s.strip()
|
||||
for s in self.valves.DISABLED_SKILLS.split(",")
|
||||
if s.strip()
|
||||
]
|
||||
|
||||
return skill_dirs, disabled
|
||||
|
||||
# 应用
|
||||
skill_dirs, disabled_skills = self._parse_skills_config()
|
||||
session_config = SessionConfig(
|
||||
skill_directories=skill_dirs if skill_dirs else None,
|
||||
disabled_skills=disabled_skills if disabled_skills else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13. ✅ infinite_sessions(已实现)
|
||||
|
||||
**功能:** 无限会话与自动上下文压缩
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
INFINITE_SESSION: bool = Field(default=True)
|
||||
COMPACTION_THRESHOLD: float = Field(default=0.8)
|
||||
BUFFER_THRESHOLD: float = Field(default=0.95)
|
||||
|
||||
infinite_session_config = None
|
||||
if self.valves.INFINITE_SESSION:
|
||||
infinite_session_config = {
|
||||
"enabled": True,
|
||||
"background_compaction_threshold": self.valves.COMPACTION_THRESHOLD,
|
||||
"buffer_exhaustion_threshold": self.valves.BUFFER_THRESHOLD,
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
infinite_sessions=infinite_session_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 实施优先级建议
|
||||
|
||||
### 🔥 高优先级(立即实现)
|
||||
|
||||
1. **system_message** - 用户最常需要的功能
|
||||
2. **on_permission_request (基于规则)** - 安全性需求
|
||||
|
||||
### 📌 中优先级(下一阶段)
|
||||
|
||||
3. **excluded_tools** - 完善工具管理
|
||||
4. **provider (BYOK)** - 高级用户需求
|
||||
5. **config_dir** - 增强会话管理
|
||||
|
||||
### 📋 低优先级(可选)
|
||||
|
||||
6. **mcp_servers** - 高级集成
|
||||
7. **custom_agents** - 专业化功能
|
||||
8. **skill_directories** - 生态系统功能
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速实施计划
|
||||
|
||||
### Phase 1: 基础增强(1-2小时)
|
||||
|
||||
```python
|
||||
# 添加到 Valves
|
||||
SYSTEM_MESSAGE: str = Field(default="")
|
||||
SYSTEM_MESSAGE_MODE: str = Field(default="append")
|
||||
EXCLUDED_TOOLS: str = Field(default="")
|
||||
|
||||
# 添加到 pipe() 方法
|
||||
system_message_config = self._build_system_message_config()
|
||||
excluded_tools = self._parse_tool_list(self.valves.EXCLUDED_TOOLS)
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
excluded_tools=excluded_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 2: 权限管理(2-3小时)
|
||||
|
||||
```python
|
||||
# 添加权限控制 Valves
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
# 实现权限处理器
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=self._create_permission_handler(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 3: BYOK 支持(3-4小时)
|
||||
|
||||
```python
|
||||
# 添加 Provider Valves
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(default="openai")
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
|
||||
# 实现 Provider 配置
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- **SDK 类型定义**: `/opt/homebrew/.../copilot/types.py`
|
||||
- **工具系统**: [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
- **SDK 文档**: <https://github.com/github/copilot-sdk>
|
||||
|
||||
---
|
||||
|
||||
## ✅ 实施检查清单
|
||||
|
||||
使用此清单跟踪实施进度:
|
||||
|
||||
- [x] session_id
|
||||
- [x] model
|
||||
- [x] tools
|
||||
- [x] streaming
|
||||
- [x] infinite_sessions
|
||||
- [ ] system_message
|
||||
- [ ] available_tools (完善)
|
||||
- [ ] excluded_tools
|
||||
- [ ] on_permission_request
|
||||
- [ ] provider (BYOK)
|
||||
- [ ] mcp_servers
|
||||
- [ ] custom_agents
|
||||
- [ ] config_dir
|
||||
- [ ] skill_directories
|
||||
- [ ] disabled_skills
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** v1.0
|
||||
**日期:** 2026-01-26
|
||||
**更新:** 随功能实施持续更新
|
||||
@@ -1,191 +0,0 @@
|
||||
# 🛠️ 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
|
||||
@@ -1,431 +0,0 @@
|
||||
# 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
|
||||
@@ -1,835 +0,0 @@
|
||||
# GitHub Copilot SDK Integration Workflow
|
||||
|
||||
**Author:** Fu-Jie
|
||||
**Version:** 0.2.3
|
||||
**Last Updated:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Request Processing Flow](#request-processing-flow)
|
||||
3. [Session Management](#session-management)
|
||||
4. [Streaming Response Handling](#streaming-response-handling)
|
||||
5. [Event Processing Mechanism](#event-processing-mechanism)
|
||||
6. [Tool Execution Flow](#tool-execution-flow)
|
||||
7. [System Prompt Extraction](#system-prompt-extraction)
|
||||
8. [Configuration Parameters](#configuration-parameters)
|
||||
9. [Key Functions Reference](#key-functions-reference)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Component Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe Interface (Entry Point) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (Main Logic) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. Environment Setup (_setup_env) │ │ │
|
||||
│ │ │ 2. Model Selection (request_model parsing) │ │ │
|
||||
│ │ │ 3. Chat Context Extraction │ │ │
|
||||
│ │ │ 4. System Prompt Extraction │ │ │
|
||||
│ │ │ 5. Session Management (create/resume) │ │ │
|
||||
│ │ │ 6. Streaming/Non-streaming Response │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot Client │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK instance) │ │ │
|
||||
│ │ │ • Session (conversation context) │ │ │
|
||||
│ │ │ • Event Stream (async events) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI Process │
|
||||
│ (Backend Agent) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Pipe Interface**: OpenWebUI's standard entry point
|
||||
2. **Environment Manager**: CLI setup, token validation, environment variables
|
||||
3. **Session Manager**: Persistent conversation state with automatic compaction
|
||||
4. **Event Processor**: Asynchronous streaming event handler
|
||||
5. **Tool System**: Custom tool registration and execution
|
||||
6. **Debug Logger**: Frontend console logging for troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Request Processing Flow
|
||||
|
||||
### Complete Request Lifecycle
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI Request] --> B[pipe Entry Point]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{Setup Environment}
|
||||
D --> E[Parse Model ID]
|
||||
E --> F[Extract Chat Context]
|
||||
F --> G[Extract System Prompt]
|
||||
G --> H{Session Exists?}
|
||||
H -->|Yes| I[Resume Session]
|
||||
H -->|No| J[Create New Session]
|
||||
I --> K[Initialize Tools]
|
||||
J --> K
|
||||
K --> L[Process Images]
|
||||
L --> M{Streaming Mode?}
|
||||
M -->|Yes| N[stream_response]
|
||||
M -->|No| O[send_and_wait]
|
||||
N --> P[Async Event Stream]
|
||||
O --> Q[Direct Response]
|
||||
P --> R[Return to OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### Step-by-Step Breakdown
|
||||
|
||||
#### 1. Environment Setup (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
Priority:
|
||||
1. Check VALVES.CLI_PATH
|
||||
2. Search system PATH
|
||||
3. Auto-install via curl (if not found)
|
||||
4. Set GH_TOKEN environment variables
|
||||
"""
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Locate Copilot CLI binary
|
||||
- Set `COPILOT_CLI_PATH` environment variable
|
||||
- Configure `GH_TOKEN` for authentication
|
||||
- Apply custom environment variables
|
||||
|
||||
#### 2. Model Selection
|
||||
|
||||
```python
|
||||
# Input: body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. Chat Context Extraction (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# Priority order for chat_id:
|
||||
# 1. __metadata__ (most reliable)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. System Prompt Extraction (`_extract_system_prompt`)
|
||||
|
||||
Multi-source fallback strategy:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. Model database lookup (by model_id)
|
||||
3. `body.params.system`
|
||||
4. Messages with `role="system"`
|
||||
|
||||
#### 5. Session Creation/Resumption
|
||||
|
||||
**New Session:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**Resume Session:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# Session state preserved: history, tools, workspace
|
||||
except Exception:
|
||||
# Fallback to creating new session
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management
|
||||
|
||||
### Infinite Sessions Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Session Lifecycle │
|
||||
│ │
|
||||
│ ┌──────────┐ create ┌──────────┐ resume ┌───────┴───┐
|
||||
│ │ Chat ID │─────────▶ │ Session │ ◀────────│ OpenWebUI │
|
||||
│ └──────────┘ │ State │ └───────────┘
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Context Window Management │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Messages [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token Usage: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Threshold Reached (0.8) │ │ │
|
||||
│ │ │ → Background Compaction Triggered │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Compacted Summary + Recent Messages │ │ │
|
||||
│ │ │ Token Usage: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # Enable infinite sessions
|
||||
background_compaction_threshold=0.8, # Start compaction at 80% token usage
|
||||
buffer_exhaustion_threshold=0.95 # Emergency threshold at 95%
|
||||
)
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **< 80%**: Normal operation, no compaction
|
||||
- **80-95%**: Background compaction (summarize older messages)
|
||||
- **> 95%**: Force compaction before next request
|
||||
|
||||
---
|
||||
|
||||
## Streaming Response Handling
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
Asynchronous event processing with queue-based buffering.
|
||||
|
||||
Flow:
|
||||
1. Start async send task
|
||||
2. Register event handler
|
||||
3. Process events via queue
|
||||
4. Yield chunks to OpenWebUI
|
||||
5. Clean up resources
|
||||
"""
|
||||
```
|
||||
|
||||
### Event Processing Pipeline
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK Event Stream │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Event Handler │
|
||||
│ (Sync Callback) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Async Queue │
|
||||
│ (Thread-safe) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Consumer Loop │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield to OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### State Management During Streaming
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> tags opened
|
||||
"content_sent": False # Main content has started
|
||||
}
|
||||
active_tools = {} # Track concurrent tool executions
|
||||
```
|
||||
|
||||
**State Transitions:**
|
||||
|
||||
1. `reasoning_delta` arrives → `thinking_started = True` → Output: `<think>\n{reasoning}`
|
||||
2. `message_delta` arrives → Close `</think>` if open → `content_sent = True` → Output: `{content}`
|
||||
3. `tool.execution_start` → Output tool indicator (inside/outside `<think>`)
|
||||
4. `session.complete` → Finalize stream
|
||||
|
||||
---
|
||||
|
||||
## Event Processing Mechanism
|
||||
|
||||
### Event Type Reference
|
||||
|
||||
Following official SDK patterns (from `copilot.SessionEventType`):
|
||||
|
||||
| Event Type | Description | Key Data Fields | Handler Action |
|
||||
|-----------|-------------|-----------------|----------------|
|
||||
| `assistant.message_delta` | Main content streaming | `delta_content` | Yield text chunk |
|
||||
| `assistant.reasoning_delta` | Chain-of-thought | `delta_content` | Wrap in `<think>` tags |
|
||||
| `tool.execution_start` | Tool call initiated | `name`, `tool_call_id` | Display tool indicator |
|
||||
| `tool.execution_complete` | Tool finished | `result.content` | Show completion status |
|
||||
| `session.compaction_start` | Context compaction begins | - | Log debug info |
|
||||
| `session.compaction_complete` | Compaction done | - | Log debug info |
|
||||
| `session.error` | Error occurred | `error`, `message` | Emit error notification |
|
||||
|
||||
### Event Handler Implementation
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""Process streaming events following official SDK patterns."""
|
||||
event_type = get_event_type(event) # Handle enum/string types
|
||||
|
||||
# Extract data using safe_get_data_attr (handles dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # Thread-safe enqueue
|
||||
```
|
||||
|
||||
### Official SDK Pattern Compliance
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
Official pattern: event.data.delta_content
|
||||
Handles both dict and object access patterns.
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict access (JSON-like)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# Object attribute (Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution Flow
|
||||
|
||||
### Tool Registration
|
||||
|
||||
```python
|
||||
# 1. Define tool at module level
|
||||
@define_tool(description="Generate a random integer within a specified range.")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"Generated random number: {number}"
|
||||
|
||||
# 2. Register in _initialize_custom_tools
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# Filter based on AVAILABLE_TOOLS valve
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
```
|
||||
|
||||
### Tool Execution Timeline
|
||||
|
||||
```
|
||||
User Message: "Generate a random number between 1 and 100"
|
||||
│
|
||||
▼
|
||||
Model Decision: Use tool `generate_random_number`
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_start
|
||||
│ → Display: "🔧 Running Tool: generate_random_number"
|
||||
▼
|
||||
Tool Function Execution (async)
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_complete
|
||||
│ → Result: "Generated random number: 42"
|
||||
│ → Display: "✅ Tool Completed: 42"
|
||||
▼
|
||||
Model generates response using tool result
|
||||
│
|
||||
▼
|
||||
Event: assistant.message_delta
|
||||
│ → "I generated the number 42 for you."
|
||||
▼
|
||||
Stream Complete
|
||||
```
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
**Before Content:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
Running Tool: generate_random_number...
|
||||
Tool `generate_random_number` Completed. Result: 42
|
||||
</think>
|
||||
|
||||
I generated the number 42 for you.
|
||||
```
|
||||
|
||||
**After Content Started:**
|
||||
|
||||
```markdown
|
||||
The number is
|
||||
|
||||
> 🔧 **Running Tool**: `generate_random_number`
|
||||
|
||||
> ✅ **Tool Completed**: 42
|
||||
|
||||
actually 42.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Prompt Extraction
|
||||
|
||||
### Multi-Source Priority System
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
Priority order:
|
||||
1. metadata.model.params.system (highest)
|
||||
2. Model database lookup
|
||||
3. body.params.system
|
||||
4. messages[role="system"] (fallback)
|
||||
"""
|
||||
```
|
||||
|
||||
### Source 1: Metadata Model Params
|
||||
|
||||
```python
|
||||
# OpenWebUI injects model configuration
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # Priority 1
|
||||
```
|
||||
|
||||
### Source 2: Model Database
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# Try multiple model ID variations
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # From valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### Source 3: Body Params
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### Source 4: System Message
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### Configuration in SessionConfig
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # Append to conversation context
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... other params
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### Valve Definitions
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub Fine-grained Token (requires 'Copilot Requests' permission) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | Default model when dynamic fetching fails |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Path to Copilot CLI binary |
|
||||
| `DEBUG` | bool | `False` | Enable frontend console debug logging |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI log level: none, error, warning, info, debug, all |
|
||||
| `SHOW_THINKING` | bool | `True` | Display model reasoning in `<think>` tags |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | Show session workspace path in debug mode |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | Comma-separated keywords to exclude models |
|
||||
| `WORKSPACE_DIR` | str | `""` | Restricted workspace directory (empty = process cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | Enable automatic context compaction |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | Background compaction at 80% token usage |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | Emergency threshold at 95% |
|
||||
| `TIMEOUT` | int | `300` | Stream chunk timeout (seconds) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | JSON string of custom environment variables |
|
||||
| `ENABLE_TOOLS` | bool | `False` | Enable custom tool system |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | Available tools: "all" or comma-separated list |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Set by _setup_env
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Custom variables (from CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Functions Reference
|
||||
|
||||
### Entry Points
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: OpenWebUI stable entry point
|
||||
- **Returns**: Delegates to `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: Main request processing logic
|
||||
- **Flow**: Setup → Extract → Session → Response
|
||||
- **Returns**: `str` (non-streaming) or `AsyncGenerator` (streaming)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **Purpose**: Dynamic model list fetching
|
||||
- **Returns**: List of available models with multiplier info
|
||||
- **Caching**: Uses `_model_cache` to avoid repeated API calls
|
||||
|
||||
### Session Management
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **Purpose**: Construct SessionConfig object
|
||||
- **Returns**: `SessionConfig` with infinite sessions and tools
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract chat_id with priority fallback
|
||||
- **Returns**: `{"chat_id": str}`
|
||||
|
||||
### Streaming
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **Purpose**: Async streaming event processor
|
||||
- **Yields**: Text chunks to OpenWebUI
|
||||
- **Resources**: Auto-cleanup client and session
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **Purpose**: Sync event callback (inside `stream_response`)
|
||||
- **Action**: Parse event → Enqueue chunks → Update state
|
||||
|
||||
### Helpers
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **Purpose**: Send debug logs to frontend console
|
||||
- **Condition**: Only when `DEBUG=True`
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **Purpose**: Locate CLI, set environment variables
|
||||
- **Side Effects**: Modifies `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **Purpose**: Multi-source system prompt extraction
|
||||
- **Returns**: `(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract text and images from multimodal messages
|
||||
- **Returns**: `(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **Purpose**: Register and filter custom tools
|
||||
- **Returns**: List of tool functions
|
||||
|
||||
### Utility Functions
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **Purpose**: Extract event type string from enum/string
|
||||
- **Handles**: `SessionEventType` enum → `.value` extraction
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **Purpose**: Safe attribute extraction from event.data
|
||||
- **Handles**: Both dict access and object attribute access
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```python
|
||||
# In OpenWebUI Valves UI:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### Debug Output Location
|
||||
|
||||
**Frontend Console:**
|
||||
|
||||
```javascript
|
||||
// Open browser DevTools (F12)
|
||||
// Look for logs with prefix: [Copilot Pipe]
|
||||
console.debug("[Copilot Pipe] Extracted ChatID: abc123 (Source: __metadata__)")
|
||||
```
|
||||
|
||||
**Backend Logs:**
|
||||
|
||||
```python
|
||||
# Python logging output
|
||||
logger.debug(f"[Copilot Pipe] Session resumed: {chat_id}")
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Session Not Resuming
|
||||
|
||||
**Symptom:** New session created every request
|
||||
**Causes:**
|
||||
|
||||
- `chat_id` not extracted correctly
|
||||
- Session expired on Copilot side
|
||||
- `INFINITE_SESSION=False` (sessions not persistent)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted ChatID: <id> (Source: ...)"
|
||||
"Session <id> not found (...), creating new."
|
||||
```
|
||||
|
||||
#### 2. System Prompt Not Applied
|
||||
|
||||
**Symptom:** Model ignores configured system prompt
|
||||
**Causes:**
|
||||
|
||||
- Not found in any of 4 sources
|
||||
- Session resumed (system prompt only set on creation)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted system prompt from <source> (length: X)"
|
||||
"Configured system message (mode: append)"
|
||||
```
|
||||
|
||||
#### 3. Tools Not Available
|
||||
|
||||
**Symptom:** Model can't use custom tools
|
||||
**Causes:**
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- Tool not registered in `_initialize_custom_tools`
|
||||
- Wrong `AVAILABLE_TOOLS` filter
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Enabled X custom tools: ['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Model List Caching
|
||||
|
||||
```python
|
||||
# First request: Fetch from API
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # Cache result
|
||||
|
||||
# Subsequent requests: Use cache
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### Session Persistence
|
||||
|
||||
**Impact:** Eliminates redundant model initialization on every request
|
||||
|
||||
```python
|
||||
# Without session:
|
||||
# Each request: Initialize model → Load context → Generate → Discard
|
||||
|
||||
# With session (chat_id):
|
||||
# First request: Initialize model → Load context → Generate → Save
|
||||
# Later: Resume → Generate (instant)
|
||||
```
|
||||
|
||||
### Streaming vs Non-streaming
|
||||
|
||||
**Streaming:**
|
||||
|
||||
- Lower perceived latency (first token faster)
|
||||
- Better UX for long responses
|
||||
- Resource cleanup via generator exit
|
||||
|
||||
**Non-streaming:**
|
||||
|
||||
- Simpler error handling
|
||||
- Atomic response (no partial output)
|
||||
- Use for short responses
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Protection
|
||||
|
||||
```python
|
||||
# ❌ Never log tokens
|
||||
logger.debug(f"Token: {self.valves.GH_TOKEN}") # DON'T DO THIS
|
||||
|
||||
# ✅ Mask sensitive data
|
||||
logger.debug(f"Token configured: {'*' * 10}")
|
||||
```
|
||||
|
||||
### Workspace Isolation
|
||||
|
||||
```python
|
||||
# Set WORKSPACE_DIR to restrict file access
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI respects this directory
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```python
|
||||
# Validate chat_id format
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"Invalid chat_id format: {chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **Multi-Session Management**: Support multiple parallel sessions per user
|
||||
2. **Session Analytics**: Track token usage, compaction frequency
|
||||
3. **Tool Result Caching**: Avoid redundant tool calls
|
||||
4. **Custom Event Filters**: User-configurable event handling
|
||||
5. **Workspace Templates**: Pre-configured workspace environments
|
||||
6. **Streaming Abort**: Graceful cancellation of long-running requests
|
||||
|
||||
### API Evolution
|
||||
|
||||
Monitoring Copilot SDK updates for:
|
||||
|
||||
- New event types (e.g., `assistant.function_call`)
|
||||
- Enhanced tool capabilities
|
||||
- Improved session serialization
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe Development](https://docs.openwebui.com/)
|
||||
- [OpenWebUI Extensions Project](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
---
|
||||
|
||||
**License:** MIT
|
||||
**Maintainer:** Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
@@ -1,835 +0,0 @@
|
||||
# GitHub Copilot SDK 集成工作流程
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** 0.2.3
|
||||
**最后更新:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构概览](#架构概览)
|
||||
2. [请求处理流程](#请求处理流程)
|
||||
3. [会话管理](#会话管理)
|
||||
4. [流式响应处理](#流式响应处理)
|
||||
5. [事件处理机制](#事件处理机制)
|
||||
6. [工具执行流程](#工具执行流程)
|
||||
7. [系统提示词提取](#系统提示词提取)
|
||||
8. [配置参数](#配置参数)
|
||||
9. [核心函数参考](#核心函数参考)
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
### 组件图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe 接口 (入口点) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (主逻辑) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. 环境设置 (_setup_env) │ │ │
|
||||
│ │ │ 2. 模型选择 (request_model 解析) │ │ │
|
||||
│ │ │ 3. 聊天上下文提取 │ │ │
|
||||
│ │ │ 4. 系统提示词提取 │ │ │
|
||||
│ │ │ 5. 会话管理 (创建/恢复) │ │ │
|
||||
│ │ │ 6. 流式/非流式响应 │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot 客户端 │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK 实例) │ │ │
|
||||
│ │ │ • Session (对话上下文) │ │ │
|
||||
│ │ │ • Event Stream (异步事件流) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI 进程 │
|
||||
│ (后端代理) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **Pipe 接口**:OpenWebUI 的标准入口点
|
||||
2. **环境管理器**:CLI 设置、令牌验证、环境变量
|
||||
3. **会话管理器**:持久化对话状态,自动压缩
|
||||
4. **事件处理器**:异步流式事件处理器
|
||||
5. **工具系统**:自定义工具注册和执行
|
||||
6. **调试日志器**:前端控制台日志,用于故障排除
|
||||
|
||||
---
|
||||
|
||||
## 请求处理流程
|
||||
|
||||
### 完整请求生命周期
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI 请求] --> B[pipe 入口点]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{设置环境}
|
||||
D --> E[解析模型 ID]
|
||||
E --> F[提取聊天上下文]
|
||||
F --> G[提取系统提示词]
|
||||
G --> H{会话存在?}
|
||||
H -->|是| I[恢复会话]
|
||||
H -->|否| J[创建新会话]
|
||||
I --> K[初始化工具]
|
||||
J --> K
|
||||
K --> L[处理图片]
|
||||
L --> M{流式模式?}
|
||||
M -->|是| N[stream_response]
|
||||
M -->|否| O[send_and_wait]
|
||||
N --> P[异步事件流]
|
||||
O --> Q[直接响应]
|
||||
P --> R[返回到 OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### 逐步分解
|
||||
|
||||
#### 1. 环境设置 (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
优先级:
|
||||
1. 检查 VALVES.CLI_PATH
|
||||
2. 搜索系统 PATH
|
||||
3. 自动通过 curl 安装(如果未找到)
|
||||
4. 设置 GH_TOKEN 环境变量
|
||||
"""
|
||||
```
|
||||
|
||||
**操作:**
|
||||
|
||||
- 定位 Copilot CLI 二进制文件
|
||||
- 设置 `COPILOT_CLI_PATH` 环境变量
|
||||
- 配置 `GH_TOKEN` 进行身份验证
|
||||
- 应用自定义环境变量
|
||||
|
||||
#### 2. 模型选择
|
||||
|
||||
```python
|
||||
# 输入:body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. 聊天上下文提取 (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# chat_id 的优先级顺序:
|
||||
# 1. __metadata__(最可靠)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. 系统提示词提取 (`_extract_system_prompt`)
|
||||
|
||||
多源回退策略:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. 模型数据库查询(按 model_id)
|
||||
3. `body.params.system`
|
||||
4. 包含 `role="system"` 的消息
|
||||
|
||||
#### 5. 会话创建/恢复
|
||||
|
||||
**新会话:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**恢复会话:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# 会话状态保留:历史、工具、工作区
|
||||
except Exception:
|
||||
# 回退到创建新会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话管理
|
||||
|
||||
### 无限会话架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 会话生命周期 │
|
||||
│ │
|
||||
│ ┌──────────┐ 创建 ┌──────────┐ 恢复 ┌───────────┐ │
|
||||
│ │ Chat ID │─────▶ │ Session │ ◀────────│ OpenWebUI │ │
|
||||
│ └──────────┘ │ State │ └───────────┘ │
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ 上下文窗口管理 │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 消息 [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token 使用率: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 达到阈值 (0.8) │ │ │
|
||||
│ │ │ → 后台压缩触发 │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 压缩摘要 + 最近消息 │ │ │
|
||||
│ │ │ Token 使用率: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 配置参数
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # 启用无限会话
|
||||
background_compaction_threshold=0.8, # 在 80% token 使用率时开始压缩
|
||||
buffer_exhaustion_threshold=0.95 # 95% 紧急阈值
|
||||
)
|
||||
```
|
||||
|
||||
**行为:**
|
||||
|
||||
- **< 80%**:正常操作,无压缩
|
||||
- **80-95%**:后台压缩(总结旧消息)
|
||||
- **> 95%**:在下一个请求前强制压缩
|
||||
|
||||
---
|
||||
|
||||
## 流式响应处理
|
||||
|
||||
### 事件驱动架构
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
使用基于队列的缓冲进行异步事件处理。
|
||||
|
||||
流程:
|
||||
1. 启动异步发送任务
|
||||
2. 注册事件处理器
|
||||
3. 通过队列处理事件
|
||||
4. 向 OpenWebUI 产出块
|
||||
5. 清理资源
|
||||
"""
|
||||
```
|
||||
|
||||
### 事件处理管道
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK 事件流 │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 事件处理器 │
|
||||
│ (同步回调) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 异步队列 │
|
||||
│ (线程安全) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 消费者循环 │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield 到 OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### 流式传输期间的状态管理
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> 标签已打开
|
||||
"content_sent": False # 主内容已开始
|
||||
}
|
||||
active_tools = {} # 跟踪并发工具执行
|
||||
```
|
||||
|
||||
**状态转换:**
|
||||
|
||||
1. `reasoning_delta` 到达 → `thinking_started = True` → 输出:`<think>\n{reasoning}`
|
||||
2. `message_delta` 到达 → 如果打开则关闭 `</think>` → `content_sent = True` → 输出:`{content}`
|
||||
3. `tool.execution_start` → 输出工具指示器(在 `<think>` 内部/外部)
|
||||
4. `session.complete` → 完成流
|
||||
|
||||
---
|
||||
|
||||
## 事件处理机制
|
||||
|
||||
### 事件类型参考
|
||||
|
||||
遵循官方 SDK 模式(来自 `copilot.SessionEventType`):
|
||||
|
||||
| 事件类型 | 描述 | 关键数据字段 | 处理器操作 |
|
||||
|---------|------|-------------|-----------|
|
||||
| `assistant.message_delta` | 主内容流式传输 | `delta_content` | 产出文本块 |
|
||||
| `assistant.reasoning_delta` | 思维链 | `delta_content` | 用 `<think>` 标签包装 |
|
||||
| `tool.execution_start` | 工具调用启动 | `name`, `tool_call_id` | 显示工具指示器 |
|
||||
| `tool.execution_complete` | 工具完成 | `result.content` | 显示完成状态 |
|
||||
| `session.compaction_start` | 上下文压缩开始 | - | 记录调试信息 |
|
||||
| `session.compaction_complete` | 压缩完成 | - | 记录调试信息 |
|
||||
| `session.error` | 发生错误 | `error`, `message` | 发出错误通知 |
|
||||
|
||||
### 事件处理器实现
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""遵循官方 SDK 模式处理流式事件。"""
|
||||
event_type = get_event_type(event) # 处理枚举/字符串类型
|
||||
|
||||
# 使用 safe_get_data_attr 提取数据(处理 dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # 线程安全入队
|
||||
```
|
||||
|
||||
### 官方 SDK 模式合规性
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
官方模式:event.data.delta_content
|
||||
处理 dict 和对象访问模式。
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict 访问(类似 JSON)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# 对象属性(Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具执行流程
|
||||
|
||||
### 工具注册
|
||||
|
||||
```python
|
||||
# 1. 在模块级别定义工具
|
||||
@define_tool(description="在指定范围内生成随机整数。")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"生成的随机数: {number}"
|
||||
|
||||
# 2. 在 _initialize_custom_tools 中注册
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# 根据 AVAILABLE_TOOLS valve 过滤
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
```
|
||||
|
||||
### 工具执行时间线
|
||||
|
||||
```
|
||||
用户消息:生成一个 1 到 100 之间的随机数
|
||||
│
|
||||
▼
|
||||
模型决策:使用工具 `generate_random_number`
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_start
|
||||
│ → 显示:"🔧 运行工具:generate_random_number"
|
||||
▼
|
||||
工具函数执行(异步)
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_complete
|
||||
│ → 结果:"生成的随机数:42"
|
||||
│ → 显示:"✅ 工具完成:42"
|
||||
▼
|
||||
模型使用工具结果生成响应
|
||||
│
|
||||
▼
|
||||
事件:assistant.message_delta
|
||||
│ → "我为你生成了数字 42。"
|
||||
▼
|
||||
流完成
|
||||
```
|
||||
|
||||
### 视觉指示器
|
||||
|
||||
**内容前:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
运行工具:generate_random_number...
|
||||
工具 `generate_random_number` 完成。结果:42
|
||||
</think>
|
||||
|
||||
我为你生成了数字 42。
|
||||
```
|
||||
|
||||
**内容开始后:**
|
||||
|
||||
```markdown
|
||||
数字是
|
||||
|
||||
> 🔧 **运行工具**:`generate_random_number`
|
||||
|
||||
> ✅ **工具完成**:42
|
||||
|
||||
实际上是 42。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 系统提示词提取
|
||||
|
||||
### 多源优先级系统
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
优先级顺序:
|
||||
1. metadata.model.params.system(最高)
|
||||
2. 模型数据库查询
|
||||
3. body.params.system
|
||||
4. messages[role="system"](回退)
|
||||
"""
|
||||
```
|
||||
|
||||
### 来源 1:元数据模型参数
|
||||
|
||||
```python
|
||||
# OpenWebUI 注入模型配置
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # 优先级 1
|
||||
```
|
||||
|
||||
### 来源 2:模型数据库
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# 尝试多个模型 ID 变体
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # 来自 valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### 来源 3:Body 参数
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### 来源 4:系统消息
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### SessionConfig 中的配置
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # 追加到对话上下文
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... 其他参数
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置参数
|
||||
|
||||
### Valve 定义
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|--------|------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub 精细化令牌(需要 'Copilot Requests' 权限) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | 动态获取失败时的默认模型 |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Copilot CLI 二进制文件路径 |
|
||||
| `DEBUG` | bool | `False` | 启用前端控制台调试日志 |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI 日志级别:none、error、warning、info、debug、all |
|
||||
| `SHOW_THINKING` | bool | `True` | 在 `<think>` 标签中显示模型推理 |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | 在调试模式下显示会话工作区路径 |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | 逗号分隔的关键字,用于排除模型 |
|
||||
| `WORKSPACE_DIR` | str | `""` | 限制的工作区目录(空 = 进程 cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | 启用自动上下文压缩 |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | 80% token 使用率时后台压缩 |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | 95% 紧急阈值 |
|
||||
| `TIMEOUT` | int | `300` | 流块超时(秒) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | 自定义环境变量的 JSON 字符串 |
|
||||
| `ENABLE_TOOLS` | bool | `False` | 启用自定义工具系统 |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | 可用工具:"all" 或逗号分隔列表 |
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 由 _setup_env 设置
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# 自定义变量(来自 CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心函数参考
|
||||
|
||||
### 入口点
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:OpenWebUI 稳定入口点
|
||||
- **返回**:委托给 `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:主请求处理逻辑
|
||||
- **流程**:设置 → 提取 → 会话 → 响应
|
||||
- **返回**:`str`(非流式)或 `AsyncGenerator`(流式)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **目的**:动态模型列表获取
|
||||
- **返回**:带有倍数信息的可用模型列表
|
||||
- **缓存**:使用 `_model_cache` 避免重复 API 调用
|
||||
|
||||
### 会话管理
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **目的**:构建 SessionConfig 对象
|
||||
- **返回**:带有无限会话和工具的 `SessionConfig`
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **目的**:使用优先级回退提取 chat_id
|
||||
- **返回**:`{"chat_id": str}`
|
||||
|
||||
### 流式传输
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **目的**:异步流式事件处理器
|
||||
- **产出**:文本块到 OpenWebUI
|
||||
- **资源**:自动清理客户端和会话
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **目的**:同步事件回调(在 `stream_response` 内)
|
||||
- **操作**:解析事件 → 入队块 → 更新状态
|
||||
|
||||
### 辅助函数
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **目的**:将调试日志发送到前端控制台
|
||||
- **条件**:仅当 `DEBUG=True` 时
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **目的**:定位 CLI,设置环境变量
|
||||
- **副作用**:修改 `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **目的**:多源系统提示词提取
|
||||
- **返回**:`(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **目的**:从多模态消息中提取文本和图片
|
||||
- **返回**:`(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **目的**:注册和过滤自定义工具
|
||||
- **返回**:工具函数列表
|
||||
|
||||
### 实用函数
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **目的**:从枚举/字符串提取事件类型字符串
|
||||
- **处理**:`SessionEventType` 枚举 → `.value` 提取
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **目的**:从 event.data 安全提取属性
|
||||
- **处理**:dict 访问和对象属性访问
|
||||
|
||||
---
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
### 启用调试模式
|
||||
|
||||
```python
|
||||
# 在 OpenWebUI Valves UI 中:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### 调试输出位置
|
||||
|
||||
**前端控制台:**
|
||||
|
||||
```javascript
|
||||
// 打开浏览器开发工具 (F12)
|
||||
// 查找前缀为 [Copilot Pipe] 的日志
|
||||
console.debug("[Copilot Pipe] 提取的 ChatID:abc123(来源:__metadata__)")
|
||||
```
|
||||
|
||||
**后端日志:**
|
||||
|
||||
```python
|
||||
# Python 日志输出
|
||||
logger.debug(f"[Copilot Pipe] 会话已恢复:{chat_id}")
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 会话未恢复
|
||||
|
||||
**症状**:每次请求都创建新会话
|
||||
**原因**:
|
||||
|
||||
- `chat_id` 提取不正确
|
||||
- Copilot 端会话过期
|
||||
- `INFINITE_SESSION=False`(会话不持久)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"提取的 ChatID:<id>(来源:...)"
|
||||
"会话 <id> 未找到(...),正在创建新会话。"
|
||||
```
|
||||
|
||||
#### 2. 系统提示词未应用
|
||||
|
||||
**症状**:模型忽略配置的系统提示词
|
||||
**原因**:
|
||||
|
||||
- 在 4 个来源中均未找到
|
||||
- 会话已恢复(系统提示词仅在创建时设置)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"从 <source> 提取系统提示词(长度:X)"
|
||||
"配置系统消息(模式:append)"
|
||||
```
|
||||
|
||||
#### 3. 工具不可用
|
||||
|
||||
**症状**:模型无法使用自定义工具
|
||||
**原因**:
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- 工具未在 `_initialize_custom_tools` 中注册
|
||||
- 错误的 `AVAILABLE_TOOLS` 过滤器
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"已启用 X 个自定义工具:['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 模型列表缓存
|
||||
|
||||
```python
|
||||
# 第一次请求:从 API 获取
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # 缓存结果
|
||||
|
||||
# 后续请求:使用缓存
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### 会话持久化
|
||||
|
||||
**影响**:消除每次请求的冗余模型初始化
|
||||
|
||||
```python
|
||||
# 没有会话:
|
||||
# 每次请求:初始化模型 → 加载上下文 → 生成 → 丢弃
|
||||
|
||||
# 有会话(chat_id):
|
||||
# 第一次请求:初始化模型 → 加载上下文 → 生成 → 保存
|
||||
# 后续:恢复 → 生成(即时)
|
||||
```
|
||||
|
||||
### 流式 vs 非流式
|
||||
|
||||
**流式:**
|
||||
|
||||
- 降低感知延迟(首个 token 更快)
|
||||
- 长响应的更好用户体验
|
||||
- 通过生成器退出进行资源清理
|
||||
|
||||
**非流式:**
|
||||
|
||||
- 更简单的错误处理
|
||||
- 原子响应(无部分输出)
|
||||
- 用于短响应
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 令牌保护
|
||||
|
||||
```python
|
||||
# ❌ 永远不要记录令牌
|
||||
logger.debug(f"令牌:{self.valves.GH_TOKEN}") # 不要这样做
|
||||
|
||||
# ✅ 屏蔽敏感数据
|
||||
logger.debug(f"令牌已配置:{'*' * 10}")
|
||||
```
|
||||
|
||||
### 工作区隔离
|
||||
|
||||
```python
|
||||
# 设置 WORKSPACE_DIR 以限制文件访问
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI 遵守此目录
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### 输入验证
|
||||
|
||||
```python
|
||||
# 验证 chat_id 格式
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"无效的 chat_id 格式:{chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未来增强
|
||||
|
||||
### 计划功能
|
||||
|
||||
1. **多会话管理**:支持每个用户的多个并行会话
|
||||
2. **会话分析**:跟踪 token 使用率、压缩频率
|
||||
3. **工具结果缓存**:避免冗余工具调用
|
||||
4. **自定义事件过滤器**:用户可配置的事件处理
|
||||
5. **工作区模板**:预配置的工作区环境
|
||||
6. **流式中止**:优雅取消长时间运行的请求
|
||||
|
||||
### API 演进
|
||||
|
||||
监控 Copilot SDK 更新:
|
||||
|
||||
- 新事件类型(例如 `assistant.function_call`)
|
||||
- 增强的工具功能
|
||||
- 改进的会话序列化
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [GitHub Copilot SDK 文档](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe 开发](https://docs.openwebui.com/)
|
||||
- [OpenWebUI Extensions 项目](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
---
|
||||
|
||||
**许可证**:MIT
|
||||
**维护者**:Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
@@ -1,124 +0,0 @@
|
||||
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())
|
||||
@@ -1,94 +0,0 @@
|
||||
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())
|
||||
@@ -1,47 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from copilot import CopilotClient
|
||||
|
||||
|
||||
async def main():
|
||||
token = os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")
|
||||
if not token:
|
||||
print(
|
||||
"Error: GH_TOKEN (or GITHUB_TOKEN) environment variable not set. Please export GH_TOKEN=... before running."
|
||||
)
|
||||
return
|
||||
|
||||
client = CopilotClient()
|
||||
await client.start()
|
||||
|
||||
async def on_permission_request(request, _ctx):
|
||||
if request.get("kind") == "mcp":
|
||||
return {"kind": "approved"}
|
||||
return {"kind": "approved"}
|
||||
|
||||
session = await client.create_session(
|
||||
{
|
||||
"model": "gpt-5-mini",
|
||||
"mcp_servers": {
|
||||
"github": {
|
||||
"type": "http",
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"headers": {"Authorization": f"Bearer {token}"},
|
||||
"tools": ["*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
result = await session.send_and_wait(
|
||||
{
|
||||
"prompt": "Use GitHub MCP tools to find the owner of the 'openwebui-extensions' repository.",
|
||||
},timeout=1000
|
||||
)
|
||||
print(result.data.content)
|
||||
|
||||
await client.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,31 +0,0 @@
|
||||
import re
|
||||
|
||||
def reproduce_bug():
|
||||
# 模拟 Issue #49 中提到的受损逻辑
|
||||
# 核心问题在于正则表达式过于贪婪,或者在多次迭代中错误地将两个加粗块中间的部分当作了“带空格的加粗内容”
|
||||
text = "I **prefer** tea **to** coffee."
|
||||
|
||||
# 模拟一个不严谨的、容易跨块匹配的正则
|
||||
# 它会匹配 ** 开始,中间任意字符,** 结束
|
||||
buggy_pattern = re.compile(r"(\*\*)(.*?)(\*\*)")
|
||||
|
||||
def buggy_fix(content):
|
||||
# 模拟插件中的 strip 逻辑:它想去掉加粗符号内部的空格
|
||||
# 但由于正则匹配了 "**prefer** tea **", 这里的 m.group(2) 变成了 "prefer** tea "
|
||||
return buggy_pattern.sub(lambda m: f"{m.group(1)}{m.group(2).strip()}{m.group(1)}", content)
|
||||
|
||||
# 第一次执行:处理了 "**prefer**" -> "**prefer**"
|
||||
result_1 = buggy_fix(text)
|
||||
|
||||
# 第二次执行(模拟 while 循环或重复运行):
|
||||
# 此时如果正则引擎从第一个加粗的结束符开始匹配到第二个加粗的起始符
|
||||
# 指针位置: I **prefer**[匹配开始] tea [匹配结束]**to** coffee.
|
||||
# 就会把 " tea " 两侧的 ** 当作一对,然后 strip() 掉空格
|
||||
result_2 = buggy_fix(result_1)
|
||||
|
||||
print(f"Original: {text}")
|
||||
print(f"Step 1: {result_1}")
|
||||
print(f"Step 2: {result_2} (Bug Reproduced!)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
reproduce_bug()
|
||||
@@ -1,28 +0,0 @@
|
||||
import re
|
||||
|
||||
def reproduce_bug_v2():
|
||||
# 模拟更接近旧版实际代码的情况
|
||||
# 旧版代码中循环多次处理,且正则可能在处理嵌套或连续块时出现偏移
|
||||
text = "I **prefer** tea **to** coffee."
|
||||
|
||||
# 这是一个贪婪且不具备前瞻断言的正则
|
||||
buggy_pattern = re.compile(r"(\*\*)( +)(.*?)( +)(\*\*)")
|
||||
|
||||
# 模拟那种“只要看到 ** 且中间有空格就想修”的逻辑
|
||||
# 如果文本是 "I **prefer** tea **to**"
|
||||
# 这里的空格出现在 "prefer**" 和 "**to" 之间
|
||||
content = "I **prefer** tea **to** coffee."
|
||||
|
||||
# 错误的匹配尝试:将第一个块的结尾和第二个块的开头误认为是一对
|
||||
# I **prefer** tea **to**
|
||||
# ^^ ^^
|
||||
# A B
|
||||
# 正则误以为 A 是开始,B 是结束
|
||||
|
||||
bug_result = re.sub(r"\*\*( +)(.*?)( +)\*\*", r"**\2**", content)
|
||||
|
||||
print(f"Input: {content}")
|
||||
print(f"Output: {bug_result}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
reproduce_bug_v2()
|
||||
@@ -1,44 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add plugin dir to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
plugin_dir = os.path.abspath(os.path.join(current_dir, "..", "filters", "markdown_normalizer"))
|
||||
sys.path.append(plugin_dir)
|
||||
|
||||
from markdown_normalizer import ContentNormalizer, NormalizerConfig
|
||||
|
||||
def test_latex_protection():
|
||||
# Test case 1: The reported issue with \times
|
||||
content_1 = r"Calculation: $C(33, 6) \times C(16, 1)$"
|
||||
|
||||
config = NormalizerConfig(enable_escape_fix=True)
|
||||
normalizer = ContentNormalizer(config)
|
||||
|
||||
result_1 = normalizer.normalize(content_1)
|
||||
|
||||
print("--- Test 1: \times Protection ---")
|
||||
print(f"Input: {content_1}")
|
||||
print(f"Output: {result_1}")
|
||||
|
||||
if r"\times" in result_1:
|
||||
print("✅ PASSED")
|
||||
else:
|
||||
print("❌ FAILED")
|
||||
|
||||
# Test case 2: Other potential collisions like \nu (newline) or \theta (tab?)
|
||||
# Using raw strings carefully
|
||||
content_2 = r"Formula: $\theta = \nu + \tau$"
|
||||
result_2 = normalizer.normalize(content_2)
|
||||
|
||||
print("\n--- Test 2: \theta and \nu Protection ---")
|
||||
print(f"Input: {content_2}")
|
||||
print(f"Output: {result_2}")
|
||||
|
||||
if r"\theta" in result_2 and r"\nu" in result_2:
|
||||
print("✅ PASSED")
|
||||
else:
|
||||
print("❌ FAILED")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_latex_protection()
|
||||
@@ -1,42 +0,0 @@
|
||||
import re
|
||||
|
||||
def verify_fix_v126():
|
||||
# 1. 准备触发 Bug 的测试文本
|
||||
test_cases = [
|
||||
"I **prefer** tea **to** coffee.", # 标准 Issue #49 案例
|
||||
"The **quick** brown **fox** jumps **over**.", # 多个加粗块
|
||||
"** text ** and ** more **", # 需要修复的内部空格
|
||||
"Calculations: 2 * 3 * 4 = 24", # 不应被识别为强调的数学公式
|
||||
]
|
||||
|
||||
# 2. 使用 v1.2.6 中的核心正则表达式 (移除了可能引起解析错误的中文注释)
|
||||
# 模式: (?<!\*|_)(\*{1,3}|_{1,3})(?P<inner>(?:(?!\1)[^\n])*?)(\1)(?!\*|_)
|
||||
pattern_str = r"(?<!\*|_)(\*{1,3}|_{1,3})(?P<inner>(?:(?!\1)[^\n])*?)(\1)(?!\*|_)"
|
||||
FIX_REGEX = re.compile(pattern_str)
|
||||
|
||||
def fixed_normalizer(content):
|
||||
def replacer(match):
|
||||
symbol = match.group(1)
|
||||
inner = match.group("inner")
|
||||
|
||||
stripped_inner = inner.strip()
|
||||
|
||||
# 只有当确实有空格需要修,且内部不是空的才修复
|
||||
if stripped_inner != inner and stripped_inner:
|
||||
return f"{symbol}{stripped_inner}{symbol}"
|
||||
return match.group(0)
|
||||
|
||||
# 模拟插件循环处理
|
||||
for _ in range(2):
|
||||
content = FIX_REGEX.sub(replacer, content)
|
||||
return content
|
||||
|
||||
print("--- v1.2.6 Fix Verification ---")
|
||||
for text in test_cases:
|
||||
result = fixed_normalizer(text)
|
||||
print(f"Input: {text}")
|
||||
print(f"Output: {result}")
|
||||
print("-" * 30)
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_fix_v126()
|
||||
@@ -153,10 +153,12 @@ BASE_GUIDELINES = (
|
||||
"3. **Interactive Artifacts (HTML)**: **Premium Delivery Protocol**: For web applications, you MUST perform two actions:\n"
|
||||
" - 1. **Persist**: Create the file in the workspace (e.g., `index.html`) for project structure.\n"
|
||||
" - 2. **Publish & Embed**: Call `publish_file_from_workspace(filename='your_file.html')`. This will automatically trigger the **Premium Experience** by directly embedding the interactive component using the action-style return.\n"
|
||||
" - **CRITICAL**: When using this protocol, **DO NOT** output the raw HTML code in a code block. Provide ONLY the **[Preview]** and **[Download]** links returned by the tool. The interactive embed will appear automatically after your message finishes.\n"
|
||||
" - **Process Visibility**: While raw code is forbidden, you SHOULD provide a **very brief Markdown summary** of the component's structure or key features (e.g., 'Generated login form with validation') before publishing. This keeps the user informed of the 'processing' progress.\n"
|
||||
" - **CRITICAL ANTI-INLINE RULE**: Never output your *own* raw HTML source code directly in the chat. You MUST ALWAYS persist the HTML to a file and call `publish_file_from_workspace`.\n"
|
||||
" - **CRITICAL**: When using this protocol in **Rich UI mode** (`embed_type='richui'`), **DO NOT** output the raw HTML code in a code block. Provide ONLY the **[Preview]** and **[Download]** links returned by the tool. The interactive embed will appear automatically after your message finishes.\n"
|
||||
" - **Artifacts mode** (`embed_type='artifacts'`): You MUST provide the **[Preview]** and **[Download]** links. DO NOT output HTML code block. The system will automatically append the HTML visualization to the chat string.\n"
|
||||
" - **Process Visibility**: While raw code is often replaced by links/frames, you SHOULD provide a **very brief Markdown summary** of the component's structure or key features (e.g., 'Generated login form with validation') before publishing. This keeps the user informed of the 'processing' progress.\n"
|
||||
" - **Game/App Controls**: If your HTML includes keyboard controls (e.g., arrow keys, spacebar for games), you MUST include `event.preventDefault()` in your `keydown` listeners to prevent the parent browser page from scrolling.\n"
|
||||
"4. **Images & Files**: ALWAYS embed generated images/files directly using ``. Never provide plain text links.\n"
|
||||
"4. **Media & Files**: ALWAYS embed generated images, GIFs, and videos directly using ``. Supported formats like PNG, JPG, GIF, MOV, and MP4 should be shown as visual assets. Never provide plain text links for visual media.\n"
|
||||
"5. **File Delivery Protocol (Dual-Channel Delivery)**:\n"
|
||||
" - **Definition**: **Artifacts** = content/code-block driven visual output in chat (typically with `html_embed`). **Rich UI** = tool/action returned embedded UI rendered by emitter in a persistent sandboxed iframe.\n"
|
||||
" - **Philosophy**: Visual Artifacts (HTML/Mermaid) and Downloadable Files are **COMPLEMENTARY**. Always aim to provide BOTH: instant visual insight in the chat AND a persistent file for the user to keep.\n"
|
||||
@@ -164,7 +166,7 @@ BASE_GUIDELINES = (
|
||||
" - **Implicit Requests**: If asked to 'export', 'get link', or 'save', automatically trigger this sequence.\n"
|
||||
" - **Execution Sequence**: 1. **Write Local**: Create file. 2. **Publish**: Call `publish_file_from_workspace`. 3. **Response Structure**:\n"
|
||||
" - **For PDF files**: You MUST output ONLY Markdown links from the tool output (preview + download). **CRITICAL: NEVER output iframe/html_embed for PDF.**\n"
|
||||
" - **For HTML files**: choose mode by complexity/environment. **Artifacts mode** (`embed_type='artifacts'`): output [Preview]/[Download], then output `html_embed` in a ```html code block. **Rich UI mode** (`embed_type='richui'`): output ONLY [Preview]/[Download]; do NOT output iframe/html block because Rich UI will render automatically via emitter."
|
||||
" - **For HTML files**: Choose mode by complexity. **Artifacts mode** (`embed_type='artifacts'`): REQUIRED for dashboards, reports, and large/long UI since it has unlimited height. Output ONLY [Preview]/[Download]; do NOT output any iframe/html block because the protocol will automatically append the html code block via emitter. **Rich UI mode** (`embed_type='richui'`): For small widgets ONLY. If you MUST use Rich UI for long content, you MUST add a clickable 'Full Screen' button inside your HTML design to allow expanding. Output ONLY [Preview]/[Download]; do NOT output HTML block because Rich UI will render automatically via emitter.\n"
|
||||
" - **URL Format**: You MUST use the **ABSOLUTE URLs** provided in the tool output. NEVER modify them.\n"
|
||||
" - **Bypass RAG**: This protocol automatically handles S3 storage and bypasses RAG, ensuring 100% accurate data delivery.\n"
|
||||
"6. **TODO Visibility**: Every time you call the `update_todo` tool, you **MUST** immediately follow up with a beautifully formatted **Markdown summary** of the current TODO list. Use task checkboxes (`- [ ]`), progress indicators, and clear headings so the user can see the status directly in the chat.\n"
|
||||
@@ -1010,8 +1012,8 @@ class Pipe:
|
||||
default="artifacts",
|
||||
description=(
|
||||
"Rendering style for HTML files. For PDF files, embedding is disabled and you MUST only provide preview/download Markdown links from tool output. "
|
||||
"Use 'artifacts' for HTML (Default: output html_embed iframe inside a ```html code block; no height limit). "
|
||||
"Use 'richui' for HTML (emitter-based integrated preview). DO NOT output html_embed in richui mode; it is rendered automatically. "
|
||||
"Use 'artifacts' for large UI, dashboards, or long content (Default: output html_embed iframe inside a ```html code block; no height limit). "
|
||||
"Use 'richui' ONLY for small/compact widgets (emitter-based integrated preview). DO NOT output html_embed in richui mode. If richui is used for long content, you MUST add a 'Full Screen' expansion button in the HTML logic. "
|
||||
"Only 'artifacts' and 'richui' are supported."
|
||||
),
|
||||
)
|
||||
@@ -1150,6 +1152,10 @@ class Pipe:
|
||||
|
||||
is_html = safe_filename.lower().endswith((".html", ".htm"))
|
||||
is_pdf = safe_filename.lower().endswith(".pdf")
|
||||
is_img = safe_filename.lower().endswith(
|
||||
(".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg")
|
||||
)
|
||||
is_video = safe_filename.lower().endswith((".mov", ".mp4", ".webm"))
|
||||
|
||||
view_url = None
|
||||
has_preview = False
|
||||
@@ -1184,6 +1190,8 @@ class Pipe:
|
||||
)
|
||||
if embed_type == "richui":
|
||||
hint += "\n\nCRITICAL: You are in 'richui' mode. DO NOT output an HTML code block or iframe in your message. Just output the links above."
|
||||
elif embed_type == "artifacts":
|
||||
hint += "\n\nIMPORTANT: You are in 'artifacts' mode. DO NOT output an HTML code block in your message. The system will automatically inject it after you finish."
|
||||
elif has_preview:
|
||||
hint = self._get_translation(
|
||||
user_lang,
|
||||
@@ -1216,43 +1224,78 @@ class Pipe:
|
||||
}
|
||||
if has_preview and view_url:
|
||||
result_dict["view_url"] = view_url
|
||||
if is_html and embed_type == "artifacts":
|
||||
# Artifacts mode: standard iframe for the AI to output directly (Infinite height)
|
||||
iframe_html = (
|
||||
f'<iframe src="{view_url}" '
|
||||
f'style="width:100%; height:100vh; min-height:600px; border:none; border-radius:12px; '
|
||||
f'box-shadow: var(--shadow-lg);"></iframe>'
|
||||
)
|
||||
result_dict["html_embed"] = iframe_html
|
||||
# Note: We do NOT add to pending_embeds. The AI will output this in the message.
|
||||
elif embed_type == "richui":
|
||||
# In richui mode, we physically remove html_embed to prevent the AI from outputting it
|
||||
# The system will handle the rendering via emitter
|
||||
pass
|
||||
|
||||
# 6. Premium Rich UI Experience for HTML only (Direct Embed via emitter)
|
||||
# We emit events directly ONLY IF embed_type is 'richui'.
|
||||
# Note: Emission is now delayed until session.idle to avoid UI flicker and ensure reliability.
|
||||
if is_html and embed_type == "richui" and rich_ui_supported:
|
||||
# Premium Experience for HTML only (Direct Embed via emitter)
|
||||
# Emission is delayed until session.idle to avoid UI flicker and ensure reliability.
|
||||
if is_html and rich_ui_supported:
|
||||
try:
|
||||
# For Rich UI Integrated view, we pass a clean iframe.
|
||||
# We use 60vh directly to avoid nested iframe height collapses.
|
||||
embed_content = (
|
||||
f'<iframe src="{view_url}" '
|
||||
f'style="width:100%; height:60vh; min-height:400px; border:none; border-radius:12px; '
|
||||
f'box-shadow: var(--shadow-lg);"></iframe>'
|
||||
# For BOTH Rich UI and Artifacts Mode, OpenWebUI expects the raw HTML of the component itself
|
||||
embed_content = await asyncio.to_thread(
|
||||
lambda: target_path.read_text(
|
||||
encoding="utf-8", errors="replace"
|
||||
)
|
||||
)
|
||||
|
||||
if pending_embeds is not None:
|
||||
pending_embeds.append(
|
||||
{
|
||||
"filename": safe_filename,
|
||||
"content": embed_content,
|
||||
"type": "richui",
|
||||
}
|
||||
if embed_type == "richui":
|
||||
pending_embeds.append(
|
||||
{
|
||||
"filename": safe_filename,
|
||||
"content": embed_content,
|
||||
"type": "richui",
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
f"[Copilot] Queued richui embed for '{safe_filename}', pending_embeds len={len(pending_embeds)}"
|
||||
)
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"📦 Queued richui embed: {safe_filename} (queue={len(pending_embeds)})",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif embed_type == "artifacts":
|
||||
artifacts_content = f"\n```html\n{embed_content}\n```\n"
|
||||
pending_embeds.append(
|
||||
{
|
||||
"filename": safe_filename,
|
||||
"content": artifacts_content,
|
||||
"type": "artifacts",
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
f"[Copilot] Queued artifacts embed for '{safe_filename}' (content={len(artifacts_content)}, queue={len(pending_embeds)})"
|
||||
)
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"📦 Queued artifacts embed: {safe_filename} (content={len(artifacts_content)}, queue={len(pending_embeds)})",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[Copilot] pending_embeds is None! Cannot queue embed for '{safe_filename}'"
|
||||
)
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "status",
|
||||
"data": {
|
||||
"description": f"⚠️ pending_embeds is None for {safe_filename}",
|
||||
"done": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to prepare Rich UI embed: {e}")
|
||||
logger.error(f"Failed to prepare HTML embed: {e}")
|
||||
|
||||
return result_dict
|
||||
|
||||
@@ -3490,7 +3533,7 @@ class Pipe:
|
||||
result = input_data.get("result", "")
|
||||
|
||||
# Logic to detect and move large files saved to /tmp
|
||||
# Pattern: Saved to: /tmp/copilot_result_xxxx.txt
|
||||
# Pattern: "Saved to: /tmp/copilot_result_xxxx.txt"
|
||||
import re
|
||||
import shutil
|
||||
|
||||
@@ -5267,7 +5310,6 @@ class Pipe:
|
||||
is_done=True,
|
||||
)
|
||||
|
||||
# Display tool result with improved formatting
|
||||
# --- TODO Sync Logic (File + DB) ---
|
||||
if tool_name == "update_todo" and result_type == "success":
|
||||
try:
|
||||
@@ -5665,10 +5707,38 @@ class Pipe:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Emit Rich UI components (richui type)
|
||||
# 2. Emit UI components (richui or artifacts type)
|
||||
_idle_dbg = f"IDLE: pending_embeds count={len(pending_embeds) if pending_embeds else 0}, types={[e.get('type') for e in pending_embeds] if pending_embeds else []}"
|
||||
logger.info(f"[Copilot] {_idle_dbg}")
|
||||
if __event_call__:
|
||||
try:
|
||||
await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": f'console.debug("%c[Copilot IDLE] {_idle_dbg}", "color: #f59e0b;");'
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if pending_embeds:
|
||||
for embed in pending_embeds:
|
||||
if embed.get("type") == "richui":
|
||||
_embed_dbg = f"Processing embed type='{embed.get('type')}', filename='{embed.get('filename')}', content_len={len(embed.get('content', ''))}"
|
||||
logger.info(f"[Copilot] IDLE: {_embed_dbg}")
|
||||
if __event_call__:
|
||||
try:
|
||||
await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": f'console.debug("%c[Copilot IDLE] {_embed_dbg}", "color: #f59e0b;");'
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if embed.get("type") in ["richui", "artifacts"]:
|
||||
# Status update
|
||||
await __event_emitter__(
|
||||
{
|
||||
@@ -5695,15 +5765,22 @@ class Pipe:
|
||||
},
|
||||
}
|
||||
)
|
||||
# Standard OpenWebUI Embed Structure: type: "embeds", data: {"embeds": [content]}
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "embeds",
|
||||
"data": {
|
||||
"embeds": [embed["content"]]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if embed.get("type") == "richui":
|
||||
# Standard OpenWebUI Embed Structure: type: "embeds", data: {"embeds": [content]}
|
||||
await __event_emitter__(
|
||||
{
|
||||
"type": "embeds",
|
||||
"data": {
|
||||
"embeds": [embed["content"]]
|
||||
},
|
||||
}
|
||||
)
|
||||
elif embed.get("type") == "artifacts":
|
||||
# Directly yield the markdown block to the response stream.
|
||||
# This securely appends the code block to the final message,
|
||||
# tricking OpenWebUI into rendering it as an artifact seamlessly.
|
||||
yield embed["content"]
|
||||
|
||||
# 3. LOCK internal status emission for background tasks
|
||||
# (Stray Task A from tool.execution_complete will now be discarded)
|
||||
|
||||
10
plugins/tools/README.md
Normal file
10
plugins/tools/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Tools
|
||||
|
||||
This directory contains OpenWebUI native Tool plugins.
|
||||
|
||||
- Tool plugins can be enabled for any model that supports OpenWebUI Tools.
|
||||
- Each tool plugin follows single-file implementation with bilingual docs.
|
||||
|
||||
## Available Tools
|
||||
|
||||
- [OpenWebUI Skills Manager Tool](./openwebui-skills-manager/README.md)
|
||||
10
plugins/tools/README_CN.md
Normal file
10
plugins/tools/README_CN.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Tools(工具)
|
||||
|
||||
此目录包含 OpenWebUI 原生 Tool 插件。
|
||||
|
||||
- Tool 插件可用于任何启用了 OpenWebUI Tools 的模型。
|
||||
- 每个 Tool 插件采用单文件实现并提供中英文文档。
|
||||
|
||||
## 可用工具
|
||||
|
||||
- [OpenWebUI Skills 管理工具](./openwebui-skills-manager/README_CN.md)
|
||||
76
plugins/tools/openwebui-skills-manager/README.md
Normal file
76
plugins/tools/openwebui-skills-manager/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 🧰 OpenWebUI Skills Manager Tool
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
A standalone OpenWebUI Tool plugin to manage native **Workspace > Skills** for any model.
|
||||
|
||||
## What's New
|
||||
|
||||
- Added GitHub skills-directory auto-discovery for `install_skill` (e.g., `.../tree/main/skills`) to install all child skills in one request.
|
||||
- Fixed language detection with robust frontend-first fallback (`__event_call__` + timeout), request header fallback, and profile fallback.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **🌐 Model-agnostic**: Can be enabled for any model that supports OpenWebUI Tools.
|
||||
- **🛠️ Simple Skill Management**: Directly manage OpenWebUI skill records.
|
||||
- **🔐 User-scoped Safety**: Operates on current user's accessible skills.
|
||||
- **📡 Friendly Status Feedback**: Emits status bubbles for each operation.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Open OpenWebUI and go to **Workspace > Tools**.
|
||||
2. Create a new Tool and paste `openwebui_skills_manager.py`.
|
||||
3. Enable this tool for your model/chat.
|
||||
4. Ask the model to call tool operations, for example:
|
||||
- "List my skills"
|
||||
- "Show skill named docs-writer"
|
||||
- "Create a skill named meeting-notes with content ..."
|
||||
- "Update skill ..."
|
||||
- "Delete skill ..."
|
||||
|
||||
## Example: Install Skills
|
||||
|
||||
This tool can fetch and install skills directly from URLs (supporting GitHub tree/blob, raw markdown, and .zip/.tar archives).
|
||||
|
||||
### Install a single skill from GitHub
|
||||
|
||||
- "Install skill from <https://github.com/anthropics/skills/tree/main/skills/search_manager>"
|
||||
- "Install skill from <https://github.com/Fu-Jie/openwebui-extensions/blob/main/.agent/skills/test-copilot-pipe/SKILL.md>"
|
||||
|
||||
### Batch install multiple skills
|
||||
|
||||
- "Install these skills: ['https://github.com/anthropics/skills/tree/main/skills/search_manager', 'https://github.com/anthropics/skills/tree/main/skills/guide_writer']"
|
||||
|
||||
> **Tip**: For GitHub, the tool automatically resolves directory (tree) URLs by looking for `SKILL.md` or `README.md`.
|
||||
|
||||
## Configuration (Valves)
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --- | ---: | --- |
|
||||
| `SHOW_STATUS` | `True` | Show operation status updates in OpenWebUI status bar. |
|
||||
| `ALLOW_OVERWRITE_ON_CREATE` | `False` | Allow `create_skill`/`install_skill` to overwrite same-name skill by default. |
|
||||
| `INSTALL_FETCH_TIMEOUT` | `12.0` | URL fetch timeout in seconds for skill installation. |
|
||||
|
||||
## Supported Tool Methods
|
||||
|
||||
| Method | Purpose |
|
||||
| --- | --- |
|
||||
| `list_skills` | List current user's skills. |
|
||||
| `show_skill` | Show one skill by `skill_id` or `name`. |
|
||||
| `install_skill` | Install skill from URL into OpenWebUI native skills. |
|
||||
| `create_skill` | Create a new skill (or overwrite when allowed). |
|
||||
| `update_skill` | Update skill fields (`new_name`, `description`, `content`, `is_active`). |
|
||||
| `delete_skill` | Delete a skill by `skill_id` or `name`. |
|
||||
|
||||
## Support
|
||||
|
||||
If this plugin has been useful, a star on [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) is a big motivation for me. Thank you for the support.
|
||||
|
||||
## Others
|
||||
|
||||
- This tool manages OpenWebUI native skill records and supports direct URL installation.
|
||||
- For advanced orchestration, combine with other Pipe/Tool workflows.
|
||||
|
||||
## Changelog
|
||||
|
||||
See full history in the GitHub repository releases and commits.
|
||||
76
plugins/tools/openwebui-skills-manager/README_CN.md
Normal file
76
plugins/tools/openwebui-skills-manager/README_CN.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 🧰 OpenWebUI Skills 管理工具
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.2.1 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
|
||||
|
||||
一个 OpenWebUI 原生 Tool 插件,用于让任意模型直接管理 **Workspace > Skills**。
|
||||
|
||||
## 最新更新
|
||||
|
||||
- `install_skill` 新增 GitHub 技能目录自动发现(例如 `.../tree/main/skills`),可一键安装目录下所有子技能。
|
||||
- 修复语言获取逻辑:前端优先(`__event_call__` + 超时保护),并回退到请求头与用户资料。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **🌐 全模型可用**:只要模型启用了 OpenWebUI Tools,即可调用。
|
||||
- **🛠️ 简化技能管理**:直接管理 OpenWebUI Skills 记录。
|
||||
- **🔐 用户范围安全**:仅操作当前用户可访问的技能。
|
||||
- **📡 友好状态反馈**:每一步操作都有状态栏提示。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 打开 OpenWebUI,进入 **Workspace > Tools**。
|
||||
2. 新建 Tool,粘贴 `openwebui_skills_manager.py`。
|
||||
3. 为当前模型/聊天启用该工具。
|
||||
4. 在对话中让模型调用,例如:
|
||||
- “列出我的 skills”
|
||||
- “显示名为 docs-writer 的 skill”
|
||||
- “创建一个 meeting-notes 技能,内容是 ...”
|
||||
- “更新某个 skill ...”
|
||||
- “删除某个 skill ...”
|
||||
|
||||
## 示例:安装技能 (Install Skills)
|
||||
|
||||
该工具支持从 URL 直接抓取并安装技能(支持 GitHub tree/blob 链接、原始 Markdown 链接以及 .zip/.tar 压缩包)。
|
||||
|
||||
### 从 GitHub 安装单个技能
|
||||
|
||||
- “从 <https://github.com/anthropics/skills/tree/main/skills/search_manager> 安装技能”
|
||||
- “安装技能 <https://github.com/Fu-Jie/openwebui-extensions/blob/main/.agent/skills/test-copilot-pipe/SKILL.md”>
|
||||
|
||||
### 批量安装多个技能
|
||||
|
||||
- “安装这些技能:['https://github.com/anthropics/skills/tree/main/skills/search_manager', 'https://github.com/anthropics/skills/tree/main/skills/guide_writer']”
|
||||
|
||||
> **提示**:对于 GitHub 链接,工具会自动处理目录(tree)地址,并尝试查找目录下的 `SKILL.md` 或 `README.md` 文件。
|
||||
|
||||
## 配置参数(Valves)
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| --- | ---: | --- |
|
||||
| `SHOW_STATUS` | `True` | 是否在 OpenWebUI 状态栏显示操作状态。 |
|
||||
| `ALLOW_OVERWRITE_ON_CREATE` | `False` | 是否允许 `create_skill`/`install_skill` 默认覆盖同名技能。 |
|
||||
| `INSTALL_FETCH_TIMEOUT` | `12.0` | 从 URL 安装技能时的请求超时时间(秒)。 |
|
||||
|
||||
## 支持的方法
|
||||
|
||||
| 方法 | 用途 |
|
||||
| --- | --- |
|
||||
| `list_skills` | 列出当前用户的技能。 |
|
||||
| `show_skill` | 通过 `skill_id` 或 `name` 查看单个技能。 |
|
||||
| `install_skill` | 通过 URL 安装技能到 OpenWebUI 原生 Skills。 |
|
||||
| `create_skill` | 创建新技能(或在允许时覆盖同名技能)。 |
|
||||
| `update_skill` | 更新技能字段(`new_name`、`description`、`content`、`is_active`)。 |
|
||||
| `delete_skill` | 通过 `skill_id` 或 `name` 删除技能。 |
|
||||
|
||||
## 支持
|
||||
|
||||
如果这个插件对你有帮助,欢迎到 [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||
|
||||
## 其他说明
|
||||
|
||||
- 本工具管理 OpenWebUI 原生 Skills 记录,并支持通过 URL 直接安装。
|
||||
- 如需更复杂的工作流编排,可结合其他 Pipe/Tool 方案使用。
|
||||
|
||||
## 更新记录
|
||||
|
||||
完整历史请查看 GitHub 仓库的 commits 与 releases。
|
||||
1416
plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py
Normal file
1416
plugins/tools/openwebui-skills-manager/openwebui_skills_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ def _load_api_key() -> str:
|
||||
raise ValueError("api_key not found in .env file.")
|
||||
|
||||
|
||||
def update_pipe() -> None:
|
||||
def deploy_pipe() -> None:
|
||||
"""Push the latest local github_copilot_sdk.py content to OpenWebUI."""
|
||||
# 1. Load API key
|
||||
try:
|
||||
@@ -100,11 +100,23 @@ def update_pipe() -> None:
|
||||
if response.status_code == 200:
|
||||
print("✅ Successfully updated GitHub Copilot Official SDK Pipe!")
|
||||
else:
|
||||
print(f"❌ Failed to update. Status: {response.status_code}")
|
||||
print(f" Response: {response.text[:500]}")
|
||||
print(
|
||||
f"⚠️ Update failed with status {response.status_code}, attempting to create instead..."
|
||||
)
|
||||
CREATE_URL = "http://localhost:3003/api/v1/functions/create"
|
||||
res_create = requests.post(
|
||||
CREATE_URL, headers=headers, data=json.dumps(payload)
|
||||
)
|
||||
if res_create.status_code == 200:
|
||||
print("✅ Successfully created GitHub Copilot Official SDK Pipe!")
|
||||
else:
|
||||
print(
|
||||
f"❌ Failed to update or create. Status: {res_create.status_code}"
|
||||
)
|
||||
print(f" Response: {res_create.text[:500]}")
|
||||
except Exception as e:
|
||||
print(f"❌ Request error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_pipe()
|
||||
deploy_pipe()
|
||||
@@ -299,8 +299,10 @@ class OpenWebUICommunityClient:
|
||||
if not post_data:
|
||||
return False
|
||||
|
||||
# 严格重建 data 结构,避免包含只读字段(如 data.function.id)
|
||||
current_function = post_data.get("data", {}).get("function", {})
|
||||
# 获取当前帖子的类型和数据结构
|
||||
post_type = post_data.get("type", "function")
|
||||
data_key = post_type if post_type in post_data.get("data", {}) else "function"
|
||||
current_data = post_data.get("data", {}).get(data_key, {})
|
||||
|
||||
# 过滤 metadata,移除 openwebui_id 等系统字段
|
||||
clean_metadata = {
|
||||
@@ -309,22 +311,23 @@ class OpenWebUICommunityClient:
|
||||
if k not in ["openwebui_id", "post_id"]
|
||||
}
|
||||
|
||||
function_data = {
|
||||
"id": current_function.get("id", ""),
|
||||
"name": metadata.get("title", current_function.get("name", "Plugin")),
|
||||
"type": current_function.get("type", "action"),
|
||||
# 重建插件数据结构
|
||||
plugin_data = {
|
||||
"id": current_data.get("id", ""),
|
||||
"name": metadata.get("title", current_data.get("name", "Plugin")),
|
||||
"type": current_data.get("type", "action"),
|
||||
"content": source_code,
|
||||
"meta": {
|
||||
"description": metadata.get(
|
||||
"description",
|
||||
current_function.get("meta", {}).get("description", ""),
|
||||
current_data.get("meta", {}).get("description", ""),
|
||||
),
|
||||
"manifest": clean_metadata,
|
||||
},
|
||||
}
|
||||
|
||||
post_data["data"] = {"function": function_data}
|
||||
post_data["type"] = "function"
|
||||
post_data["data"] = {data_key: plugin_data}
|
||||
post_data["type"] = post_type
|
||||
|
||||
# 更新 README(社区页面展示内容)
|
||||
if readme_content:
|
||||
|
||||
Reference in New Issue
Block a user