Compare commits
59 Commits
v2026.01.2
...
v2026.01.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4d36c32a0 | ||
|
|
6adbcd8d42 | ||
|
|
89c039fe33 | ||
|
|
3a73ccfaa7 | ||
|
|
7eff265e1c | ||
|
|
989b45fc16 | ||
|
|
163d8ce8bd | ||
|
|
4e32e1a1da | ||
|
|
070e9f2456 | ||
|
|
219ba83df3 | ||
|
|
e412aeb93d | ||
|
|
38102ca0c4 | ||
|
|
6ab69fba1c | ||
|
|
e0c0f69dc8 | ||
|
|
7921b14dae | ||
|
|
30cde9e871 | ||
|
|
ac50cd249a | ||
|
|
927db6dbaa | ||
|
|
376c398ac7 | ||
|
|
a167a3cf83 | ||
|
|
c51e7dfdf7 | ||
|
|
1d4d13b34b | ||
|
|
18e8775f38 | ||
|
|
813b019653 | ||
|
|
b0b1542939 | ||
|
|
15f19d8b8d | ||
|
|
82253b114c | ||
|
|
e0bfbf6dd4 | ||
|
|
4689e80e7a | ||
|
|
556e6c1c67 | ||
|
|
3ab84a526d | ||
|
|
bdce96f912 | ||
|
|
4811b99a4b | ||
|
|
fb2a64c07a | ||
|
|
e023e4f2e2 | ||
|
|
0b16b1e0f4 | ||
|
|
59073ad7ac | ||
|
|
8248644c45 | ||
|
|
f38e6394c9 | ||
|
|
0aaa529c6b | ||
|
|
b81a6562a1 | ||
|
|
c5b10db23a | ||
|
|
d16e444643 | ||
|
|
8202468099 | ||
|
|
766e8bd20f | ||
|
|
1214ab5a8c | ||
|
|
ebddbb25f8 | ||
|
|
59545e1110 | ||
|
|
500e090b11 | ||
|
|
a75ee555fa | ||
|
|
6a8c2164cd | ||
|
|
7f7efa325a | ||
|
|
9ba6cb08fc | ||
|
|
1872271a2d | ||
|
|
813b50864a | ||
|
|
b18cefe320 | ||
|
|
a54c359fcf | ||
|
|
8d83221a4a | ||
|
|
1879000720 |
@@ -90,6 +90,9 @@ Reference: `.github/workflows/release.yml`
|
||||
- Action: Automatically updates the plugin code and metadata on OpenWebUI.com using `scripts/publish_plugin.py`.
|
||||
- **Auto-Sync**: If a local plugin has no ID but matches an existing published plugin by **Title**, the script will automatically fetch the ID, update the local file, and proceed with the update.
|
||||
- Requirement: `OPENWEBUI_API_KEY` secret must be set.
|
||||
- **README Link**: When announcing a release, always include the GitHub README URL for the plugin:
|
||||
- Format: `https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/{type}/{name}/README.md`
|
||||
- Example: `https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/README.md`
|
||||
|
||||
### Pull Request Check
|
||||
- Workflow: `.github/workflows/plugin-version-check.yml`
|
||||
|
||||
100
.github/copilot-instructions.md
vendored
100
.github/copilot-instructions.md
vendored
@@ -62,18 +62,41 @@ plugins/
|
||||
│ ├── ACTION_PLUGIN_TEMPLATE_CN.py # Chinese template
|
||||
│ └── README.md
|
||||
├── filters/ # Filter 插件 (输入处理)
|
||||
│ ├── my_filter/
|
||||
│ │ ├── my_filter.py
|
||||
│ │ ├── 我的过滤器.py
|
||||
│ │ ├── README.md
|
||||
│ │ └── README_CN.md
|
||||
│ └── README.md
|
||||
│ └── ...
|
||||
├── pipes/ # Pipe 插件 (输出处理)
|
||||
│ └── ...
|
||||
└── pipelines/ # Pipeline 插件
|
||||
├── pipelines/ # Pipeline 插件
|
||||
└── ...
|
||||
├── debug/ # 调试与开发工具 (Debug & Development Tools)
|
||||
│ ├── my_debug_tool/
|
||||
│ │ ├── debug_script.py
|
||||
│ │ └── notes.md
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
#### 调试目录规范 (Debug Directory Standards)
|
||||
|
||||
`plugins/debug/` 目录用于存放调试用的脚本、临时验证代码或开发笔记。
|
||||
|
||||
**目录结构 (Directory Structure)**:
|
||||
应根据调试工具所属的插件或功能模块进行子目录分类,而非将文件散落在根目录。
|
||||
|
||||
```
|
||||
plugins/debug/
|
||||
├── my_plugin_name/ # 特定插件的调试文件 (Debug files for specific plugin)
|
||||
│ ├── debug_script.py
|
||||
│ └── guides/
|
||||
├── common_tools/ # 通用调试工具 (General debug tools)
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
**规范说明 (Guidelines)**:
|
||||
- **不强制要求 README**: 该目录下的子项目不需要包含 `README.md`。
|
||||
- **发布豁免**: 该目录下的内容**绝不会**被发布脚本处理。
|
||||
- **内容灵活性**: 可以包含 Python 脚本、Markdown 文档、JSON 数据等。
|
||||
- **分类存放**: 任何调试产物(如 `test_*.py`, `inspect_*.py`)都不应直接存放在项目根目录,必须移动到此目录下相应的子文件夹中。
|
||||
|
||||
### 3. 文档字符串规范 (Docstring Standard)
|
||||
|
||||
每个插件文件必须以标准化的文档字符串开头:
|
||||
@@ -100,13 +123,14 @@ description: 插件功能的简短描述。Brief description of plugin functiona
|
||||
| `author_url` | 作者主页链接 | `https://github.com/Fu-Jie/awesome-openwebui` |
|
||||
| `funding_url` | 赞助/项目链接 | `https://github.com/open-webui` |
|
||||
| `version` | 语义化版本号 | `0.1.0`, `1.2.3` |
|
||||
| `icon_url` | 图标 (Base64 编码的 SVG) | 见下方图标规范 |
|
||||
| `icon_url` | 图标 (Base64 编码的 SVG) | 仅 Action 插件**必须**提供。其他类型可选。 |
|
||||
| `requirements` | 额外依赖 (仅 OpenWebUI 环境未安装的) | `python-docx==1.1.2` |
|
||||
| `description` | 功能描述 | `将对话导出为 Word 文档` |
|
||||
|
||||
#### 图标规范 (Icon Guidelines)
|
||||
|
||||
- 图标来源:从 [Lucide Icons](https://lucide.dev/icons/) 获取符合插件功能的图标
|
||||
- 适用范围:Action 插件**必须**提供,其他插件可选
|
||||
- 格式:Base64 编码的 SVG
|
||||
- 获取方法:从 Lucide 下载 SVG,然后使用 Base64 编码
|
||||
- 示例格式:
|
||||
@@ -408,6 +432,51 @@ async def long_running_task_with_notification(self, event_emitter, ...):
|
||||
return task_future.result()
|
||||
```
|
||||
|
||||
### 7. 前端数据获取与交互规范 (Frontend Data Access & Interaction)
|
||||
|
||||
#### 获取前端信息 (Retrieving Frontend Info)
|
||||
|
||||
当需要获取用户浏览器的上下文信息(如语言、时区、LocalStorage)时,**必须**使用 `__event_call__` 的 `execute` 类型,而不是通过文件上传或复杂的 API 请求。
|
||||
|
||||
```python
|
||||
async def _get_frontend_value(self, js_code: str) -> str:
|
||||
"""Helper to execute JS and get return value."""
|
||||
try:
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": js_code,
|
||||
},
|
||||
}
|
||||
)
|
||||
return str(response)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to execute JS: {e}")
|
||||
return ""
|
||||
|
||||
# 示例:获取界面语言 (Get UI Language)
|
||||
async def get_user_language(self):
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
return await self._get_frontend_value(js_code)
|
||||
```
|
||||
|
||||
#### 适用场景与引导 (Usage Guidelines)
|
||||
|
||||
- **语言适配**: 动态获取界面语言 (`ru-RU`, `zh-CN`) 自动切换输出语言。
|
||||
- **时区处理**: 获取 `Intl.DateTimeFormat().resolvedOptions().timeZone` 处理时间。
|
||||
- **客户端存储**: 读取 `localStorage` 中的用户偏好设置。
|
||||
- **硬件能力**: 获取 `navigator.clipboard` 或 `navigator.geolocation` (需授权)。
|
||||
|
||||
**注意**: 即使插件有 `Valves` 配置,也应优先尝试自动探测,提升用户体验。
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Action 插件规范 (Action Plugin Standards)
|
||||
@@ -788,6 +857,19 @@ Filter 实例是**单例 (Singleton)**。
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试规范 (Testing Standards)
|
||||
|
||||
### 1. Copilot SDK 测试模型 (Copilot SDK Test Models)
|
||||
|
||||
在编写 Copilot SDK 相关的测试脚本时 (如 `test_injection.py`, `test_capabilities.py` 等),**必须**优先使用以下免费/低成本模型之一,严禁使用高昂费用的模型进行常规测试,除非用户明确要求:
|
||||
|
||||
- `gpt-5-mini` (首选 / Preferred)
|
||||
- `gpt-4.1`
|
||||
|
||||
此规则适用于所有自动化测试脚本和临时验证脚本。
|
||||
|
||||
---
|
||||
|
||||
## 🔄 工作流与流程 (Workflow & Process)
|
||||
|
||||
### 1. ✅ 开发检查清单 (Development Checklist)
|
||||
@@ -841,8 +923,8 @@ Filter 实例是**单例 (Singleton)**。
|
||||
|
||||
### 4. 🤖 Git Operations (Agent Rules)
|
||||
|
||||
- **允许**: 直接推送到 `main` 分支并发布。
|
||||
- **允许**: 创建功能分支 (`feature/xxx`),推送到功能分支。
|
||||
- **禁止**: 直接推送到 `main`,自动合并到 `main`。
|
||||
|
||||
### 5. 🤝 贡献者认可规范 (Contributor Recognition)
|
||||
|
||||
|
||||
1
.github/workflows/publish_plugin.yml
vendored
1
.github/workflows/publish_plugin.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/**/*.py'
|
||||
- '!plugins/debug/**'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
@@ -246,6 +246,52 @@ jobs:
|
||||
echo "=== Collected Files ==="
|
||||
find release_plugins -name "*.py" -type f | head -20
|
||||
|
||||
- name: Update plugin icon URLs
|
||||
run: |
|
||||
echo "Updating icon_url in plugins to use absolute GitHub URLs..."
|
||||
# Base URL for raw content using the release tag
|
||||
REPO_URL="https://raw.githubusercontent.com/${{ github.repository }}/${{ steps.version.outputs.version }}"
|
||||
|
||||
find release_plugins -name "*.py" | while read -r file; do
|
||||
# $file is like release_plugins/plugins/actions/infographic/infographic.py
|
||||
# Remove release_plugins/ prefix to get the path in the repo
|
||||
src_file="${file#release_plugins/}"
|
||||
src_dir=$(dirname "$src_file")
|
||||
base_name=$(basename "$src_file" .py)
|
||||
|
||||
# Check if a corresponding png exists in the source repository
|
||||
png_file="${src_dir}/${base_name}.png"
|
||||
|
||||
if [ -f "$png_file" ]; then
|
||||
echo "Found icon for $src_file: $png_file"
|
||||
TARGET_ICON_URL="${REPO_URL}/${png_file}"
|
||||
|
||||
# Use python for safe replacement
|
||||
python3 -c "
|
||||
import sys
|
||||
import re
|
||||
|
||||
file_path = '$file'
|
||||
icon_url = '$TARGET_ICON_URL'
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace icon_url: ... with new url
|
||||
# Matches 'icon_url: ...' and replaces it
|
||||
new_content = re.sub(r'^icon_url:.*$', f'icon_url: {icon_url}', content, flags=re.MULTILINE)
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
print(f'Successfully updated icon_url in {file_path}')
|
||||
except Exception as e:
|
||||
print(f'Error updating {file_path}: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Debug Filenames
|
||||
run: |
|
||||
python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')"
|
||||
|
||||
28
README.md
28
README.md
@@ -10,28 +10,28 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
<!-- STATS_START -->
|
||||
## 📊 Community Stats
|
||||
|
||||
> 🕐 Auto-updated: 2026-01-20 19:10
|
||||
> 🕐 Auto-updated: 2026-01-28 09:37
|
||||
|
||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **137** | **134** | **25** |
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **165** | **167** | **35** |
|
||||
|
||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| **16** | **1887** | **22101** | **120** | **147** |
|
||||
| **19** | **2553** | **29974** | **150** | **198** |
|
||||
|
||||
### 🔥 Top 6 Popular Plugins
|
||||
|
||||
> 🕐 Auto-updated: 2026-01-20 19:10
|
||||
> 🕐 Auto-updated: 2026-01-28 09:37
|
||||
|
||||
| Rank | Plugin | Version | Downloads | Views | Updated |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 550 | 4939 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 282 | 2667 | 2026-01-18 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 215 | 844 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.1 | 189 | 2051 | 2026-01-20 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 170 | 1457 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 144 | 2395 | 2026-01-17 |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 670 | 5919 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.5.0 | 441 | 4010 | 2026-01-27 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 265 | 1099 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.2 | 240 | 2593 | 2026-01-21 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 240 | 1951 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 179 | 2805 | 2026-01-17 |
|
||||
|
||||
*See full stats in [Community Stats Report](./docs/community-stats.md)*
|
||||
<!-- STATS_END -->
|
||||
@@ -43,6 +43,7 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
||||
Located in the `plugins/` directory, containing Python-based enhancements:
|
||||
|
||||
#### Actions
|
||||
|
||||
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
|
||||
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
|
||||
- **Flash Card** (`flash-card`): Quickly generates beautiful flashcards for learning.
|
||||
@@ -51,12 +52,18 @@ Located in the `plugins/` directory, containing Python-based enhancements:
|
||||
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
|
||||
|
||||
#### Filters
|
||||
|
||||
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
|
||||
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
|
||||
- **Folder Memory** (`folder-memory`): Automatically extracts project rules from conversations and injects them into the folder's system prompt.
|
||||
- **Markdown Normalizer** (`markdown_normalizer`): Fixes common Markdown formatting issues in LLM outputs.
|
||||
|
||||
#### Pipes
|
||||
|
||||
- **GitHub Copilot SDK** (`github-copilot-sdk`): Official GitHub Copilot SDK integration. Supports dynamic models, multi-turn conversation, streaming, multimodal input, and infinite sessions.
|
||||
|
||||
#### Pipelines
|
||||
|
||||
- **MoE Prompt Refiner** (`moe_prompt_refiner`): Refines prompts for Mixture of Experts (MoE) summary requests to generate high-quality comprehensive reports.
|
||||
|
||||
### 🎯 Prompts
|
||||
@@ -101,6 +108,7 @@ This project is a collection of resources and does not require a Python environm
|
||||
### Contributing
|
||||
|
||||
If you have great prompts or plugins to share:
|
||||
|
||||
1. Fork this repository.
|
||||
2. Add your files to the appropriate `prompts/` or `plugins/` directory.
|
||||
3. Submit a Pull Request.
|
||||
|
||||
26
README_CN.md
26
README_CN.md
@@ -7,28 +7,28 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
<!-- STATS_START -->
|
||||
## 📊 社区统计
|
||||
|
||||
> 🕐 自动更新于 2026-01-20 19:10
|
||||
> 🕐 自动更新于 2026-01-28 09:37
|
||||
|
||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **137** | **134** | **25** |
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **165** | **167** | **35** |
|
||||
|
||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| **16** | **1887** | **22101** | **120** | **147** |
|
||||
| **19** | **2553** | **29974** | **150** | **198** |
|
||||
|
||||
### 🔥 热门插件 Top 6
|
||||
|
||||
> 🕐 自动更新于 2026-01-20 19:10
|
||||
> 🕐 自动更新于 2026-01-28 09:37
|
||||
|
||||
| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 550 | 4939 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 282 | 2667 | 2026-01-18 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 215 | 844 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.1 | 189 | 2051 | 2026-01-20 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 170 | 1457 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 144 | 2395 | 2026-01-17 |
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 670 | 5919 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.5.0 | 441 | 4010 | 2026-01-27 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 265 | 1099 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.2 | 240 | 2593 | 2026-01-21 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 240 | 1951 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 179 | 2805 | 2026-01-17 |
|
||||
|
||||
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||
<!-- STATS_END -->
|
||||
@@ -40,6 +40,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
位于 `plugins/` 目录,包含各类 Python 编写的功能增强插件:
|
||||
|
||||
#### Actions (交互增强)
|
||||
|
||||
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
|
||||
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
|
||||
- **Flash Card** (`flash-card`): 快速生成精美的学习记忆卡片。
|
||||
@@ -48,6 +49,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
|
||||
|
||||
#### Filters (消息处理)
|
||||
|
||||
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
|
||||
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
|
||||
- **Folder Memory** (`folder-memory`): 自动从对话中提取项目规则并注入到文件夹系统提示词中。
|
||||
@@ -57,9 +59,12 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
- **Multi-Model Context Merger** (`multi_model_context_merger`): 自动合并并注入多模型回答的上下文。
|
||||
|
||||
#### Pipes (模型管道)
|
||||
|
||||
- **GitHub Copilot SDK** (`github-copilot-sdk`): GitHub Copilot SDK 官方集成。支持动态模型、多轮对话、流式输出、图片输入及无限会话。
|
||||
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
|
||||
|
||||
#### Pipelines (工作流管道)
|
||||
|
||||
- **MoE Prompt Refiner** (`moe_prompt_refiner`): 优化多模型 (MoE) 汇总请求的提示词,生成高质量的综合报告。
|
||||
|
||||
### 🎯 提示词 (Prompts)
|
||||
@@ -107,6 +112,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
### 贡献代码
|
||||
|
||||
如果你有优质的提示词或插件想要分享:
|
||||
|
||||
1. Fork 本仓库。
|
||||
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
||||
3. 提交 Pull Request。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "downloads",
|
||||
"message": "1.9k",
|
||||
"message": "2.6k",
|
||||
"color": "blue",
|
||||
"namedLogo": "openwebui"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "followers",
|
||||
"message": "137",
|
||||
"message": "165",
|
||||
"color": "blue"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "plugins",
|
||||
"message": "16",
|
||||
"message": "19",
|
||||
"color": "green"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "points",
|
||||
"message": "134",
|
||||
"message": "167",
|
||||
"color": "orange"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "upvotes",
|
||||
"message": "120",
|
||||
"message": "150",
|
||||
"color": "brightgreen"
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"total_posts": 16,
|
||||
"total_downloads": 1887,
|
||||
"total_views": 22101,
|
||||
"total_upvotes": 120,
|
||||
"total_posts": 19,
|
||||
"total_downloads": 2553,
|
||||
"total_views": 29974,
|
||||
"total_upvotes": 150,
|
||||
"total_downvotes": 2,
|
||||
"total_saves": 147,
|
||||
"total_comments": 24,
|
||||
"total_saves": 198,
|
||||
"total_comments": 40,
|
||||
"by_type": {
|
||||
"action": 14,
|
||||
"unknown": 2
|
||||
"pipe": 1,
|
||||
"unknown": 3,
|
||||
"filter": 1
|
||||
},
|
||||
"posts": [
|
||||
{
|
||||
@@ -18,10 +20,10 @@
|
||||
"version": "0.9.1",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
||||
"downloads": 550,
|
||||
"views": 4939,
|
||||
"upvotes": 15,
|
||||
"saves": 30,
|
||||
"downloads": 670,
|
||||
"views": 5919,
|
||||
"upvotes": 17,
|
||||
"saves": 38,
|
||||
"comments": 11,
|
||||
"created_at": "2025-12-30",
|
||||
"updated_at": "2026-01-17",
|
||||
@@ -31,16 +33,16 @@
|
||||
"title": "📊 Smart Infographic (AntV)",
|
||||
"slug": "smart_infographic_ad6f0c7f",
|
||||
"type": "action",
|
||||
"version": "1.4.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": 282,
|
||||
"views": 2667,
|
||||
"upvotes": 14,
|
||||
"saves": 21,
|
||||
"comments": 3,
|
||||
"downloads": 441,
|
||||
"views": 4010,
|
||||
"upvotes": 19,
|
||||
"saves": 28,
|
||||
"comments": 10,
|
||||
"created_at": "2025-12-28",
|
||||
"updated_at": "2026-01-18",
|
||||
"updated_at": "2026-01-27",
|
||||
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
||||
},
|
||||
{
|
||||
@@ -50,8 +52,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": 215,
|
||||
"views": 844,
|
||||
"downloads": 265,
|
||||
"views": 1099,
|
||||
"upvotes": 4,
|
||||
"saves": 6,
|
||||
"comments": 0,
|
||||
@@ -63,16 +65,16 @@
|
||||
"title": "Async Context Compression",
|
||||
"slug": "async_context_compression_b1655bc8",
|
||||
"type": "action",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||
"downloads": 189,
|
||||
"views": 2051,
|
||||
"downloads": 240,
|
||||
"views": 2593,
|
||||
"upvotes": 9,
|
||||
"saves": 22,
|
||||
"saves": 27,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-01-20",
|
||||
"updated_at": "2026-01-21",
|
||||
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
||||
},
|
||||
{
|
||||
@@ -82,10 +84,10 @@
|
||||
"version": "0.4.3",
|
||||
"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": 170,
|
||||
"views": 1457,
|
||||
"downloads": 240,
|
||||
"views": 1951,
|
||||
"upvotes": 8,
|
||||
"saves": 17,
|
||||
"saves": 21,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-03",
|
||||
"updated_at": "2026-01-17",
|
||||
@@ -98,10 +100,10 @@
|
||||
"version": "0.2.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||
"downloads": 144,
|
||||
"views": 2395,
|
||||
"upvotes": 10,
|
||||
"saves": 12,
|
||||
"downloads": 179,
|
||||
"views": 2805,
|
||||
"upvotes": 11,
|
||||
"saves": 13,
|
||||
"comments": 2,
|
||||
"created_at": "2025-12-30",
|
||||
"updated_at": "2026-01-17",
|
||||
@@ -114,10 +116,10 @@
|
||||
"version": "1.2.4",
|
||||
"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.",
|
||||
"downloads": 96,
|
||||
"views": 2234,
|
||||
"downloads": 164,
|
||||
"views": 2910,
|
||||
"upvotes": 10,
|
||||
"saves": 17,
|
||||
"saves": 22,
|
||||
"comments": 5,
|
||||
"created_at": "2026-01-12",
|
||||
"updated_at": "2026-01-19",
|
||||
@@ -130,10 +132,10 @@
|
||||
"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": 73,
|
||||
"views": 707,
|
||||
"downloads": 96,
|
||||
"views": 880,
|
||||
"upvotes": 4,
|
||||
"saves": 7,
|
||||
"saves": 8,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-08",
|
||||
"updated_at": "2026-01-08",
|
||||
@@ -146,11 +148,11 @@
|
||||
"version": "0.4.3",
|
||||
"author": "Fu-Jie",
|
||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||
"downloads": 65,
|
||||
"views": 1335,
|
||||
"downloads": 90,
|
||||
"views": 1710,
|
||||
"upvotes": 11,
|
||||
"saves": 3,
|
||||
"comments": 1,
|
||||
"saves": 4,
|
||||
"comments": 4,
|
||||
"created_at": "2026-01-04",
|
||||
"updated_at": "2026-01-17",
|
||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||
@@ -159,18 +161,34 @@
|
||||
"title": "📊 智能信息图 (AntV Infographic)",
|
||||
"slug": "智能信息图_e04a48ff",
|
||||
"type": "action",
|
||||
"version": "1.4.9",
|
||||
"version": "1.5.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||
"downloads": 43,
|
||||
"views": 704,
|
||||
"upvotes": 6,
|
||||
"downloads": 48,
|
||||
"views": 811,
|
||||
"upvotes": 7,
|
||||
"saves": 0,
|
||||
"comments": 0,
|
||||
"created_at": "2025-12-28",
|
||||
"updated_at": "2026-01-17",
|
||||
"updated_at": "2026-01-27",
|
||||
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
||||
},
|
||||
{
|
||||
"title": "📂 Folder Memory – Auto-Evolving Project Context",
|
||||
"slug": "folder_memory_auto_evolving_project_context_4a9875b2",
|
||||
"type": "filter",
|
||||
"version": "0.1.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Automatically extracts project rules from conversations and injects them into the folder's system prompt.",
|
||||
"downloads": 29,
|
||||
"views": 834,
|
||||
"upvotes": 4,
|
||||
"saves": 7,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-20",
|
||||
"updated_at": "2026-01-20",
|
||||
"url": "https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2"
|
||||
},
|
||||
{
|
||||
"title": "思维导图",
|
||||
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
||||
@@ -178,15 +196,31 @@
|
||||
"version": "0.9.1",
|
||||
"author": "Fu-Jie",
|
||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||
"downloads": 24,
|
||||
"views": 407,
|
||||
"upvotes": 3,
|
||||
"downloads": 28,
|
||||
"views": 468,
|
||||
"upvotes": 4,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2025-12-31",
|
||||
"updated_at": "2026-01-17",
|
||||
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
||||
},
|
||||
{
|
||||
"title": "异步上下文压缩",
|
||||
"slug": "异步上下文压缩_5c0617cb",
|
||||
"type": "action",
|
||||
"version": "1.2.2",
|
||||
"author": "Fu-Jie",
|
||||
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||
"downloads": 23,
|
||||
"views": 518,
|
||||
"upvotes": 5,
|
||||
"saves": 2,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-01-21",
|
||||
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
||||
},
|
||||
{
|
||||
"title": "闪记卡 (Flash Card)",
|
||||
"slug": "闪记卡生成插件_4a31eac3",
|
||||
@@ -194,9 +228,9 @@
|
||||
"version": "0.2.4",
|
||||
"author": "Fu-Jie",
|
||||
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
||||
"downloads": 16,
|
||||
"views": 453,
|
||||
"upvotes": 5,
|
||||
"downloads": 20,
|
||||
"views": 531,
|
||||
"upvotes": 6,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2025-12-30",
|
||||
@@ -204,20 +238,20 @@
|
||||
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
||||
},
|
||||
{
|
||||
"title": "异步上下文压缩",
|
||||
"slug": "异步上下文压缩_5c0617cb",
|
||||
"type": "action",
|
||||
"version": "1.2.1",
|
||||
"title": "GitHub Copilot Official SDK Pipe",
|
||||
"slug": "github_copilot_official_sdk_pipe_ce96f7b4",
|
||||
"type": "pipe",
|
||||
"version": "0.2.3",
|
||||
"author": "Fu-Jie",
|
||||
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||
"downloads": 14,
|
||||
"views": 377,
|
||||
"upvotes": 5,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-01-20",
|
||||
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
||||
"description": "Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, and frontend debug logging.",
|
||||
"downloads": 10,
|
||||
"views": 517,
|
||||
"upvotes": 8,
|
||||
"saves": 2,
|
||||
"comments": 1,
|
||||
"created_at": "2026-01-26",
|
||||
"updated_at": "2026-01-26",
|
||||
"url": "https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4"
|
||||
},
|
||||
{
|
||||
"title": "精读",
|
||||
@@ -226,8 +260,8 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
||||
"downloads": 6,
|
||||
"views": 261,
|
||||
"downloads": 10,
|
||||
"views": 321,
|
||||
"upvotes": 3,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
@@ -235,6 +269,22 @@
|
||||
"updated_at": "2026-01-08",
|
||||
"url": "https://openwebui.com/posts/精读_99830b0f"
|
||||
},
|
||||
{
|
||||
"title": "🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager",
|
||||
"slug": "open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e",
|
||||
"type": "unknown",
|
||||
"version": "",
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 705,
|
||||
"upvotes": 7,
|
||||
"saves": 9,
|
||||
"comments": 5,
|
||||
"created_at": "2026-01-25",
|
||||
"updated_at": "2026-01-25",
|
||||
"url": "https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e"
|
||||
},
|
||||
{
|
||||
"title": "Review of Claude Haiku 4.5",
|
||||
"slug": "review_of_claude_haiku_45_41b0db39",
|
||||
@@ -243,7 +293,7 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 62,
|
||||
"views": 101,
|
||||
"upvotes": 1,
|
||||
"saves": 0,
|
||||
"comments": 0,
|
||||
@@ -259,7 +309,7 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 1208,
|
||||
"views": 1291,
|
||||
"upvotes": 12,
|
||||
"saves": 8,
|
||||
"comments": 2,
|
||||
@@ -273,11 +323,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": 137,
|
||||
"following": 2,
|
||||
"total_points": 134,
|
||||
"post_points": 118,
|
||||
"comment_points": 16,
|
||||
"contributions": 25
|
||||
"followers": 165,
|
||||
"following": 3,
|
||||
"total_points": 167,
|
||||
"post_points": 148,
|
||||
"comment_points": 19,
|
||||
"contributions": 35
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,45 @@
|
||||
# 📊 OpenWebUI Community Stats Report
|
||||
|
||||
> 📅 Updated: 2026-01-20 19:10
|
||||
> 📅 Updated: 2026-01-28 09:37
|
||||
|
||||
## 📈 Overview
|
||||
|
||||
| Metric | Value |
|
||||
|------|------|
|
||||
| 📝 Total Posts | 16 |
|
||||
| ⬇️ Total Downloads | 1887 |
|
||||
| 👁️ Total Views | 22101 |
|
||||
| 👍 Total Upvotes | 120 |
|
||||
| 💾 Total Saves | 147 |
|
||||
| 💬 Total Comments | 24 |
|
||||
| 📝 Total Posts | 19 |
|
||||
| ⬇️ Total Downloads | 2553 |
|
||||
| 👁️ Total Views | 29974 |
|
||||
| 👍 Total Upvotes | 150 |
|
||||
| 💾 Total Saves | 198 |
|
||||
| 💬 Total Comments | 40 |
|
||||
|
||||
## 📂 By Type
|
||||
|
||||
- **action**: 14
|
||||
- **unknown**: 2
|
||||
- **pipe**: 1
|
||||
- **unknown**: 3
|
||||
- **filter**: 1
|
||||
|
||||
## 📋 Posts List
|
||||
|
||||
| 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 | 0.9.1 | 550 | 4939 | 15 | 30 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 282 | 2667 | 14 | 21 | 2026-01-18 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 215 | 844 | 4 | 6 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.1 | 189 | 2051 | 9 | 22 | 2026-01-20 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 170 | 1457 | 8 | 17 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 144 | 2395 | 10 | 12 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 96 | 2234 | 10 | 17 | 2026-01-19 |
|
||||
| 8 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 73 | 707 | 4 | 7 | 2026-01-08 |
|
||||
| 9 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 65 | 1335 | 11 | 3 | 2026-01-17 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 43 | 704 | 6 | 0 | 2026-01-17 |
|
||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 24 | 407 | 3 | 1 | 2026-01-17 |
|
||||
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 16 | 453 | 5 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.1 | 14 | 377 | 5 | 1 | 2026-01-20 |
|
||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 261 | 3 | 1 | 2026-01-08 |
|
||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 62 | 1 | 0 | 2026-01-14 |
|
||||
| 16 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1208 | 12 | 8 | 2026-01-10 |
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 670 | 5919 | 17 | 38 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.5.0 | 441 | 4010 | 19 | 28 | 2026-01-27 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 265 | 1099 | 4 | 6 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.2 | 240 | 2593 | 9 | 27 | 2026-01-21 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 240 | 1951 | 8 | 21 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 179 | 2805 | 11 | 13 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 164 | 2910 | 10 | 22 | 2026-01-19 |
|
||||
| 8 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 96 | 880 | 4 | 8 | 2026-01-08 |
|
||||
| 9 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 90 | 1710 | 11 | 4 | 2026-01-17 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.5.0 | 48 | 811 | 7 | 0 | 2026-01-27 |
|
||||
| 11 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter | 0.1.0 | 29 | 834 | 4 | 7 | 2026-01-20 |
|
||||
| 12 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 28 | 468 | 4 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.2 | 23 | 518 | 5 | 2 | 2026-01-21 |
|
||||
| 14 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 20 | 531 | 6 | 1 | 2026-01-17 |
|
||||
| 15 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe | 0.2.3 | 10 | 517 | 8 | 2 | 2026-01-26 |
|
||||
| 16 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 10 | 321 | 3 | 1 | 2026-01-08 |
|
||||
| 17 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | unknown | | 0 | 705 | 7 | 9 | 2026-01-25 |
|
||||
| 18 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 101 | 1 | 0 | 2026-01-14 |
|
||||
| 19 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1291 | 12 | 8 | 2026-01-10 |
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
# 📊 OpenWebUI 社区统计报告
|
||||
|
||||
> 📅 更新时间: 2026-01-20 19:10
|
||||
> 📅 更新时间: 2026-01-28 09:37
|
||||
|
||||
## 📈 总览
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 📝 发布数量 | 16 |
|
||||
| ⬇️ 总下载量 | 1887 |
|
||||
| 👁️ 总浏览量 | 22101 |
|
||||
| 👍 总点赞数 | 120 |
|
||||
| 💾 总收藏数 | 147 |
|
||||
| 💬 总评论数 | 24 |
|
||||
| 📝 发布数量 | 19 |
|
||||
| ⬇️ 总下载量 | 2553 |
|
||||
| 👁️ 总浏览量 | 29974 |
|
||||
| 👍 总点赞数 | 150 |
|
||||
| 💾 总收藏数 | 198 |
|
||||
| 💬 总评论数 | 40 |
|
||||
|
||||
## 📂 按类型分类
|
||||
|
||||
- **action**: 14
|
||||
- **unknown**: 2
|
||||
- **pipe**: 1
|
||||
- **unknown**: 3
|
||||
- **filter**: 1
|
||||
|
||||
## 📋 发布列表
|
||||
|
||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 550 | 4939 | 15 | 30 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 282 | 2667 | 14 | 21 | 2026-01-18 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 215 | 844 | 4 | 6 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.1 | 189 | 2051 | 9 | 22 | 2026-01-20 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 170 | 1457 | 8 | 17 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 144 | 2395 | 10 | 12 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 96 | 2234 | 10 | 17 | 2026-01-19 |
|
||||
| 8 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 73 | 707 | 4 | 7 | 2026-01-08 |
|
||||
| 9 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 65 | 1335 | 11 | 3 | 2026-01-17 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 43 | 704 | 6 | 0 | 2026-01-17 |
|
||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 24 | 407 | 3 | 1 | 2026-01-17 |
|
||||
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 16 | 453 | 5 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.1 | 14 | 377 | 5 | 1 | 2026-01-20 |
|
||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 261 | 3 | 1 | 2026-01-08 |
|
||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 62 | 1 | 0 | 2026-01-14 |
|
||||
| 16 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1208 | 12 | 8 | 2026-01-10 |
|
||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 670 | 5919 | 17 | 38 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.5.0 | 441 | 4010 | 19 | 28 | 2026-01-27 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 265 | 1099 | 4 | 6 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.2 | 240 | 2593 | 9 | 27 | 2026-01-21 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 240 | 1951 | 8 | 21 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 179 | 2805 | 11 | 13 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 164 | 2910 | 10 | 22 | 2026-01-19 |
|
||||
| 8 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 96 | 880 | 4 | 8 | 2026-01-08 |
|
||||
| 9 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 90 | 1710 | 11 | 4 | 2026-01-17 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.5.0 | 48 | 811 | 7 | 0 | 2026-01-27 |
|
||||
| 11 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter | 0.1.0 | 29 | 834 | 4 | 7 | 2026-01-20 |
|
||||
| 12 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 28 | 468 | 4 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.2 | 23 | 518 | 5 | 2 | 2026-01-21 |
|
||||
| 14 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 20 | 531 | 6 | 1 | 2026-01-17 |
|
||||
| 15 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe | 0.2.3 | 10 | 517 | 8 | 2 | 2026-01-26 |
|
||||
| 16 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 10 | 321 | 3 | 1 | 2026-01-08 |
|
||||
| 17 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | unknown | | 0 | 705 | 7 | 9 | 2026-01-25 |
|
||||
| 18 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 101 | 1 | 0 | 2026-01-14 |
|
||||
| 19 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1291 | 12 | 8 | 2026-01-10 |
|
||||
|
||||
@@ -349,6 +349,53 @@ await __event_emitter__(
|
||||
)
|
||||
```
|
||||
|
||||
#### Advanced Use Case: Retrieving Frontend Data
|
||||
|
||||
One of the most powerful capabilities of the `execute` event type is the ability to fetch data from the browser environment (JavaScript) and return it to your Python backend. This allows plugins to access information like:
|
||||
|
||||
- `localStorage` items (user preferences, tokens)
|
||||
- `navigator` properties (language, geolocation, platform)
|
||||
- `document` properties (cookies, URL parameters)
|
||||
|
||||
**How it works:**
|
||||
The JavaScript code you provide in the `"code"` field is executed in the browser. If your JS code includes a `return` statement, that value is sent back to Python as the result of `await __event_call__`.
|
||||
|
||||
**Example: Getting the User's UI Language**
|
||||
|
||||
```python
|
||||
try:
|
||||
# Execute JS on the frontend to get language settings
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
# This JS code runs in the browser.
|
||||
# The 'return' value is sent back to Python.
|
||||
"code": """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
""",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 'response' will contain the string returned by JS (e.g., "en-US", "zh-CN")
|
||||
# Note: Wrap in try-except to handle potential timeouts or JS errors
|
||||
logger.info(f"Frontend Language: {response}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get frontend data: {e}")
|
||||
```
|
||||
|
||||
**Key capabilities unlocked:**
|
||||
- **Context Awareness:** Adapt responses based on user time zone or language.
|
||||
- **Client-Side Storage:** Use `localStorage` to persist simple plugin settings without a database.
|
||||
- **Hardware Access:** Request geolocation or clipboard access (requires user permission).
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ When & Where to Use Events
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Smart Mind Map
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.9.1</span>
|
||||
<span class="version-badge">v0.9.2</span>
|
||||
|
||||
Intelligently analyzes text content and generates interactive mind maps for better visualization and understanding.
|
||||
|
||||
@@ -17,7 +17,7 @@ The Smart Mind Map plugin transforms text content into beautiful, interactive mi
|
||||
- :material-gesture-swipe: **Rich Controls**: Zoom, reset view, expand level selector (All/2/3) and fullscreen
|
||||
- :material-palette: **Theme Aware**: Auto-detects OpenWebUI light/dark theme with manual toggle
|
||||
- :material-download: **One-Click Export**: Download high-res PNG, copy SVG, or copy Markdown source
|
||||
- :material-translate: **Multi-language**: Adapts output language to the user context
|
||||
- :material-translate: **Multi-language**: Matches output language to the input text
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Smart Mind Map(智能思维导图)
|
||||
|
||||
<span class="category-badge action">Action</span>
|
||||
<span class="version-badge">v0.9.1</span>
|
||||
<span class="version-badge">v0.9.2</span>
|
||||
|
||||
智能分析文本内容,生成交互式思维导图,帮助你更直观地理解信息结构。
|
||||
|
||||
@@ -17,7 +17,7 @@ Smart Mind Map 会将文本转换成漂亮的交互式思维导图。插件会
|
||||
- :material-gesture-swipe: **丰富控制**:缩放/重置、展开层级(全部/2/3 级)与全屏
|
||||
- :material-palette: **主题感知**:自动检测 OpenWebUI 亮/暗色主题并支持手动切换
|
||||
- :material-download: **一键导出**:下载高分辨率 PNG、复制 SVG 或 Markdown
|
||||
- :material-translate: **多语言**:根据用户语言自动输出
|
||||
- :material-translate: **多语言**:输出语言与输入文本一致
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Async Context Compression
|
||||
|
||||
<span class="category-badge filter">Filter</span>
|
||||
<span class="version-badge">v1.2.1</span>
|
||||
<span class="version-badge">v1.2.2</span>
|
||||
|
||||
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Async Context Compression(异步上下文压缩)
|
||||
|
||||
<span class="category-badge filter">Filter</span>
|
||||
<span class="version-badge">v1.2.1</span>
|
||||
<span class="version-badge">v1.2.2</span>
|
||||
|
||||
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# Folder Memory
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.1.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
---
|
||||
|
||||
### 📌 What's new in 0.1.0
|
||||
- **Initial Release**: Automated "Project Rules" management for OpenWebUI folders.
|
||||
- **Folder-Level Persistence**: Automatically updates folder system prompts with extracted rules.
|
||||
- **Optimized Performance**: Runs asynchronously and supports `PRIORITY` configuration for seamless integration with other filters.
|
||||
|
||||
---
|
||||
|
||||
**Folder Memory** is an intelligent context filter plugin for OpenWebUI. It automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||
|
||||
This ensures that all future conversations within that folder share the same evolved context and rules, without manual updates.
|
||||
@@ -11,6 +22,10 @@ This ensures that all future conversations within that folder share the same evo
|
||||
- **Async Processing**: Runs in the background without blocking the user's chat experience.
|
||||
- **ORM Integration**: Directly updates folder data using OpenWebUI's internal models for reliability.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Conversations must occur inside a folder.** This plugin only triggers when a chat belongs to a folder (i.e., you need to create a folder in OpenWebUI and start a conversation within it).
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy `folder_memory.py` to your OpenWebUI `plugins/filters/` directory (or upload via Admin UI).
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 文件夹记忆 (Folder Memory)
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.1.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
---
|
||||
|
||||
### 📌 0.1.0 版本特性
|
||||
- **首个版本发布**:专注于自动化的“项目规则”管理。
|
||||
- **文件夹级持久化**:自动将提取的规则回写到文件夹系统提示词中。
|
||||
- **性能优化**:采用异步处理机制,并支持 `PRIORITY` 配置,确保与其他过滤器(如上下文压缩)完美协作。
|
||||
|
||||
---
|
||||
|
||||
**文件夹记忆 (Folder Memory)** 是一个 OpenWebUI 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
|
||||
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||
@@ -11,6 +22,10 @@
|
||||
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- **对话必须在文件夹内进行。** 此插件仅在聊天属于某个文件夹时触发(即您需要先在 OpenWebUI 中创建一个文件夹,并在其内部开始对话)。
|
||||
|
||||
## 安装指南
|
||||
|
||||
1. 将 `folder_memory.py` (或中文版 `folder_memory_cn.py`) 复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||
|
||||
@@ -22,7 +22,7 @@ Filters act as middleware in the message pipeline:
|
||||
|
||||
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
|
||||
|
||||
**Version:** 1.2.1
|
||||
**Version:** 1.2.2
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Filter 充当消息管线中的中间件:
|
||||
|
||||
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
||||
|
||||
**版本:** 1.2.1
|
||||
**版本:** 1.2.2
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ OpenWebUI supports four types of plugins, each serving a different purpose:
|
||||
|
||||
| Plugin | Type | Description | Version |
|
||||
|--------|------|-------------|---------|
|
||||
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.1 |
|
||||
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.2 |
|
||||
| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.4.9 |
|
||||
| [Flash Card](actions/flash-card.md) | Action | Create beautiful learning flashcards | 0.2.4 |
|
||||
| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 0.3.7 |
|
||||
|
||||
@@ -48,7 +48,7 @@ OpenWebUI 支持四种类型的插件,每种都有不同的用途:
|
||||
|
||||
| 插件 | 类型 | 描述 | 版本 |
|
||||
|--------|------|-------------|---------|
|
||||
| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.1 |
|
||||
| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.2 |
|
||||
| [Smart Infographic(智能信息图)](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.4.9 |
|
||||
| [Flash Card(闪记卡)](actions/flash-card.md) | Action | 生成精美学习卡片 | 0.2.4 |
|
||||
| [Export to Excel(导出到 Excel)](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 0.3.7 |
|
||||
|
||||
120
docs/plugins/pipes/github-copilot-sdk.md
Normal file
120
docs/plugins/pipes/github-copilot-sdk.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# GitHub Copilot SDK Pipe for OpenWebUI
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.2.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that allows you to use GitHub Copilot models (such as `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`) directly within OpenWebUI. It is built upon the official [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk), providing a native integration experience.
|
||||
|
||||
## 🚀 What's New (v0.2.3)
|
||||
|
||||
* **🧩 Per-user Overrides**: Added user-level overrides for `REASONING_EFFORT`, `CLI_PATH`, `DEBUG`, `SHOW_THINKING`, and `MODEL_ID`.
|
||||
* **🧠 Thinking Output Reliability**: Thinking visibility now respects the user setting and is correctly passed into streaming.
|
||||
* **📝 Formatting Enforcement**: Added automatic formatting hints to ensure outputs are well-structured (paragraphs, lists).
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
* **🚀 Official SDK Integration**: Built on the official SDK for stability and reliability.
|
||||
* **🛠️ Custom Tools Support**: Example tools included (random number). Easy to extend with your own tools.
|
||||
* **💬 Multi-turn Conversation**: Automatically concatenates history context so Copilot understands your previous messages.
|
||||
* **🌊 Streaming Output**: Supports typewriter effect for fast responses.
|
||||
* **🖼️ Multimodal Support**: Supports image uploads, automatically converting them to attachments for Copilot (requires model support).
|
||||
* **🛠️ Zero-config Installation**: Automatically detects and downloads the GitHub Copilot CLI, ready to use out of the box.
|
||||
* **🔑 Secure Authentication**: Supports Fine-grained Personal Access Tokens for minimized permissions.
|
||||
* **🐛 Debug Mode**: Built-in detailed log output (browser console) for easy troubleshooting.
|
||||
* **⚠️ Single Node Only**: Due to local session storage, this plugin currently supports single-node OpenWebUI deployment or multi-node with sticky sessions enabled.
|
||||
|
||||
## 📦 Installation & Usage
|
||||
|
||||
### 1. Import Function
|
||||
|
||||
1. Open OpenWebUI.
|
||||
2. Go to **Workspace** -> **Functions**.
|
||||
3. Click **+** (Create Function).
|
||||
4. Paste the content of `github_copilot_sdk.py` (or `github_copilot_sdk_cn.py` for Chinese) completely.
|
||||
5. Save.
|
||||
|
||||
### 2. Configure Valves (Settings)
|
||||
|
||||
Find "GitHub Copilot" in the function list and click the **⚙️ (Valves)** icon to configure:
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| **GH_TOKEN** | **(Required)** Your GitHub Token. | - |
|
||||
| **MODEL_ID** | The model name to use. Recommended `gpt-5-mini` or `gpt-5`. | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Path to the Copilot CLI. Will download automatically if not found. | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | Whether to enable debug logs (output to browser console). | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI log level: none, error, warning, info, debug, all. | `error` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | Show session workspace path and summary in debug mode. | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | Exclude models containing these keywords (comma separated). | - |
|
||||
| **WORKSPACE_DIR** | Restricted workspace directory for file operations. | - |
|
||||
| **INFINITE_SESSION** | Enable Infinite Sessions (automatic context compaction). | `True` |
|
||||
| **COMPACTION_THRESHOLD** | Background compaction threshold (0.0-1.0). | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | Buffer exhaustion threshold (0.0-1.0). | `0.95` |
|
||||
| **TIMEOUT** | Timeout for each stream chunk (seconds). | `300` |
|
||||
| **CUSTOM_ENV_VARS** | Custom environment variables (JSON format). | - |
|
||||
| **REASONING_EFFORT** | Reasoning effort level: low, medium, high. `xhigh` is supported for gpt-5.2-codex. | `medium` |
|
||||
| **ENFORCE_FORMATTING** | Add formatting instructions to system prompt for better readability. | `True` |
|
||||
| **ENABLE_TOOLS** | Enable custom tools (example: random number). | `False` |
|
||||
| **AVAILABLE_TOOLS** | Available tools: 'all' or comma-separated list. | `all` |
|
||||
|
||||
#### User Valves (per-user overrides)
|
||||
|
||||
These optional settings can be set per user (overrides global Valves):
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | Reasoning effort level (low/medium/high/xhigh). | - |
|
||||
| **CLI_PATH** | Custom path to Copilot CLI. | - |
|
||||
| **DEBUG** | Enable technical debug logs. | `False` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **MODEL_ID** | Custom model ID. | - |
|
||||
|
||||
### 3. Using Custom Tools (🆕 Optional)
|
||||
|
||||
This pipe includes **1 example tool** to demonstrate tool calling:
|
||||
|
||||
* **🎲 generate_random_number**: Generate random integers
|
||||
|
||||
**To enable:**
|
||||
|
||||
1. Set `ENABLE_TOOLS: true` in Valves
|
||||
2. Try: "Give me a random number"
|
||||
|
||||
**📚 For detailed usage and creating your own tools, see [TOOLS_USAGE.md](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md)**
|
||||
|
||||
### 4. Get GH_TOKEN
|
||||
|
||||
For security, it is recommended to use a **Fine-grained Personal Access Token**:
|
||||
|
||||
1. Visit [GitHub Token Settings](https://github.com/settings/tokens?type=beta).
|
||||
2. Click **Generate new token**.
|
||||
3. **Repository access**: Select **Public repositories** (Required to access Copilot permissions).
|
||||
4. **Permissions**:
|
||||
|
||||
* Click **Account permissions**.
|
||||
* Find **Copilot Requests** (It defaults to **Read-only**, no selection needed).
|
||||
|
||||
5. Generate and copy the Token.
|
||||
|
||||
## 📋 Dependencies
|
||||
|
||||
This Pipe will automatically attempt to install the following dependencies:
|
||||
|
||||
* `github-copilot-sdk` (Python package)
|
||||
* `github-copilot-cli` (Binary file, installed via official script)
|
||||
|
||||
## ⚠️ FAQ
|
||||
|
||||
* **Stuck on "Waiting..."**:
|
||||
* Check if `GH_TOKEN` is correct and has `Copilot Requests` permission.
|
||||
* **Images not recognized**:
|
||||
* Ensure `MODEL_ID` is a model that supports multimodal input.
|
||||
* **Thinking not shown**:
|
||||
* Ensure **streaming is enabled** and the selected model supports reasoning output.
|
||||
* **CLI Installation Failed**:
|
||||
* Ensure the OpenWebUI container has internet access.
|
||||
* You can manually download the CLI and specify `CLI_PATH` in Valves.
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT
|
||||
120
docs/plugins/pipes/github-copilot-sdk.zh.md
Normal file
120
docs/plugins/pipes/github-copilot-sdk.zh.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# GitHub Copilot SDK 官方管道
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.2.3 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,允许你直接在 OpenWebUI 中使用 GitHub Copilot 模型(如 `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`)。它基于官方 [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk) 构建,提供了原生级的集成体验。
|
||||
|
||||
## 🚀 最新特性 (v0.2.3)
|
||||
|
||||
* **🧩 用户级覆盖**:新增 `REASONING_EFFORT`、`CLI_PATH`、`DEBUG`、`SHOW_THINKING`、`MODEL_ID` 的用户级覆盖。
|
||||
* **🧠 思考输出可靠性**:思考显示会遵循用户设置,并正确传递到流式输出中。
|
||||
* **📝 格式化输出增强**:自动优化输出格式(段落、列表),并解决了在某些界面下显示过于紧凑的问题。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
* **🚀 官方 SDK 集成**:基于官方 SDK,稳定可靠。
|
||||
* **🛠️ 自定义工具支持**:内置示例工具(随机数)。易于扩展自定义工具。
|
||||
* **💬 多轮对话支持**:自动拼接历史上下文,Copilot 能理解你的前文。
|
||||
* **🌊 流式输出 (Streaming)**:支持打字机效果,响应迅速。
|
||||
* **🖼️ 多模态支持**:支持上传图片,自动转换为附件发送给 Copilot(需模型支持)。
|
||||
* **🛠️ 零配置安装**:自动检测并下载 GitHub Copilot CLI,开箱即用。
|
||||
* **🔑 安全认证**:支持 Fine-grained Personal Access Tokens,权限最小化。
|
||||
* **🐛 调试模式**:内置详细的日志输出(浏览器控制台),方便排查问题。
|
||||
* **⚠️ 仅支持单节点**:由于会话状态存储在本地,本插件目前仅支持 OpenWebUI 单节点部署,或开启了会话粘性 (Sticky Session) 的多节点集群。
|
||||
|
||||
## 📦 安装与使用
|
||||
|
||||
### 1. 导入函数
|
||||
|
||||
1. 打开 OpenWebUI。
|
||||
2. 进入 **Workspace** -> **Functions**。
|
||||
3. 点击 **+** (创建函数)。
|
||||
4. 将 `github_copilot_sdk_cn.py` 的内容完整粘贴进去。
|
||||
5. 保存。
|
||||
|
||||
### 2. 配置 Valves (设置)
|
||||
|
||||
在函数列表中找到 "GitHub Copilot",点击 **⚙️ (Valves)** 图标进行配置:
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| **GH_TOKEN** | **(必填)** 你的 GitHub Token。 | - |
|
||||
| **MODEL_ID** | 使用的模型名称。推荐 `gpt-5-mini` 或 `gpt-5`。 | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Copilot CLI 的路径。如果未找到会自动下载。 | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | 是否开启调试日志(输出到浏览器控制台)。 | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI 日志级别: none, error, warning, info, debug, all。 | `error` |
|
||||
| **SHOW_THINKING** | 是否显示模型推理/思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | 在调试模式下显示会话工作空间路径和摘要。 | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | 排除包含这些关键词的模型 (逗号分隔)。 | - |
|
||||
| **WORKSPACE_DIR** | 文件操作的受限工作目录。 | - |
|
||||
| **INFINITE_SESSION** | 启用无限会话 (自动上下文压缩)。 | `True` |
|
||||
| **COMPACTION_THRESHOLD** | 后台压缩阈值 (0.0-1.0)。 | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | 缓冲耗尽阈值 (0.0-1.0)。 | `0.95` |
|
||||
| **TIMEOUT** | 流式数据块超时时间 (秒)。 | `300` |
|
||||
| **CUSTOM_ENV_VARS** | 自定义环境变量 (JSON 格式)。 | - |
|
||||
| **ENABLE_TOOLS** | 启用自定义工具 (示例:随机数)。 | `False` |
|
||||
| **AVAILABLE_TOOLS** | 可用工具: 'all' 或逗号分隔列表。 | `all` |
|
||||
| **REASONING_EFFORT** | 推理强度级别:low, medium, high。`gpt-5.2-codex`额外支持`xhigh`。 | `medium` |
|
||||
| **ENFORCE_FORMATTING** | 是否强制添加格式化指导,以提高输出可读性。 | `True` |
|
||||
|
||||
#### 用户 Valves(按用户覆盖)
|
||||
|
||||
以下设置可按用户单独配置(覆盖全局 Valves):
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | 推理强度级别(low/medium/high/xhigh)。 | - |
|
||||
| **CLI_PATH** | 自定义 Copilot CLI 路径。 | - |
|
||||
| **DEBUG** | 是否启用技术调试日志。 | `False` |
|
||||
| **SHOW_THINKING** | 是否显示思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **MODEL_ID** | 自定义模型 ID。 | - |
|
||||
|
||||
### 3. 使用自定义工具 (🆕 可选)
|
||||
|
||||
本 Pipe 内置了 **1 个示例工具**来展示工具调用功能:
|
||||
|
||||
* **🎲 generate_random_number**:生成随机整数
|
||||
|
||||
**启用方法:**
|
||||
|
||||
1. 在 Valves 中设置 `ENABLE_TOOLS: true`
|
||||
2. 尝试问:“给我一个随机数”
|
||||
|
||||
**📚 详细使用说明和创建自定义工具,请参阅 [TOOLS_USAGE.md](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md)**
|
||||
|
||||
### 4. 获取 GH_TOKEN
|
||||
|
||||
为了安全起见,推荐使用 **Fine-grained Personal Access Token**:
|
||||
|
||||
1. 访问 [GitHub Token Settings](https://github.com/settings/tokens?type=beta)。
|
||||
2. 点击 **Generate new token**。
|
||||
3. **Repository access**: 选择 **Public repositories** (必须选择此项才能看到 Copilot 权限)。
|
||||
4. **Permissions**:
|
||||
|
||||
* 点击 **Account permissions**。
|
||||
* 找到 **Copilot Requests** (默认即为 **Read-only**,无需手动修改)。
|
||||
|
||||
5. 生成并复制 Token。
|
||||
|
||||
## 📋 依赖说明
|
||||
|
||||
该 Pipe 会自动尝试安装以下依赖(如果环境中缺失):
|
||||
|
||||
* `github-copilot-sdk` (Python 包)
|
||||
* `github-copilot-cli` (二进制文件,通过官方脚本安装)
|
||||
|
||||
## ⚠️ 常见问题
|
||||
|
||||
* **一直显示 "Waiting..."**:
|
||||
* 检查 `GH_TOKEN` 是否正确且拥有 `Copilot Requests` 权限。
|
||||
* **图片无法识别**:
|
||||
* 确保 `MODEL_ID` 是支持多模态的模型。
|
||||
* **看不到思考过程**:
|
||||
* 确认已开启**流式输出**,且所选模型支持推理输出。
|
||||
* **CLI 安装失败**:
|
||||
* 确保 OpenWebUI 容器有外网访问权限。
|
||||
* 你可以手动下载 CLI 并挂载到容器中,然后在 Valves 中指定 `CLI_PATH`。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT
|
||||
@@ -15,7 +15,7 @@ Pipes allow you to:
|
||||
|
||||
## Available Pipe Plugins
|
||||
|
||||
|
||||
- [GitHub Copilot SDK](github-copilot-sdk.md) (v0.1.1) - Official GitHub Copilot SDK integration. Supports dynamic models, multi-turn conversation, streaming, multimodal input, and infinite sessions.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Pipes 可以用于:
|
||||
|
||||
## 可用的 Pipe 插件
|
||||
|
||||
|
||||
- [GitHub Copilot SDK](github-copilot-sdk.zh.md) (v0.1.1) - GitHub Copilot SDK 官方集成。支持动态模型、多轮对话、流式输出、图片输入及无限会话。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# 📊 Smart Infographic (AntV)
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.4.9 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.5.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click.
|
||||
|
||||
## 🔥 What's New in v1.4.9
|
||||
## 🔥 What's New in v1.5.0
|
||||
|
||||
- 🌐 **Smart Language Detection**: Automatically detects the accurate UI language from your browser.
|
||||
- 🗣️ **Context-Aware Generation**: Generated infographics now strictly follow the language of your input content (e.g., input Japanese -> output Japanese infographic).
|
||||
- 🐛 **Bug Fixes**: Fixed issues with language synchronization between the UI and generated content.
|
||||
|
||||
### Previous: v1.4.9
|
||||
|
||||
- 🎨 **70+ Official Templates**: Integrated comprehensive AntV infographic template library.
|
||||
- 🖼️ **Iconify & unDraw Support**: Richer visuals with official icons and illustrations.
|
||||
@@ -63,7 +69,6 @@ You can adjust the following parameters in the plugin settings to optimize the g
|
||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
|
||||
## 📝 Syntax Example (For Advanced Users)
|
||||
|
||||
You can also input this syntax directly for AI to render:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# 📊 智能信息图 (AntV Infographic)
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.4.9 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.5.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
||||
|
||||
## 🔥 v1.4.9 更新日志
|
||||
## 🔥 v1.5.0 更新日志
|
||||
|
||||
- 🌐 **智能语言检测**:自动从浏览器准确识别当前界面语言设置。
|
||||
- 🗣️ **上下文感知生成**:生成的信息图内容现在严格跟随用户输入内容的语言(例如:输入日语 -> 生成日语信息图)。
|
||||
- 🐛 **问题修复**:修复了界面语言与生成内容语言不同步的问题。
|
||||
|
||||
### 此前: v1.4.9
|
||||
|
||||
- 🎨 **70+ 官方模板**:全面集成 AntV 官方信息图模板库。
|
||||
- 🖼️ **图标与插图支持**:支持 Iconify 图标库与 unDraw 插图库,视觉效果更丰富。
|
||||
@@ -63,7 +69,6 @@
|
||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
|
||||
## 📝 语法示例 (高级用户)
|
||||
|
||||
你也可以直接输入以下语法让 AI 渲染:
|
||||
|
||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.4.9
|
||||
version: 1.5.0
|
||||
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
|
||||
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
|
||||
"""
|
||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Important Language Rule
|
||||
- **GENERATE CONTENT IN INPUT LANGUAGE**: You must generate the text content of the infographic in the **exact same language** as the user's input content (the text you are analyzing).
|
||||
- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the infographic content must be in Chinese. If input is Japanese, output Japanese.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
@@ -958,7 +962,11 @@ class Action:
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""Safely extracts user context information."""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -967,10 +975,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "User")
|
||||
user_language = user_data.get("language", "en-US")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", "en-US"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1469,18 +1499,10 @@ class Action:
|
||||
logger.info("Action: Infographic started (v1.4.0)")
|
||||
|
||||
# Get user information
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = __user__[0].get("language", "en") if __user__ else "en"
|
||||
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "en")
|
||||
user_name = __user__.get("name", "User")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
user_language = user_ctx["user_language"]
|
||||
|
||||
# Get current time
|
||||
now = datetime.now()
|
||||
|
||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||
version: 1.4.9
|
||||
version: 1.5.0
|
||||
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
||||
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
||||
"""
|
||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Important Language Rule (语言规则)
|
||||
- **Priority Input Language (优先使用输入语言)**: You must generate the text content of the infographic in the **exact same language** as the user's input content.
|
||||
- **Example**: If the user provides a summary in Chinese, the labels and descriptions in the infographic must be in Chinese.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
@@ -974,7 +978,11 @@ class Action:
|
||||
"Sunday": "星期日",
|
||||
}
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""安全提取用户上下文信息。"""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -983,10 +991,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "用户")
|
||||
user_language = user_data.get("language", "zh-CN")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'zh-CN'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "用户"),
|
||||
"user_language": user_data.get("language", "zh-CN"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1509,20 +1539,10 @@ class Action:
|
||||
logger.info("Action: 信息图启动 (v1.4.0)")
|
||||
|
||||
# 获取用户信息
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_language = (
|
||||
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
|
||||
)
|
||||
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
|
||||
user_id = (
|
||||
__user__[0]["id"]
|
||||
if __user__ and "id" in __user__[0]
|
||||
else "unknown_user"
|
||||
)
|
||||
elif isinstance(__user__, dict):
|
||||
user_language = __user__.get("language", "zh-CN")
|
||||
user_name = __user__.get("name", "用户")
|
||||
user_id = __user__.get("id", "unknown_user")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
user_language = user_ctx["user_language"]
|
||||
|
||||
# 获取当前时间
|
||||
now = datetime.now()
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes long-form text content and automatically generates interactive mind maps, helping users structure and visualize knowledge.
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.9.1 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.9.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
## What's New in v0.9.1
|
||||
## What's New in v0.9.2
|
||||
|
||||
**New Feature: Image Output Mode**
|
||||
**Language Rule Alignment**
|
||||
|
||||
- **Static Image Support**: Added `OUTPUT_MODE` configuration parameter.
|
||||
- `html` (default): Interactive HTML mind map.
|
||||
- `image`: Static SVG image embedded directly in Markdown (**No HTML code output**, cleaner chat history).
|
||||
- **Efficient Storage**: Image mode uploads SVG to `/api/v1/files`, avoiding huge base64 strings in chat history.
|
||||
- **Smart Features**: Auto-responsive width and automatic theme detection (light/dark) for generated images.
|
||||
- **Input Language First**: Mind map output now strictly matches the input text language.
|
||||
- **Consistent Behavior**: Matches the infographic language rule for predictable multilingual output.
|
||||
|
||||
## Key Features 🔑
|
||||
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
思维导图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.9.1 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.9.2 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
## v0.9.1 更新亮点
|
||||
## v0.9.2 更新亮点
|
||||
|
||||
**新功能:图片输出模式**
|
||||
**语言规则对齐**
|
||||
|
||||
- **静态图片支持**:新增 `OUTPUT_MODE` 配置参数。
|
||||
- `html`(默认):交互式 HTML 思维导图。
|
||||
- `image`:静态 SVG 图片直接嵌入 Markdown(**不输出 HTML 代码**,聊天记录更简洁)。
|
||||
- **高效存储**:图片模式将 SVG 上传至 `/api/v1/files`,避免聊天记录中出现超长 Base64 字符串。
|
||||
- **智能特性**:生成的图片支持自动响应式宽度和自动主题检测(亮色/暗色)。
|
||||
- **输入语言优先**:导图输出严格与输入文本语言一致。
|
||||
- **一致性提升**:与信息图语言规则保持一致,多语言输出更可预期。
|
||||
|
||||
## 核心特性 🔑
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
version: 0.9.1
|
||||
version: 0.9.2
|
||||
openwebui_id: 3094c59a-b4dd-4e0c-9449-15e2dd547dc4
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
||||
@@ -33,7 +33,8 @@ SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
|
||||
You are a professional mind map generation assistant, capable of efficiently analyzing long-form text provided by users and structuring its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for rendering by Markmap.js.
|
||||
|
||||
Please strictly follow these guidelines:
|
||||
- **Language**: All output must be in the language specified by the user.
|
||||
- **Language**: All output must be in the exact same language as the input text (the text you are analyzing).
|
||||
- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the mind map content must be in Chinese. If input is Japanese, output Japanese.
|
||||
- **Format**: Your output must strictly be in Markdown list format, wrapped with ```markdown and ```.
|
||||
- Use `#` to define the central theme (root node).
|
||||
- Use `-` with two-space indentation to represent branches and sub-branches.
|
||||
@@ -811,7 +812,11 @@ class Action:
|
||||
"Sunday": "Sunday",
|
||||
}
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""Extract basic user context with safe fallbacks."""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -820,10 +825,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "User")
|
||||
user_language = user_data.get("language", "en-US")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", "en-US"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1369,8 +1396,8 @@ class Action:
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Request] = None,
|
||||
) -> Optional[dict]:
|
||||
logger.info("Action: Smart Mind Map (v0.9.1) started")
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
logger.info("Action: Smart Mind Map (v0.9.2) started")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_language = user_ctx["user_language"]
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
|
||||
@@ -3,7 +3,7 @@ title: 思维导图
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.9.1
|
||||
version: 0.9.2
|
||||
openwebui_id: 8d4b097b-219b-4dd2-b509-05fbe6388335
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
@@ -32,7 +32,8 @@ SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
|
||||
你是一个专业的思维导图生成助手,能够高效地分析用户提供的长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以便Markmap.js进行渲染。
|
||||
|
||||
请严格遵循以下指导原则:
|
||||
- **语言**: 所有输出必须使用用户指定的语言。
|
||||
- **语言**: 所有输出必须与输入文本(正在分析的文本)保持完全一致的语言。
|
||||
- **格式一致性**: 即使系统提示词是中文,只要用户输入是英文,导图内容必须是英文;若输入为日文,则输出日文。
|
||||
- **格式**: 你的输出必须严格为Markdown列表格式,并用```markdown 和 ``` 包裹。
|
||||
- 使用 `#` 定义中心主题(根节点)。
|
||||
- 使用 `-` 和两个空格的缩进表示分支和子分支。
|
||||
@@ -809,7 +810,11 @@ class Action:
|
||||
"Sunday": "星期日",
|
||||
}
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
async def _get_user_context(
|
||||
self,
|
||||
__user__: Optional[Dict[str, Any]],
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""Extract basic user context with safe fallbacks."""
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
@@ -818,10 +823,32 @@ class Action:
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
user_id = user_data.get("id", "unknown_user")
|
||||
user_name = user_data.get("name", "User")
|
||||
user_language = user_data.get("language", "en-US")
|
||||
|
||||
if __event_call__:
|
||||
try:
|
||||
js_code = """
|
||||
return (
|
||||
localStorage.getItem('locale') ||
|
||||
localStorage.getItem('language') ||
|
||||
navigator.language ||
|
||||
'en-US'
|
||||
);
|
||||
"""
|
||||
frontend_lang = await __event_call__(
|
||||
{"type": "execute", "data": {"code": js_code}}
|
||||
)
|
||||
if frontend_lang and isinstance(frontend_lang, str):
|
||||
user_language = frontend_lang
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "用户"),
|
||||
"user_language": user_data.get("language", "zh-CN"),
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"user_language": user_language,
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
@@ -1348,8 +1375,8 @@ class Action:
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Request] = None,
|
||||
) -> Optional[dict]:
|
||||
logger.info("Action: 思维导图 (v0.9.1) started")
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
logger.info("Action: 思维导图 (v0.9.2) started")
|
||||
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||
user_language = user_ctx["user_language"]
|
||||
user_name = user_ctx["user_name"]
|
||||
user_id = user_ctx["user_id"]
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
# GitHub Copilot SDK 自定义工具快速入门
|
||||
|
||||
## 🎯 目标
|
||||
|
||||
在 OpenWebUI Pipe 中直接使用 GitHub Copilot SDK 的自定义工具功能,无需集成 OpenWebUI Function 系统。
|
||||
|
||||
---
|
||||
|
||||
## 📖 基础概念
|
||||
|
||||
### Copilot SDK Tool 的三要素
|
||||
|
||||
```python
|
||||
from copilot.types import Tool, ToolInvocation, ToolResult
|
||||
|
||||
# 1. Tool Definition(工具定义)
|
||||
tool = Tool(
|
||||
name="tool_name", # 工具名称
|
||||
description="What it does", # 描述(给 AI 看的)
|
||||
parameters={...}, # JSON Schema 参数定义
|
||||
handler=handler_function # 处理函数
|
||||
)
|
||||
|
||||
# 2. Tool Handler(处理函数)
|
||||
async def handler_function(invocation: ToolInvocation) -> ToolResult:
|
||||
# invocation 包含:
|
||||
# - session_id: 会话 ID
|
||||
# - tool_call_id: 调用 ID
|
||||
# - tool_name: 工具名称
|
||||
# - arguments: dict(实际参数)
|
||||
|
||||
result = do_something(invocation["arguments"])
|
||||
|
||||
return ToolResult(
|
||||
textResultForLlm="结果文本",
|
||||
resultType="success", # 或 "failure"
|
||||
error=None,
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
# 3. Session Configuration(会话配置)
|
||||
session_config = SessionConfig(
|
||||
model="claude-sonnet-4.5",
|
||||
tools=[tool1, tool2, tool3], # ✅ 传入工具列表
|
||||
streaming=True
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 完整实现示例
|
||||
|
||||
### 示例 1:获取当前时间
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
from copilot.types import Tool, ToolInvocation, ToolResult
|
||||
|
||||
def create_time_tool():
|
||||
"""创建获取时间的工具"""
|
||||
|
||||
async def get_time_handler(invocation: ToolInvocation) -> ToolResult:
|
||||
"""工具处理函数"""
|
||||
try:
|
||||
# 获取参数
|
||||
timezone = invocation["arguments"].get("timezone", "UTC")
|
||||
format_str = invocation["arguments"].get("format", "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 执行逻辑
|
||||
current_time = datetime.now().strftime(format_str)
|
||||
result_text = f"Current time: {current_time}"
|
||||
|
||||
# 返回结果
|
||||
return ToolResult(
|
||||
textResultForLlm=result_text,
|
||||
resultType="success",
|
||||
error=None,
|
||||
toolTelemetry={"execution_time": "fast"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Error getting time: {str(e)}",
|
||||
resultType="failure",
|
||||
error=str(e),
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
# 创建工具定义
|
||||
return Tool(
|
||||
name="get_current_time",
|
||||
description="Get the current date and time. Useful when user asks 'what time is it' or needs to know the current date.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Timezone name (e.g., 'UTC', 'Asia/Shanghai')",
|
||||
"default": "UTC"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Time format string",
|
||||
"default": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
}
|
||||
},
|
||||
handler=get_time_handler
|
||||
)
|
||||
```
|
||||
|
||||
### 示例 2:数学计算器
|
||||
|
||||
```python
|
||||
def create_calculator_tool():
|
||||
"""创建计算器工具"""
|
||||
|
||||
async def calculate_handler(invocation: ToolInvocation) -> ToolResult:
|
||||
try:
|
||||
expression = invocation["arguments"].get("expression", "")
|
||||
|
||||
# 安全检查
|
||||
allowed_chars = set("0123456789+-*/()., ")
|
||||
if not all(c in allowed_chars for c in expression):
|
||||
raise ValueError("Expression contains invalid characters")
|
||||
|
||||
# 计算(安全的 eval)
|
||||
result = eval(expression, {"__builtins__": {}})
|
||||
|
||||
return ToolResult(
|
||||
textResultForLlm=f"The result of {expression} is {result}",
|
||||
resultType="success",
|
||||
error=None,
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Calculation error: {str(e)}",
|
||||
resultType="failure",
|
||||
error=str(e),
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
return Tool(
|
||||
name="calculate",
|
||||
description="Perform mathematical calculations. Supports basic arithmetic operations (+, -, *, /).",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Mathematical expression to evaluate (e.g., '2 + 2 * 3')"
|
||||
}
|
||||
},
|
||||
"required": ["expression"]
|
||||
},
|
||||
handler=calculate_handler
|
||||
)
|
||||
```
|
||||
|
||||
### 示例 3:随机数生成器
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
def create_random_number_tool():
|
||||
"""创建随机数生成工具"""
|
||||
|
||||
async def random_handler(invocation: ToolInvocation) -> ToolResult:
|
||||
try:
|
||||
min_val = invocation["arguments"].get("min", 1)
|
||||
max_val = invocation["arguments"].get("max", 100)
|
||||
|
||||
if min_val >= max_val:
|
||||
raise ValueError("min must be less than max")
|
||||
|
||||
number = random.randint(min_val, max_val)
|
||||
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Generated random number: {number}",
|
||||
resultType="success",
|
||||
error=None,
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Error: {str(e)}",
|
||||
resultType="failure",
|
||||
error=str(e),
|
||||
toolTelemetry={}
|
||||
)
|
||||
|
||||
return Tool(
|
||||
name="generate_random_number",
|
||||
description="Generate a random integer within a specified range.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {
|
||||
"type": "integer",
|
||||
"description": "Minimum value (inclusive)",
|
||||
"default": 1
|
||||
},
|
||||
"max": {
|
||||
"type": "integer",
|
||||
"description": "Maximum value (inclusive)",
|
||||
"default": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
handler=random_handler
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 集成到 Pipe
|
||||
|
||||
### 完整的 Pipe 实现
|
||||
|
||||
```python
|
||||
class Pipe:
|
||||
class Valves(BaseModel):
|
||||
# ... 现有 Valves ...
|
||||
|
||||
ENABLE_TOOLS: bool = Field(
|
||||
default=False,
|
||||
description="Enable custom tools (time, calculator, random)"
|
||||
)
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="Available tools: 'all' or comma-separated list (e.g., 'get_current_time,calculate')"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# ... 现有初始化 ...
|
||||
self._custom_tools = []
|
||||
|
||||
def _initialize_custom_tools(self):
|
||||
"""初始化自定义工具"""
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
# 定义所有可用工具
|
||||
all_tools = {
|
||||
"get_current_time": create_time_tool(),
|
||||
"calculate": create_calculator_tool(),
|
||||
"generate_random_number": create_random_number_tool(),
|
||||
}
|
||||
|
||||
# 根据配置过滤工具
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
# 只启用指定的工具
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
|
||||
async def pipe(
|
||||
self,
|
||||
body: dict,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__event_emitter__=None,
|
||||
__event_call__=None,
|
||||
) -> Union[str, AsyncGenerator]:
|
||||
# ... 现有代码 ...
|
||||
|
||||
# ✅ 初始化工具
|
||||
custom_tools = self._initialize_custom_tools()
|
||||
|
||||
if custom_tools:
|
||||
await self._emit_debug_log(
|
||||
f"Enabled {len(custom_tools)} custom tools: {[t.name for t in custom_tools]}",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
# ✅ 创建会话配置(传入工具)
|
||||
from copilot.types import SessionConfig, InfiniteSessionConfig
|
||||
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id if chat_id else None,
|
||||
model=real_model_id,
|
||||
streaming=body.get("stream", False),
|
||||
tools=custom_tools, # ✅✅✅ 关键:传入工具列表
|
||||
infinite_sessions=infinite_session_config if self.valves.INFINITE_SESSION else None,
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
|
||||
# ... 其余代码保持不变 ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 处理工具调用事件
|
||||
|
||||
### 在 stream_response 中显示工具调用
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
# ... 现有代码 ...
|
||||
|
||||
def handler(event):
|
||||
event_type = str(getattr(event.type, "value", event.type))
|
||||
|
||||
# ✅ 工具调用开始
|
||||
if "tool_invocation_started" in event_type or "tool_call_started" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
if tool_name:
|
||||
queue.put_nowait(f"\n\n🔧 **Calling tool**: `{tool_name}`\n")
|
||||
|
||||
# ✅ 工具调用完成
|
||||
elif "tool_invocation_completed" in event_type or "tool_call_completed" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
result = get_event_data(event, "result", "")
|
||||
if tool_name:
|
||||
queue.put_nowait(f"\n✅ **Tool `{tool_name}` completed**\n")
|
||||
|
||||
# ✅ 工具调用失败
|
||||
elif "tool_invocation_failed" in event_type or "tool_call_failed" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
error = get_event_data(event, "error", "")
|
||||
if tool_name:
|
||||
queue.put_nowait(f"\n❌ **Tool `{tool_name}` failed**: {error}\n")
|
||||
|
||||
# ... 其他事件处理 ...
|
||||
|
||||
# ... 其余代码 ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试示例
|
||||
|
||||
### 测试 1:询问时间
|
||||
|
||||
```
|
||||
User: "What time is it now?"
|
||||
|
||||
Expected Flow:
|
||||
1. Copilot 识别需要调用 get_current_time 工具
|
||||
2. 调用工具(无参数或默认参数)
|
||||
3. 工具返回: "Current time: 2026-01-26 15:30:00"
|
||||
4. Copilot 回答: "The current time is 2026-01-26 15:30:00"
|
||||
|
||||
Pipe Output:
|
||||
---
|
||||
🔧 **Calling tool**: `get_current_time`
|
||||
✅ **Tool `get_current_time` completed**
|
||||
The current time is 2026-01-26 15:30:00
|
||||
---
|
||||
```
|
||||
|
||||
### 测试 2:数学计算
|
||||
|
||||
```
|
||||
User: "Calculate 123 * 456"
|
||||
|
||||
Expected Flow:
|
||||
1. Copilot 调用 calculate 工具
|
||||
2. 参数: {"expression": "123 * 456"}
|
||||
3. 工具返回: "The result of 123 * 456 is 56088"
|
||||
4. Copilot 回答: "123 multiplied by 456 equals 56,088"
|
||||
|
||||
Pipe Output:
|
||||
---
|
||||
🔧 **Calling tool**: `calculate`
|
||||
✅ **Tool `calculate` completed**
|
||||
123 multiplied by 456 equals 56,088
|
||||
---
|
||||
```
|
||||
|
||||
### 测试 3:生成随机数
|
||||
|
||||
```
|
||||
User: "Give me a random number between 1 and 10"
|
||||
|
||||
Expected Flow:
|
||||
1. Copilot 调用 generate_random_number 工具
|
||||
2. 参数: {"min": 1, "max": 10}
|
||||
3. 工具返回: "Generated random number: 7"
|
||||
4. Copilot 回答: "I generated a random number for you: 7"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 调试技巧
|
||||
|
||||
### 1. 记录所有工具事件
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
event_type = str(getattr(event.type, "value", event.type))
|
||||
|
||||
# 记录所有包含 "tool" 的事件
|
||||
if "tool" in event_type.lower():
|
||||
event_data = {}
|
||||
if hasattr(event, "data"):
|
||||
try:
|
||||
event_data = {
|
||||
"type": event_type,
|
||||
"data": str(event.data)[:200] # 截断长数据
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
self._emit_debug_log_sync(
|
||||
f"Tool Event: {json.dumps(event_data)}",
|
||||
__event_call__
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 验证工具注册
|
||||
|
||||
```python
|
||||
async def pipe(...):
|
||||
# ...
|
||||
custom_tools = self._initialize_custom_tools()
|
||||
|
||||
# 调试:打印工具信息
|
||||
if self.valves.DEBUG:
|
||||
tool_info = [
|
||||
{
|
||||
"name": t.name,
|
||||
"description": t.description[:50],
|
||||
"has_handler": t.handler is not None
|
||||
}
|
||||
for t in custom_tools
|
||||
]
|
||||
await self._emit_debug_log(
|
||||
f"Registered tools: {json.dumps(tool_info, indent=2)}",
|
||||
__event_call__
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 测试工具处理函数
|
||||
|
||||
```python
|
||||
# 单独测试工具
|
||||
async def test_tool():
|
||||
tool = create_time_tool()
|
||||
|
||||
# 模拟调用
|
||||
invocation = {
|
||||
"session_id": "test",
|
||||
"tool_call_id": "test_call",
|
||||
"tool_name": "get_current_time",
|
||||
"arguments": {"format": "%H:%M:%S"}
|
||||
}
|
||||
|
||||
result = await tool.handler(invocation)
|
||||
print(f"Result: {result}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 工具描述的重要性
|
||||
|
||||
工具的 `description` 字段非常重要,它告诉 AI 何时应该使用这个工具:
|
||||
|
||||
```python
|
||||
# ❌ 差的描述
|
||||
description="Get time"
|
||||
|
||||
# ✅ 好的描述
|
||||
description="Get the current date and time. Use this when the user asks 'what time is it', 'what's the date', or needs to know the current timestamp."
|
||||
```
|
||||
|
||||
### 2. 参数定义
|
||||
|
||||
使用标准的 JSON Schema 定义参数:
|
||||
|
||||
```python
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param_name": {
|
||||
"type": "string", # string, integer, boolean, array, object
|
||||
"description": "Clear description",
|
||||
"enum": ["option1", "option2"], # 可选:枚举值
|
||||
"default": "default_value" # 可选:默认值
|
||||
}
|
||||
},
|
||||
"required": ["param_name"] # 必需参数
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
总是捕获异常并返回有意义的错误:
|
||||
|
||||
```python
|
||||
try:
|
||||
result = do_something()
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Success: {result}",
|
||||
resultType="success",
|
||||
error=None,
|
||||
toolTelemetry={}
|
||||
)
|
||||
except Exception as e:
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Error occurred: {str(e)}",
|
||||
resultType="failure",
|
||||
error=str(e), # 用于调试
|
||||
toolTelemetry={}
|
||||
)
|
||||
```
|
||||
|
||||
### 4. 异步 vs 同步
|
||||
|
||||
工具处理函数可以是同步或异步:
|
||||
|
||||
```python
|
||||
# 同步工具
|
||||
def sync_handler(invocation):
|
||||
result = calculate(invocation["arguments"])
|
||||
return ToolResult(...)
|
||||
|
||||
# 异步工具(推荐)
|
||||
async def async_handler(invocation):
|
||||
result = await fetch_data(invocation["arguments"])
|
||||
return ToolResult(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始清单
|
||||
|
||||
- [ ] 1. 在 Valves 中添加 `ENABLE_TOOLS` 配置
|
||||
- [ ] 2. 定义 2-3 个简单的工具函数
|
||||
- [ ] 3. 实现 `_initialize_custom_tools()` 方法
|
||||
- [ ] 4. 修改 `SessionConfig` 传入 `tools` 参数
|
||||
- [ ] 5. 在 `stream_response` 中添加工具事件处理
|
||||
- [ ] 6. 测试:询问时间、计算数学、生成随机数
|
||||
- [ ] 7. 添加调试日志
|
||||
- [ ] 8. 同步中文版本
|
||||
|
||||
---
|
||||
|
||||
## 📚 完整的工具事件列表
|
||||
|
||||
根据 SDK 源码,可能的工具相关事件:
|
||||
|
||||
- `tool_invocation_started` / `tool_call_started`
|
||||
- `tool_invocation_completed` / `tool_call_completed`
|
||||
- `tool_invocation_failed` / `tool_call_failed`
|
||||
- `tool_parameter_validation_failed`
|
||||
|
||||
实际事件名称可能因 SDK 版本而异,建议先记录所有事件类型:
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
print(f"Event type: {event.type}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**快速实现入口:** 从示例 1(获取时间)开始,这是最简单的工具,可以快速验证整个流程!
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**日期:** 2026-01-26
|
||||
@@ -0,0 +1,480 @@
|
||||
# OpenWebUI Native Tool Call Display Implementation Guide
|
||||
|
||||
**Date:** 2026-01-27
|
||||
**Purpose:** Analyze and implement OpenWebUI's native tool call display mechanism
|
||||
|
||||
---
|
||||
|
||||
## 📸 Current vs Native Display
|
||||
|
||||
### Current Implementation
|
||||
|
||||
```markdown
|
||||
> 🔧 **Running Tool**: `search_chats`
|
||||
|
||||
> ✅ **Tool Completed**: {...}
|
||||
```
|
||||
|
||||
### OpenWebUI Native Display (from screenshot)
|
||||
|
||||
- ✅ Collapsible panel: "查看来自 search_chats 的结果"
|
||||
- ✅ Formatted JSON display
|
||||
- ✅ Syntax highlighting
|
||||
- ✅ Expand/collapse functionality
|
||||
- ✅ Clean visual separation
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Understanding OpenWebUI's Tool Call Format
|
||||
|
||||
### Standard OpenAI Tool Call Message Format
|
||||
|
||||
OpenWebUI follows the OpenAI Chat Completion API format for tool calls:
|
||||
|
||||
#### 1. Assistant Message with Tool Calls
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None, # or explanatory text
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_chats",
|
||||
"arguments": '{"query": ""}'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Tool Response Message
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"name": "search_chats", # Optional but recommended
|
||||
"content": '{"count": 5, "results": [...]}' # JSON string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Implementation Strategy for Native Display
|
||||
|
||||
### Option 1: Event Emitter Approach (Recommended)
|
||||
|
||||
Use OpenWebUI's event emitter to send structured tool call data:
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# When tool execution starts
|
||||
if event_type == "tool.execution_start":
|
||||
await self._emit_tool_call_start(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
# When tool execution completes
|
||||
elif event_type == "tool.execution_complete":
|
||||
await self._emit_tool_call_result(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
result=result_content
|
||||
)
|
||||
```
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
```python
|
||||
async def _emit_tool_call_start(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
arguments: dict
|
||||
):
|
||||
"""Emit a tool call start event to OpenWebUI."""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# OpenWebUI expects tool_calls in assistant message format
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to emit tool call start: {e}")
|
||||
|
||||
async def _emit_tool_call_result(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
result: Any
|
||||
):
|
||||
"""Emit a tool call result to OpenWebUI."""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# Format result as JSON string
|
||||
if isinstance(result, str):
|
||||
result_content = result
|
||||
else:
|
||||
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
# OpenWebUI expects tool results in tool message format
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": result_content
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to emit tool result: {e}")
|
||||
```
|
||||
|
||||
### Option 2: Message History Injection
|
||||
|
||||
Modify the conversation history to include tool calls:
|
||||
|
||||
```python
|
||||
# After tool execution, append to messages
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments)
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": json.dumps(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Challenges with Current Architecture
|
||||
|
||||
### 1. Streaming Context
|
||||
|
||||
Our current implementation uses:
|
||||
|
||||
- **Queue-based streaming**: Events → Queue → Yield chunks
|
||||
- **Text chunks only**: We yield plain text, not structured messages
|
||||
|
||||
OpenWebUI's native display requires:
|
||||
|
||||
- **Structured message events**: Not text chunks
|
||||
- **Message-level control**: Need to emit complete messages
|
||||
|
||||
### 2. Event Emitter Compatibility
|
||||
|
||||
**Current usage:**
|
||||
|
||||
```python
|
||||
# We use event_emitter for status/notifications
|
||||
await event_emitter({
|
||||
"type": "status",
|
||||
"data": {"description": "Processing..."}
|
||||
})
|
||||
```
|
||||
|
||||
**Need for tool calls:**
|
||||
|
||||
```python
|
||||
# Need to emit message-type events
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"content": "..."
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Question:** Does `__event_emitter__` support `message` type events?
|
||||
|
||||
### 3. Session SDK Events vs OpenWebUI Messages
|
||||
|
||||
**Copilot SDK events:**
|
||||
|
||||
- `tool.execution_start` → We get tool name, arguments
|
||||
- `tool.execution_complete` → We get tool result
|
||||
- Designed for streaming text output
|
||||
|
||||
**OpenWebUI messages:**
|
||||
|
||||
- Expect structured message objects
|
||||
- Not designed for mid-stream injection
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Experimental Implementation
|
||||
|
||||
### Step 1: Add Valve for Native Display
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="Use OpenWebUI's native tool call display instead of markdown formatting"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 2: Modify Tool Event Handling
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# ...existing code...
|
||||
|
||||
def handler(event):
|
||||
event_type = get_event_type(event)
|
||||
|
||||
if event_type == "tool.execution_start":
|
||||
tool_name = safe_get_data_attr(event, "name")
|
||||
|
||||
# Get tool arguments
|
||||
tool_input = safe_get_data_attr(event, "input") or {}
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||
|
||||
if tool_call_id:
|
||||
active_tools[tool_call_id] = {
|
||||
"name": tool_name,
|
||||
"arguments": tool_input
|
||||
}
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# Emit structured tool call
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_start(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
tool_input
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Current markdown display
|
||||
queue.put_nowait(f"\n\n> 🔧 **Running Tool**: `{tool_name}`\n\n")
|
||||
|
||||
elif event_type == "tool.execution_complete":
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||
tool_info = active_tools.get(tool_call_id, {})
|
||||
tool_name = tool_info.get("name", "Unknown")
|
||||
|
||||
# Extract result
|
||||
result_obj = safe_get_data_attr(event, "result")
|
||||
result_content = ""
|
||||
if hasattr(result_obj, "content"):
|
||||
result_content = result_obj.content
|
||||
elif isinstance(result_obj, dict):
|
||||
result_content = result_obj.get("content", "")
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# Emit structured tool result
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_result(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
result_content
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Current markdown display
|
||||
queue.put_nowait(f"> ✅ **Tool Completed**: {result_content}\n\n")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Testing Plan
|
||||
|
||||
### Test 1: Event Emitter Message Type Support
|
||||
|
||||
```python
|
||||
# In a test conversation, try:
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "Test message"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Expected:** Message appears in chat
|
||||
**If fails:** Event emitter doesn't support message type
|
||||
|
||||
### Test 2: Tool Call Message Format
|
||||
|
||||
```python
|
||||
# Send a tool call message
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": "test_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"arguments": '{"param": "value"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
# Send tool result
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": "test_123",
|
||||
"name": "test_tool",
|
||||
"content": '{"result": "success"}'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Expected:** OpenWebUI displays collapsible tool panel
|
||||
**If fails:** Event format doesn't match OpenWebUI expectations
|
||||
|
||||
### Test 3: Mid-Stream Tool Call Injection
|
||||
|
||||
Test if tool call messages can be injected during streaming:
|
||||
|
||||
```python
|
||||
# Start streaming text
|
||||
yield "Processing your request..."
|
||||
|
||||
# Mid-stream: emit tool call
|
||||
await __event_emitter__({"type": "message", "data": {...}})
|
||||
|
||||
# Continue streaming
|
||||
yield "Done!"
|
||||
```
|
||||
|
||||
**Expected:** Tool panel appears mid-response
|
||||
**Risk:** May break streaming flow
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Checklist
|
||||
|
||||
- [x] Add `REASONING_EFFORT` valve (completed)
|
||||
- [ ] Add `USE_NATIVE_TOOL_DISPLAY` valve
|
||||
- [ ] Implement `_emit_tool_call_start()` helper
|
||||
- [ ] Implement `_emit_tool_call_result()` helper
|
||||
- [ ] Modify tool event handling in `stream_response()`
|
||||
- [ ] Test event emitter message type support
|
||||
- [ ] Test tool call message format
|
||||
- [ ] Test mid-stream injection
|
||||
- [ ] Update documentation
|
||||
- [ ] Add user configuration guide
|
||||
|
||||
---
|
||||
|
||||
## 🤔 Recommendation
|
||||
|
||||
### Hybrid Approach (Safest)
|
||||
|
||||
Keep both display modes:
|
||||
|
||||
1. **Default (Current):** Markdown-based display
|
||||
- ✅ Works reliably with streaming
|
||||
- ✅ No OpenWebUI API dependencies
|
||||
- ✅ Consistent across versions
|
||||
|
||||
2. **Experimental (Native):** Structured tool messages
|
||||
- ✅ Better visual integration
|
||||
- ⚠️ Requires testing with OpenWebUI internals
|
||||
- ⚠️ May not work in all scenarios
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```python
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="[EXPERIMENTAL] Use OpenWebUI's native tool call display"
|
||||
)
|
||||
```
|
||||
|
||||
### Why Markdown Display is Currently Better
|
||||
|
||||
1. **Reliability:** Always works with streaming
|
||||
2. **Flexibility:** Can customize format easily
|
||||
3. **Context:** Shows tools inline with reasoning
|
||||
4. **Compatibility:** Works across OpenWebUI versions
|
||||
|
||||
### When to Use Native Display
|
||||
|
||||
- Non-streaming mode (easier to inject messages)
|
||||
- After confirming event emitter supports message type
|
||||
- For tools with large JSON results (better formatting)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Research OpenWebUI Source Code**
|
||||
- Check `__event_emitter__` implementation
|
||||
- Verify supported event types
|
||||
- Test message injection patterns
|
||||
|
||||
2. **Create Proof of Concept**
|
||||
- Simple test plugin
|
||||
- Emit tool call messages
|
||||
- Verify UI rendering
|
||||
|
||||
3. **Document Findings**
|
||||
- Update this guide with test results
|
||||
- Add code examples that work
|
||||
- Create migration guide if successful
|
||||
|
||||
---
|
||||
|
||||
## 🔗 References
|
||||
|
||||
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||
- [OpenWebUI Plugin Development](https://docs.openwebui.com/)
|
||||
- [Copilot SDK Events](https://github.com/github/copilot-sdk)
|
||||
|
||||
---
|
||||
|
||||
**Author:** Fu-Jie
|
||||
**Status:** Analysis Complete - Implementation Pending Testing
|
||||
@@ -0,0 +1,480 @@
|
||||
# OpenWebUI 原生工具调用展示实现指南
|
||||
|
||||
**日期:** 2026-01-27
|
||||
**目的:** 分析并实现 OpenWebUI 的原生工具调用展示机制
|
||||
|
||||
---
|
||||
|
||||
## 📸 当前展示 vs 原生展示
|
||||
|
||||
### 当前实现
|
||||
|
||||
```markdown
|
||||
> 🔧 **Running Tool**: `search_chats`
|
||||
|
||||
> ✅ **Tool Completed**: {...}
|
||||
```
|
||||
|
||||
### OpenWebUI 原生展示(来自截图)
|
||||
|
||||
- ✅ 可折叠面板:"查看来自 search_chats 的结果"
|
||||
- ✅ 格式化的 JSON 显示
|
||||
- ✅ 语法高亮
|
||||
- ✅ 展开/折叠功能
|
||||
- ✅ 清晰的视觉分隔
|
||||
|
||||
---
|
||||
|
||||
## 🔍 理解 OpenWebUI 的工具调用格式
|
||||
|
||||
### 标准 OpenAI 工具调用消息格式
|
||||
|
||||
OpenWebUI 遵循 OpenAI Chat Completion API 的工具调用格式:
|
||||
|
||||
#### 1. 带工具调用的助手消息
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None, # 或解释性文本
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_chats",
|
||||
"arguments": '{"query": ""}'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 工具响应消息
|
||||
|
||||
```python
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"name": "search_chats", # 可选但推荐
|
||||
"content": '{"count": 5, "results": [...]}' # JSON 字符串
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 原生展示的实现策略
|
||||
|
||||
### 方案 1:事件发射器方法(推荐)
|
||||
|
||||
使用 OpenWebUI 的事件发射器发送结构化工具调用数据:
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# 工具执行开始时
|
||||
if event_type == "tool.execution_start":
|
||||
await self._emit_tool_call_start(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments
|
||||
)
|
||||
|
||||
# 工具执行完成时
|
||||
elif event_type == "tool.execution_complete":
|
||||
await self._emit_tool_call_result(
|
||||
emitter=__event_call__,
|
||||
tool_call_id=tool_call_id,
|
||||
tool_name=tool_name,
|
||||
result=result_content
|
||||
)
|
||||
```
|
||||
|
||||
#### 辅助方法
|
||||
|
||||
```python
|
||||
async def _emit_tool_call_start(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
arguments: dict
|
||||
):
|
||||
"""向 OpenWebUI 发射工具调用开始事件。"""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# OpenWebUI 期望 assistant 消息格式的 tool_calls
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"发射工具调用开始事件失败: {e}")
|
||||
|
||||
async def _emit_tool_call_result(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
result: Any
|
||||
):
|
||||
"""向 OpenWebUI 发射工具调用结果。"""
|
||||
if not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
# 将结果格式化为 JSON 字符串
|
||||
if isinstance(result, str):
|
||||
result_content = result
|
||||
else:
|
||||
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
# OpenWebUI 期望 tool 消息格式的工具结果
|
||||
await emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": result_content
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"发射工具结果失败: {e}")
|
||||
```
|
||||
|
||||
### 方案 2:消息历史注入
|
||||
|
||||
修改对话历史以包含工具调用:
|
||||
|
||||
```python
|
||||
# 工具执行后,追加到消息中
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": json.dumps(arguments)
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"name": tool_name,
|
||||
"content": json.dumps(result)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 当前架构的挑战
|
||||
|
||||
### 1. 流式上下文
|
||||
|
||||
我们当前的实现使用:
|
||||
|
||||
- **基于队列的流式传输**:事件 → 队列 → 产出块
|
||||
- **仅文本块**:我们产出纯文本,而非结构化消息
|
||||
|
||||
OpenWebUI 的原生展示需要:
|
||||
|
||||
- **结构化消息事件**:不是文本块
|
||||
- **消息级别控制**:需要发射完整消息
|
||||
|
||||
### 2. 事件发射器兼容性
|
||||
|
||||
**当前用法:**
|
||||
|
||||
```python
|
||||
# 我们使用 event_emitter 发送状态/通知
|
||||
await event_emitter({
|
||||
"type": "status",
|
||||
"data": {"description": "处理中..."}
|
||||
})
|
||||
```
|
||||
|
||||
**工具调用所需:**
|
||||
|
||||
```python
|
||||
# 需要发射 message 类型事件
|
||||
await event_emitter({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"content": "..."
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**问题:** `__event_emitter__` 是否支持 `message` 类型事件?
|
||||
|
||||
### 3. Session SDK 事件 vs OpenWebUI 消息
|
||||
|
||||
**Copilot SDK 事件:**
|
||||
|
||||
- `tool.execution_start` → 获取工具名称、参数
|
||||
- `tool.execution_complete` → 获取工具结果
|
||||
- 为流式文本输出设计
|
||||
|
||||
**OpenWebUI 消息:**
|
||||
|
||||
- 期望结构化消息对象
|
||||
- 不为中间流注入设计
|
||||
|
||||
---
|
||||
|
||||
## 🧪 实验性实现
|
||||
|
||||
### 步骤 1:添加原生展示 Valve
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="使用 OpenWebUI 的原生工具调用展示,而非 Markdown 格式"
|
||||
)
|
||||
```
|
||||
|
||||
### 步骤 2:修改工具事件处理
|
||||
|
||||
```python
|
||||
async def stream_response(self, ...):
|
||||
# ...现有代码...
|
||||
|
||||
def handler(event):
|
||||
event_type = get_event_type(event)
|
||||
|
||||
if event_type == "tool.execution_start":
|
||||
tool_name = safe_get_data_attr(event, "name")
|
||||
|
||||
# 获取工具参数
|
||||
tool_input = safe_get_data_attr(event, "input") or {}
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||
|
||||
if tool_call_id:
|
||||
active_tools[tool_call_id] = {
|
||||
"name": tool_name,
|
||||
"arguments": tool_input
|
||||
}
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# 发射结构化工具调用
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_start(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
tool_input
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 当前 Markdown 展示
|
||||
queue.put_nowait(f"\n\n> 🔧 **运行工具**: `{tool_name}`\n\n")
|
||||
|
||||
elif event_type == "tool.execution_complete":
|
||||
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||
tool_info = active_tools.get(tool_call_id, {})
|
||||
tool_name = tool_info.get("name", "未知")
|
||||
|
||||
# 提取结果
|
||||
result_obj = safe_get_data_attr(event, "result")
|
||||
result_content = ""
|
||||
if hasattr(result_obj, "content"):
|
||||
result_content = result_obj.content
|
||||
elif isinstance(result_obj, dict):
|
||||
result_content = result_obj.get("content", "")
|
||||
|
||||
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||
# 发射结构化工具结果
|
||||
asyncio.create_task(
|
||||
self._emit_tool_call_result(
|
||||
__event_call__,
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
result_content
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 当前 Markdown 展示
|
||||
queue.put_nowait(f"> ✅ **工具完成**: {result_content}\n\n")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔬 测试计划
|
||||
|
||||
### 测试 1:事件发射器消息类型支持
|
||||
|
||||
```python
|
||||
# 在测试对话中尝试:
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": "测试消息"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**预期:** 消息出现在聊天中
|
||||
**如果失败:** 事件发射器不支持 message 类型
|
||||
|
||||
### 测试 2:工具调用消息格式
|
||||
|
||||
```python
|
||||
# 发送工具调用消息
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{
|
||||
"id": "test_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"arguments": '{"param": "value"}'
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
# 发送工具结果
|
||||
await __event_emitter__({
|
||||
"type": "message",
|
||||
"data": {
|
||||
"role": "tool",
|
||||
"tool_call_id": "test_123",
|
||||
"name": "test_tool",
|
||||
"content": '{"result": "success"}'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**预期:** OpenWebUI 显示可折叠工具面板
|
||||
**如果失败:** 事件格式与 OpenWebUI 期望不符
|
||||
|
||||
### 测试 3:中间流工具调用注入
|
||||
|
||||
测试是否可以在流式传输期间注入工具调用消息:
|
||||
|
||||
```python
|
||||
# 开始流式文本
|
||||
yield "正在处理您的请求..."
|
||||
|
||||
# 中间流:发射工具调用
|
||||
await __event_emitter__({"type": "message", "data": {...}})
|
||||
|
||||
# 继续流式传输
|
||||
yield "完成!"
|
||||
```
|
||||
|
||||
**预期:** 工具面板出现在响应中间
|
||||
**风险:** 可能破坏流式传输流程
|
||||
|
||||
---
|
||||
|
||||
## 📋 实施检查清单
|
||||
|
||||
- [x] 添加 `REASONING_EFFORT` valve(已完成)
|
||||
- [ ] 添加 `USE_NATIVE_TOOL_DISPLAY` valve
|
||||
- [ ] 实现 `_emit_tool_call_start()` 辅助方法
|
||||
- [ ] 实现 `_emit_tool_call_result()` 辅助方法
|
||||
- [ ] 修改 `stream_response()` 中的工具事件处理
|
||||
- [ ] 测试事件发射器消息类型支持
|
||||
- [ ] 测试工具调用消息格式
|
||||
- [ ] 测试中间流注入
|
||||
- [ ] 更新文档
|
||||
- [ ] 添加用户配置指南
|
||||
|
||||
---
|
||||
|
||||
## 🤔 建议
|
||||
|
||||
### 混合方法(最安全)
|
||||
|
||||
保留两种展示模式:
|
||||
|
||||
1. **默认(当前):** 基于 Markdown 的展示
|
||||
- ✅ 与流式传输可靠工作
|
||||
- ✅ 无 OpenWebUI API 依赖
|
||||
- ✅ 跨版本一致
|
||||
|
||||
2. **实验性(原生):** 结构化工具消息
|
||||
- ✅ 更好的视觉集成
|
||||
- ⚠️ 需要测试 OpenWebUI 内部
|
||||
- ⚠️ 可能不适用于所有场景
|
||||
|
||||
**配置:**
|
||||
|
||||
```python
|
||||
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||
default=False,
|
||||
description="[实验性] 使用 OpenWebUI 的原生工具调用展示"
|
||||
)
|
||||
```
|
||||
|
||||
### 为什么 Markdown 展示目前更好
|
||||
|
||||
1. **可靠性:** 始终与流式传输兼容
|
||||
2. **灵活性:** 可以轻松自定义格式
|
||||
3. **上下文:** 与推理内联显示工具
|
||||
4. **兼容性:** 跨 OpenWebUI 版本工作
|
||||
|
||||
### 何时使用原生展示
|
||||
|
||||
- 非流式模式(更容易注入消息)
|
||||
- 确认事件发射器支持 message 类型后
|
||||
- 对于具有大型 JSON 结果的工具(更好的格式化)
|
||||
|
||||
---
|
||||
|
||||
## 📚 后续步骤
|
||||
|
||||
1. **研究 OpenWebUI 源代码**
|
||||
- 检查 `__event_emitter__` 实现
|
||||
- 验证支持的事件类型
|
||||
- 测试消息注入模式
|
||||
|
||||
2. **创建概念验证**
|
||||
- 简单测试插件
|
||||
- 发射工具调用消息
|
||||
- 验证 UI 渲染
|
||||
|
||||
3. **记录发现**
|
||||
- 使用测试结果更新本指南
|
||||
- 添加有效的代码示例
|
||||
- 如果成功,创建迁移指南
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||
- [OpenWebUI 插件开发](https://docs.openwebui.com/)
|
||||
- [Copilot SDK 事件](https://github.com/github/copilot-sdk)
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**状态:** 分析完成 - 实施等待测试
|
||||
@@ -0,0 +1,182 @@
|
||||
# Native Tool Display Usage Guide
|
||||
|
||||
## 🎨 What is Native Tool Display?
|
||||
|
||||
Native Tool Display is an experimental feature that integrates with OpenWebUI's built-in tool call visualization system. When enabled, tool calls and their results are displayed in **collapsible JSON panels** instead of plain markdown text.
|
||||
|
||||
### Visual Comparison
|
||||
|
||||
**Traditional Display (markdown):**
|
||||
|
||||
```
|
||||
> 🔧 Running Tool: `get_current_time`
|
||||
> ✅ Tool Completed: 2026-01-27 10:30:00
|
||||
```
|
||||
|
||||
**Native Display (collapsible panels):**
|
||||
|
||||
- Tool call appears in a collapsible `assistant.tool_calls` panel
|
||||
- Tool result appears in a separate collapsible `tool.content` panel
|
||||
- JSON syntax highlighting for better readability
|
||||
- Compact by default, expandable on click
|
||||
|
||||
## 🚀 How to Enable
|
||||
|
||||
1. Open the GitHub Copilot SDK Pipe configuration (Valves)
|
||||
2. Set `USE_NATIVE_TOOL_DISPLAY` to `true`
|
||||
3. Save the configuration
|
||||
4. Start a new conversation with tool calls
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- OpenWebUI with native tool display support
|
||||
- `__event_emitter__` must support `message` type events
|
||||
- Tool-enabled models (e.g., GPT-4, Claude Sonnet)
|
||||
|
||||
## ⚙️ How It Works
|
||||
|
||||
### OpenAI Standard Format
|
||||
|
||||
The native display uses the OpenAI standard message format:
|
||||
|
||||
**Tool Call (Assistant Message):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"arguments": "{\"timezone\":\"UTC\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Tool Result (Tool Message):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": "2026-01-27 10:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
### Message Flow
|
||||
|
||||
1. **Tool Execution Start**:
|
||||
- SDK emits `tool.execution_start` event
|
||||
- Plugin sends `assistant` message with `tool_calls` array
|
||||
- OpenWebUI displays collapsible tool call panel
|
||||
|
||||
2. **Tool Execution Complete**:
|
||||
- SDK emits `tool.execution_complete` event
|
||||
- Plugin sends `tool` message with `tool_call_id` and `content`
|
||||
- OpenWebUI displays collapsible result panel
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Panel Not Showing
|
||||
|
||||
**Symptoms:** Tool calls still appear as markdown text
|
||||
|
||||
**Possible Causes:**
|
||||
|
||||
1. `__event_emitter__` doesn't support `message` type events
|
||||
2. OpenWebUI version too old
|
||||
3. Feature not enabled (`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Enable DEBUG mode to see error messages in browser console
|
||||
- Check browser console for "Native message emission failed" warnings
|
||||
- Update OpenWebUI to latest version
|
||||
- Keep `USE_NATIVE_TOOL_DISPLAY = false` to use traditional markdown display
|
||||
|
||||
### Duplicate Tool Information
|
||||
|
||||
**Symptoms:** Tool calls appear in both native panels and markdown
|
||||
|
||||
**Cause:** Mixed display modes
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Ensure `USE_NATIVE_TOOL_DISPLAY` is either `true` (native only) or `false` (markdown only)
|
||||
- Restart the conversation after changing this setting
|
||||
|
||||
## 🧪 Experimental Status
|
||||
|
||||
This feature is marked as **EXPERIMENTAL** because:
|
||||
|
||||
1. **Event Emitter API**: The `__event_emitter__` support for `message` type events is not fully documented
|
||||
2. **OpenWebUI Version Dependency**: Requires recent versions of OpenWebUI with native tool display support
|
||||
3. **Streaming Architecture**: May have compatibility issues with streaming responses
|
||||
|
||||
### Fallback Behavior
|
||||
|
||||
If native message emission fails:
|
||||
|
||||
- Plugin automatically falls back to markdown display
|
||||
- Error logged to browser console (when DEBUG is enabled)
|
||||
- No interruption to conversation flow
|
||||
|
||||
## 📊 Performance Considerations
|
||||
|
||||
Native display has slightly better performance characteristics:
|
||||
|
||||
| Aspect | Native Display | Markdown Display |
|
||||
|--------|----------------|------------------|
|
||||
| **Rendering** | Native UI components | Markdown parser |
|
||||
| **Interactivity** | Collapsible panels | Static text |
|
||||
| **JSON Parsing** | Handled by UI | Not formatted |
|
||||
| **Token Usage** | Minimal overhead | Formatting tokens |
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
Planned improvements for native tool display:
|
||||
|
||||
- [ ] Automatic fallback detection
|
||||
- [ ] Tool call history persistence
|
||||
- [ ] Rich metadata display (execution time, arguments preview)
|
||||
- [ ] Copy tool call JSON button
|
||||
- [ ] Tool call replay functionality
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Enable DEBUG First**: Test with DEBUG mode before using in production
|
||||
2. **Monitor Browser Console**: Check for warning messages during tool calls
|
||||
3. **Test with Simple Tools**: Verify with built-in tools before custom implementations
|
||||
4. **Keep Fallback Option**: Don't rely solely on native display until it exits experimental status
|
||||
|
||||
## 📖 Related Documentation
|
||||
|
||||
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - How to create and use custom tools
|
||||
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - Technical implementation details
|
||||
- [WORKFLOW.md](WORKFLOW.md) - Complete integration workflow
|
||||
|
||||
## 🐛 Reporting Issues
|
||||
|
||||
If you encounter issues with native tool display:
|
||||
|
||||
1. Enable `DEBUG` and `USE_NATIVE_TOOL_DISPLAY`
|
||||
2. Open browser console (F12)
|
||||
3. Trigger a tool call
|
||||
4. Copy any error messages
|
||||
5. Report to [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
Include:
|
||||
|
||||
- OpenWebUI version
|
||||
- Browser and version
|
||||
- Error messages from console
|
||||
- Steps to reproduce
|
||||
|
||||
---
|
||||
|
||||
**Author:** Fu-Jie | **Version:** 0.2.0 | **License:** MIT
|
||||
@@ -0,0 +1,182 @@
|
||||
# 原生工具显示使用指南
|
||||
|
||||
## 🎨 什么是原生工具显示?
|
||||
|
||||
原生工具显示是一项实验性功能,与 OpenWebUI 的内置工具调用可视化系统集成。启用后,工具调用及其结果将以**可折叠的 JSON 面板**显示,而不是纯文本 markdown。
|
||||
|
||||
### 视觉对比
|
||||
|
||||
**传统显示 (markdown):**
|
||||
|
||||
```
|
||||
> 🔧 正在运行工具: `get_current_time`
|
||||
> ✅ 工具已完成: 2026-01-27 10:30:00
|
||||
```
|
||||
|
||||
**原生显示 (可折叠面板):**
|
||||
|
||||
- 工具调用显示在可折叠的 `assistant.tool_calls` 面板中
|
||||
- 工具结果显示在单独的可折叠 `tool.content` 面板中
|
||||
- JSON 语法高亮,提高可读性
|
||||
- 默认折叠,点击即可展开
|
||||
|
||||
## 🚀 如何启用
|
||||
|
||||
1. 打开 GitHub Copilot SDK Pipe 配置 (Valves)
|
||||
2. 将 `USE_NATIVE_TOOL_DISPLAY` 设置为 `true`
|
||||
3. 保存配置
|
||||
4. 开始新的对话并使用工具调用
|
||||
|
||||
## 📋 要求
|
||||
|
||||
- 支持原生工具显示的 OpenWebUI
|
||||
- `__event_emitter__` 必须支持 `message` 类型事件
|
||||
- 支持工具的模型(例如 GPT-4、Claude Sonnet)
|
||||
|
||||
## ⚙️ 工作原理
|
||||
|
||||
### OpenAI 标准格式
|
||||
|
||||
原生显示使用 OpenAI 标准消息格式:
|
||||
|
||||
**工具调用(助手消息):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_time",
|
||||
"arguments": "{\"timezone\":\"UTC\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**工具结果(工具消息):**
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": "2026-01-27 10:30:00 UTC"
|
||||
}
|
||||
```
|
||||
|
||||
### 消息流程
|
||||
|
||||
1. **工具执行开始**:
|
||||
- SDK 发出 `tool.execution_start` 事件
|
||||
- 插件发送带有 `tool_calls` 数组的 `assistant` 消息
|
||||
- OpenWebUI 显示可折叠的工具调用面板
|
||||
|
||||
2. **工具执行完成**:
|
||||
- SDK 发出 `tool.execution_complete` 事件
|
||||
- 插件发送带有 `tool_call_id` 和 `content` 的 `tool` 消息
|
||||
- OpenWebUI 显示可折叠的结果面板
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 面板未显示
|
||||
|
||||
**症状:** 工具调用仍以 markdown 文本形式显示
|
||||
|
||||
**可能原因:**
|
||||
|
||||
1. `__event_emitter__` 不支持 `message` 类型事件
|
||||
2. OpenWebUI 版本过旧
|
||||
3. 功能未启用(`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 启用 DEBUG 模式查看浏览器控制台中的错误消息
|
||||
- 检查浏览器控制台的 "Native message emission failed" 警告
|
||||
- 更新 OpenWebUI 到最新版本
|
||||
- 保持 `USE_NATIVE_TOOL_DISPLAY = false` 使用传统 markdown 显示
|
||||
|
||||
### 重复的工具信息
|
||||
|
||||
**症状:** 工具调用同时出现在原生面板和 markdown 中
|
||||
|
||||
**原因:** 混合显示模式
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 确保 `USE_NATIVE_TOOL_DISPLAY` 为 `true`(仅原生)或 `false`(仅 markdown)
|
||||
- 更改设置后重启对话
|
||||
|
||||
## 🧪 实验性状态
|
||||
|
||||
此功能标记为**实验性**,因为:
|
||||
|
||||
1. **事件发射器 API**:`__event_emitter__` 对 `message` 类型事件的支持未完全文档化
|
||||
2. **OpenWebUI 版本依赖**:需要支持原生工具显示的较新 OpenWebUI 版本
|
||||
3. **流式架构**:可能与流式响应存在兼容性问题
|
||||
|
||||
### 回退行为
|
||||
|
||||
如果原生消息发送失败:
|
||||
|
||||
- 插件自动回退到 markdown 显示
|
||||
- 错误记录到浏览器控制台(启用 DEBUG 时)
|
||||
- 不会中断对话流程
|
||||
|
||||
## 📊 性能考虑
|
||||
|
||||
原生显示具有略好的性能特征:
|
||||
|
||||
| 方面 | 原生显示 | Markdown 显示 |
|
||||
|------|----------|---------------|
|
||||
| **渲染** | 原生 UI 组件 | Markdown 解析器 |
|
||||
| **交互性** | 可折叠面板 | 静态文本 |
|
||||
| **JSON 解析** | 由 UI 处理 | 未格式化 |
|
||||
| **Token 使用** | 最小开销 | 格式化 token |
|
||||
|
||||
## 🔮 未来增强
|
||||
|
||||
原生工具显示的计划改进:
|
||||
|
||||
- [ ] 自动回退检测
|
||||
- [ ] 工具调用历史持久化
|
||||
- [ ] 丰富的元数据显示(执行时间、参数预览)
|
||||
- [ ] 复制工具调用 JSON 按钮
|
||||
- [ ] 工具调用重放功能
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **先启用 DEBUG**:在生产环境使用前先在 DEBUG 模式下测试
|
||||
2. **监控浏览器控制台**:在工具调用期间检查警告消息
|
||||
3. **使用简单工具测试**:在自定义实现前先用内置工具验证
|
||||
4. **保留回退选项**:在退出实验性状态前不要完全依赖原生显示
|
||||
|
||||
## 📖 相关文档
|
||||
|
||||
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - 如何创建和使用自定义工具
|
||||
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - 技术实现细节
|
||||
- [WORKFLOW.md](WORKFLOW.md) - 完整集成工作流程
|
||||
|
||||
## 🐛 报告问题
|
||||
|
||||
如果您在使用原生工具显示时遇到问题:
|
||||
|
||||
1. 启用 `DEBUG` 和 `USE_NATIVE_TOOL_DISPLAY`
|
||||
2. 打开浏览器控制台(F12)
|
||||
3. 触发工具调用
|
||||
4. 复制任何错误消息
|
||||
5. 报告到 [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||
|
||||
包含:
|
||||
|
||||
- OpenWebUI 版本
|
||||
- 浏览器和版本
|
||||
- 控制台的错误消息
|
||||
- 复现步骤
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie | **版本:** 0.2.0 | **许可证:** MIT
|
||||
@@ -0,0 +1,509 @@
|
||||
# OpenWebUI Function 集成方案
|
||||
|
||||
## 🎯 核心挑战
|
||||
|
||||
在 Copilot Tool Handler 中调用 OpenWebUI Functions 的关键问题:
|
||||
|
||||
**问题:** Copilot SDK 的 Tool Handler 是一个独立的回调函数,如何在这个上下文中访问和执行 OpenWebUI 的 Function?
|
||||
|
||||
---
|
||||
|
||||
## 🔍 OpenWebUI Function 系统分析
|
||||
|
||||
### 1. Function 数据结构
|
||||
|
||||
OpenWebUI 的 Function/Tool 传递格式:
|
||||
|
||||
```python
|
||||
body = {
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get current weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Function 执行机制
|
||||
|
||||
OpenWebUI Functions 的执行方式有几种可能:
|
||||
|
||||
#### 选项 A: 通过 Function ID 调用内部 API
|
||||
|
||||
```python
|
||||
# 假设 OpenWebUI 提供内部 API
|
||||
from open_webui.apps.webui.models.functions import Functions
|
||||
|
||||
function_id = "function_uuid" # 需要从配置中获取
|
||||
result = await Functions.execute_function(
|
||||
function_id=function_id,
|
||||
arguments={"location": "Beijing"}
|
||||
)
|
||||
```
|
||||
|
||||
#### 选项 B: 通过 **event_emitter** 触发
|
||||
|
||||
```python
|
||||
# 通过事件系统触发 function 执行
|
||||
if __event_emitter__:
|
||||
await __event_emitter__({
|
||||
"type": "function_call",
|
||||
"data": {
|
||||
"name": "get_weather",
|
||||
"arguments": {"location": "Beijing"}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 选项 C: 自己实现 Function 逻辑
|
||||
|
||||
```python
|
||||
# 在 Pipe 内部实现常用功能
|
||||
class Pipe:
|
||||
def _builtin_get_weather(self, location: str) -> dict:
|
||||
# 实现天气查询
|
||||
pass
|
||||
|
||||
def _builtin_search_web(self, query: str) -> dict:
|
||||
# 实现网页搜索
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 推荐方案:混合架构
|
||||
|
||||
### 架构设计
|
||||
|
||||
```
|
||||
User Message
|
||||
↓
|
||||
OpenWebUI UI (Functions 已配置)
|
||||
↓
|
||||
Pipe.pipe(body) - body 包含 tools[]
|
||||
↓
|
||||
转换为 Copilot Tools + 存储 Function Registry
|
||||
↓
|
||||
Copilot 决定调用 Tool
|
||||
↓
|
||||
Tool Handler 查询 Registry → 执行对应逻辑
|
||||
↓
|
||||
返回结果给 Copilot
|
||||
↓
|
||||
继续生成回答
|
||||
```
|
||||
|
||||
### 核心实现
|
||||
|
||||
#### 1. Function Registry(函数注册表)
|
||||
|
||||
```python
|
||||
class Pipe:
|
||||
def __init__(self):
|
||||
# ...
|
||||
self._function_registry = {} # {function_name: callable}
|
||||
self._function_metadata = {} # {function_name: metadata}
|
||||
```
|
||||
|
||||
#### 2. 注册 Functions
|
||||
|
||||
```python
|
||||
def _register_openwebui_functions(
|
||||
self,
|
||||
owui_functions: List[dict],
|
||||
__event_emitter__=None,
|
||||
__event_call__=None
|
||||
):
|
||||
"""
|
||||
注册 OpenWebUI Functions 到内部 registry
|
||||
|
||||
关键:将 function 定义和执行逻辑关联起来
|
||||
"""
|
||||
for func_def in owui_functions:
|
||||
if func_def.get("type") != "function":
|
||||
continue
|
||||
|
||||
func_info = func_def.get("function", {})
|
||||
func_name = func_info.get("name")
|
||||
|
||||
if not func_name:
|
||||
continue
|
||||
|
||||
# 存储元数据
|
||||
self._function_metadata[func_name] = {
|
||||
"description": func_info.get("description", ""),
|
||||
"parameters": func_info.get("parameters", {}),
|
||||
"original_def": func_def
|
||||
}
|
||||
|
||||
# 创建执行器(关键)
|
||||
executor = self._create_function_executor(
|
||||
func_name,
|
||||
func_def,
|
||||
__event_emitter__,
|
||||
__event_call__
|
||||
)
|
||||
|
||||
self._function_registry[func_name] = executor
|
||||
```
|
||||
|
||||
#### 3. Function Executor 工厂
|
||||
|
||||
```python
|
||||
def _create_function_executor(
|
||||
self,
|
||||
func_name: str,
|
||||
func_def: dict,
|
||||
__event_emitter__=None,
|
||||
__event_call__=None
|
||||
):
|
||||
"""
|
||||
为每个 function 创建执行器
|
||||
|
||||
策略:
|
||||
1. 优先使用内置实现
|
||||
2. 尝试调用 OpenWebUI API
|
||||
3. 返回错误
|
||||
"""
|
||||
|
||||
async def executor(arguments: dict) -> dict:
|
||||
# 策略 1: 检查是否有内置实现
|
||||
builtin_method = getattr(self, f"_builtin_{func_name}", None)
|
||||
if builtin_method:
|
||||
self._emit_debug_log_sync(
|
||||
f"Using builtin implementation for {func_name}",
|
||||
__event_call__
|
||||
)
|
||||
try:
|
||||
result = builtin_method(arguments)
|
||||
if inspect.iscoroutine(result):
|
||||
result = await result
|
||||
return {"success": True, "result": result}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# 策略 2: 尝试通过 Event Emitter 调用
|
||||
if __event_emitter__:
|
||||
try:
|
||||
# 尝试触发 function_call 事件
|
||||
response_queue = asyncio.Queue()
|
||||
|
||||
await __event_emitter__({
|
||||
"type": "function_call",
|
||||
"data": {
|
||||
"name": func_name,
|
||||
"arguments": arguments,
|
||||
"response_queue": response_queue # 回调队列
|
||||
}
|
||||
})
|
||||
|
||||
# 等待结果(带超时)
|
||||
result = await asyncio.wait_for(
|
||||
response_queue.get(),
|
||||
timeout=self.valves.TOOL_TIMEOUT
|
||||
)
|
||||
|
||||
return {"success": True, "result": result}
|
||||
except asyncio.TimeoutError:
|
||||
return {"success": False, "error": "Function execution timeout"}
|
||||
except Exception as e:
|
||||
self._emit_debug_log_sync(
|
||||
f"Event emitter call failed: {e}",
|
||||
__event_call__
|
||||
)
|
||||
# 继续尝试其他方法
|
||||
|
||||
# 策略 3: 尝试调用 OpenWebUI internal API
|
||||
try:
|
||||
# 这需要研究 OpenWebUI 源码确定正确的调用方式
|
||||
from open_webui.apps.webui.models.functions import Functions
|
||||
|
||||
# 需要获取 function_id(这是关键问题)
|
||||
function_id = self._get_function_id_by_name(func_name)
|
||||
|
||||
if function_id:
|
||||
result = await Functions.execute(
|
||||
function_id=function_id,
|
||||
params=arguments
|
||||
)
|
||||
return {"success": True, "result": result}
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
self._emit_debug_log_sync(
|
||||
f"OpenWebUI API call failed: {e}",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
# 策略 4: 返回"未实现"错误
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Function '{func_name}' is not implemented. "
|
||||
"Please implement it as a builtin method or ensure OpenWebUI API is available."
|
||||
}
|
||||
|
||||
return executor
|
||||
```
|
||||
|
||||
#### 4. Tool Handler 实现
|
||||
|
||||
```python
|
||||
def _create_tool_handler(self, tool_name: str, __event_call__=None):
|
||||
"""为 Copilot SDK 创建 Tool Handler"""
|
||||
|
||||
async def handler(invocation: dict) -> dict:
|
||||
"""
|
||||
Copilot Tool Handler
|
||||
|
||||
invocation: {
|
||||
"session_id": str,
|
||||
"tool_call_id": str,
|
||||
"tool_name": str,
|
||||
"arguments": dict
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 从 registry 获取 executor
|
||||
executor = self._function_registry.get(invocation["tool_name"])
|
||||
|
||||
if not executor:
|
||||
return {
|
||||
"textResultForLlm": f"Function '{invocation['tool_name']}' not found.",
|
||||
"resultType": "failure",
|
||||
"error": "function_not_found",
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
|
||||
# 执行 function
|
||||
self._emit_debug_log_sync(
|
||||
f"Executing function: {invocation['tool_name']}({invocation['arguments']})",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
exec_result = await executor(invocation["arguments"])
|
||||
|
||||
# 处理结果
|
||||
if exec_result.get("success"):
|
||||
result_text = str(exec_result.get("result", ""))
|
||||
return {
|
||||
"textResultForLlm": result_text,
|
||||
"resultType": "success",
|
||||
"error": None,
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
else:
|
||||
error_msg = exec_result.get("error", "Unknown error")
|
||||
return {
|
||||
"textResultForLlm": f"Function execution failed: {error_msg}",
|
||||
"resultType": "failure",
|
||||
"error": error_msg,
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self._emit_debug_log_sync(
|
||||
f"Tool handler error: {e}",
|
||||
__event_call__
|
||||
)
|
||||
return {
|
||||
"textResultForLlm": "An unexpected error occurred during function execution.",
|
||||
"resultType": "failure",
|
||||
"error": str(e),
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
|
||||
return handler
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 内置 Functions 实现示例
|
||||
|
||||
### 示例 1: 获取当前时间
|
||||
|
||||
```python
|
||||
def _builtin_get_current_time(self, arguments: dict) -> str:
|
||||
"""内置实现:获取当前时间"""
|
||||
from datetime import datetime
|
||||
|
||||
timezone = arguments.get("timezone", "UTC")
|
||||
format_str = arguments.get("format", "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
now = datetime.now()
|
||||
return now.strftime(format_str)
|
||||
```
|
||||
|
||||
### 示例 2: 简单计算器
|
||||
|
||||
```python
|
||||
def _builtin_calculate(self, arguments: dict) -> str:
|
||||
"""内置实现:数学计算"""
|
||||
expression = arguments.get("expression", "")
|
||||
|
||||
try:
|
||||
# 安全的数学计算(仅允许基本运算)
|
||||
allowed_chars = set("0123456789+-*/()., ")
|
||||
if not all(c in allowed_chars for c in expression):
|
||||
raise ValueError("Invalid characters in expression")
|
||||
|
||||
result = eval(expression, {"__builtins__": {}})
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Calculation error: {e}")
|
||||
```
|
||||
|
||||
### 示例 3: 网页搜索(需要外部 API)
|
||||
|
||||
```python
|
||||
async def _builtin_search_web(self, arguments: dict) -> str:
|
||||
"""内置实现:网页搜索(使用 DuckDuckGo)"""
|
||||
query = arguments.get("query", "")
|
||||
max_results = arguments.get("max_results", 5)
|
||||
|
||||
try:
|
||||
# 使用 duckduckgo_search 库
|
||||
from duckduckgo_search import DDGS
|
||||
|
||||
results = []
|
||||
with DDGS() as ddgs:
|
||||
for r in ddgs.text(query, max_results=max_results):
|
||||
results.append({
|
||||
"title": r.get("title", ""),
|
||||
"url": r.get("href", ""),
|
||||
"snippet": r.get("body", "")
|
||||
})
|
||||
|
||||
# 格式化结果
|
||||
formatted = "\n\n".join([
|
||||
f"**{r['title']}**\n{r['url']}\n{r['snippet']}"
|
||||
for r in results
|
||||
])
|
||||
|
||||
return formatted
|
||||
except Exception as e:
|
||||
raise ValueError(f"Search failed: {e}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 完整集成流程
|
||||
|
||||
### pipe() 方法中的集成
|
||||
|
||||
```python
|
||||
async def pipe(
|
||||
self,
|
||||
body: dict,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__event_emitter__=None,
|
||||
__event_call__=None,
|
||||
) -> Union[str, AsyncGenerator]:
|
||||
# ... 现有代码 ...
|
||||
|
||||
# ✅ Step 1: 提取 OpenWebUI Functions
|
||||
owui_functions = body.get("tools", [])
|
||||
|
||||
# ✅ Step 2: 注册 Functions
|
||||
if self.valves.ENABLE_TOOLS and owui_functions:
|
||||
self._register_openwebui_functions(
|
||||
owui_functions,
|
||||
__event_emitter__,
|
||||
__event_call__
|
||||
)
|
||||
|
||||
# ✅ Step 3: 转换为 Copilot Tools
|
||||
copilot_tools = []
|
||||
for func_name in self._function_registry.keys():
|
||||
metadata = self._function_metadata[func_name]
|
||||
copilot_tools.append({
|
||||
"name": func_name,
|
||||
"description": metadata["description"],
|
||||
"parameters": metadata["parameters"],
|
||||
"handler": self._create_tool_handler(func_name, __event_call__)
|
||||
})
|
||||
|
||||
# ✅ Step 4: 创建 Session 并传递 Tools
|
||||
session_config = SessionConfig(
|
||||
model=real_model_id,
|
||||
tools=copilot_tools, # ✅ 关键
|
||||
...
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
|
||||
# ... 后续代码 ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 待解决问题
|
||||
|
||||
### 1. Function ID 映射
|
||||
|
||||
**问题:** OpenWebUI Functions 通常通过 UUID 标识,但 body 中只有 name
|
||||
|
||||
**解决思路:**
|
||||
|
||||
- 在 OpenWebUI 启动时建立 name → id 映射表
|
||||
- 或者修改 OpenWebUI 在 body 中同时传递 id
|
||||
|
||||
### 2. Event Emitter 回调机制
|
||||
|
||||
**问题:** 不确定 **event_emitter** 是否支持 function_call 事件
|
||||
|
||||
**验证方法:**
|
||||
|
||||
```python
|
||||
# 测试代码
|
||||
await __event_emitter__({
|
||||
"type": "function_call",
|
||||
"data": {"name": "test_func", "arguments": {}}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 异步执行超时
|
||||
|
||||
**问题:** 某些 Functions 可能执行很慢
|
||||
|
||||
**解决方案:**
|
||||
|
||||
- 实现 timeout 机制(已在 executor 中实现)
|
||||
- 对于长时间运行的任务,考虑返回"processing"状态
|
||||
|
||||
---
|
||||
|
||||
## 📝 实现清单
|
||||
|
||||
- [ ] 实现 _function_registry 和 _function_metadata
|
||||
- [ ] 实现 _register_openwebui_functions()
|
||||
- [ ] 实现 _create_function_executor()
|
||||
- [ ] 实现 _create_tool_handler()
|
||||
- [ ] 实现 3-5 个常用内置 Functions
|
||||
- [ ] 测试 Function 注册和调用流程
|
||||
- [ ] 验证 **event_emitter** 机制
|
||||
- [ ] 研究 OpenWebUI Functions API
|
||||
- [ ] 添加错误处理和超时机制
|
||||
- [ ] 更新文档
|
||||
|
||||
---
|
||||
|
||||
**下一步行动:**
|
||||
|
||||
1. 实现基础的 Function Registry
|
||||
2. 添加 2-3 个简单的内置 Functions(如 get_time, calculate)
|
||||
3. 测试基本的 Tool Calling 流程
|
||||
4. 根据测试结果调整架构
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**日期:** 2026-01-26
|
||||
@@ -0,0 +1,708 @@
|
||||
# SessionConfig 完整功能集成指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档详细说明如何将 GitHub Copilot SDK 的 `SessionConfig` 所有功能集成到 OpenWebUI Pipe 中。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能清单与集成状态
|
||||
|
||||
| 功能 | 状态 | 优先级 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `session_id` | ✅ 已实现 | 高 | 使用 OpenWebUI chat_id |
|
||||
| `model` | ✅ 已实现 | 高 | 从 body 动态获取 |
|
||||
| `tools` | ✅ 已实现 | 高 | v0.2.0 新增示例工具 |
|
||||
| `streaming` | ✅ 已实现 | 高 | 支持流式输出 |
|
||||
| `infinite_sessions` | ✅ 已实现 | 高 | 自动上下文压缩 |
|
||||
| `system_message` | ⚠️ 部分支持 | 中 | 可通过 Valves 添加 |
|
||||
| `available_tools` | ⚠️ 部分支持 | 中 | 已有 AVAILABLE_TOOLS |
|
||||
| `excluded_tools` | 🔲 未实现 | 低 | 可添加到 Valves |
|
||||
| `on_permission_request` | 🔲 未实现 | 中 | 需要 UI 交互支持 |
|
||||
| `provider` (BYOK) | 🔲 未实现 | 低 | 高级功能 |
|
||||
| `mcp_servers` | 🔲 未实现 | 低 | MCP 协议支持 |
|
||||
| `custom_agents` | 🔲 未实现 | 低 | 自定义代理 |
|
||||
| `config_dir` | 🔲 未实现 | 低 | 可通过 WORKSPACE_DIR |
|
||||
| `skill_directories` | 🔲 未实现 | 低 | 技能系统 |
|
||||
| `disabled_skills` | 🔲 未实现 | 低 | 技能过滤 |
|
||||
|
||||
---
|
||||
|
||||
## 📖 详细集成方案
|
||||
|
||||
### 1. ✅ session_id(已实现)
|
||||
|
||||
**功能:** 持久化会话 ID
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id if chat_id else None, # 使用 OpenWebUI 的 chat_id
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
- OpenWebUI 的 `chat_id` 直接映射为 Copilot 的 `session_id`
|
||||
- 会话状态持久化到磁盘
|
||||
- 支持跨重启恢复对话
|
||||
|
||||
---
|
||||
|
||||
### 2. ✅ model(已实现)
|
||||
|
||||
**功能:** 选择 Copilot 模型
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
# 从用户选择的模型中提取
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):]
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
MODEL_ID: str = Field(
|
||||
default="claude-sonnet-4.5",
|
||||
description="默认模型(动态获取失败时使用)"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ✅ tools(已实现 - v0.2.0)
|
||||
|
||||
**功能:** 自定义工具/函数调用
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
custom_tools = self._initialize_custom_tools()
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**Valves 配置:**
|
||||
|
||||
```python
|
||||
ENABLE_TOOLS: bool = Field(default=False)
|
||||
AVAILABLE_TOOLS: str = Field(default="all")
|
||||
```
|
||||
|
||||
**内置示例工具:**
|
||||
|
||||
- `get_current_time` - 获取当前时间
|
||||
- `calculate` - 数学计算
|
||||
- `generate_random_number` - 随机数生成
|
||||
|
||||
**扩展方法:** 参考 [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
|
||||
---
|
||||
|
||||
### 4. ⚠️ system_message(部分支持)
|
||||
|
||||
**功能:** 自定义系统提示词
|
||||
|
||||
**集成方案:**
|
||||
|
||||
#### 方案 A:通过 Valves 添加(推荐)
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SYSTEM_MESSAGE: str = Field(
|
||||
default="",
|
||||
description="Custom system message (append mode)"
|
||||
)
|
||||
SYSTEM_MESSAGE_MODE: str = Field(
|
||||
default="append",
|
||||
description="System message mode: 'append' or 'replace'"
|
||||
)
|
||||
```
|
||||
|
||||
**实现:**
|
||||
|
||||
```python
|
||||
async def pipe(self, body, ...):
|
||||
system_message_config = None
|
||||
|
||||
if self.valves.SYSTEM_MESSAGE:
|
||||
if self.valves.SYSTEM_MESSAGE_MODE == "replace":
|
||||
system_message_config = {
|
||||
"mode": "replace",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
else:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": self.valves.SYSTEM_MESSAGE
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:从 OpenWebUI 系统提示词读取
|
||||
|
||||
```python
|
||||
# 从 body 中获取系统提示词
|
||||
system_prompt = body.get("system", "")
|
||||
if system_prompt:
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": system_prompt
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
|
||||
- `append` 模式:在默认系统提示词后追加
|
||||
- `replace` 模式:完全替换(移除 SDK 安全保护)
|
||||
|
||||
---
|
||||
|
||||
### 5. ⚠️ available_tools / excluded_tools
|
||||
|
||||
**功能:** 工具白名单/黑名单
|
||||
|
||||
**当前部分支持:**
|
||||
|
||||
```python
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="'all' or comma-separated list"
|
||||
)
|
||||
```
|
||||
|
||||
**增强实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="all",
|
||||
description="Available tools (comma-separated or 'all')"
|
||||
)
|
||||
EXCLUDED_TOOLS: str = Field(
|
||||
default="",
|
||||
description="Excluded tools (comma-separated)"
|
||||
)
|
||||
```
|
||||
|
||||
**应用到 SessionConfig:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
tools=custom_tools,
|
||||
available_tools=self._parse_tool_list(self.valves.AVAILABLE_TOOLS),
|
||||
excluded_tools=self._parse_tool_list(self.valves.EXCLUDED_TOOLS),
|
||||
...
|
||||
)
|
||||
|
||||
def _parse_tool_list(self, value: str) -> list[str]:
|
||||
"""解析工具列表"""
|
||||
if not value or value == "all":
|
||||
return []
|
||||
return [t.strip() for t in value.split(",") if t.strip()]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 🔲 on_permission_request(未实现)
|
||||
|
||||
**功能:** 处理权限请求(shell 命令、文件写入等)
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- Copilot 需要执行 shell 命令
|
||||
- 需要写入文件
|
||||
- 需要访问 URL
|
||||
|
||||
**集成挑战:**
|
||||
|
||||
- 需要 OpenWebUI 前端支持实时权限弹窗
|
||||
- 需要异步处理用户确认
|
||||
|
||||
**推荐方案:**
|
||||
|
||||
#### 方案 A:自动批准(开发/测试环境)
|
||||
|
||||
```python
|
||||
async def auto_approve_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""自动批准所有权限请求(危险!)"""
|
||||
return {
|
||||
"kind": "approved",
|
||||
"rules": []
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=auto_approve_permission_handler,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
#### 方案 B:基于规则的批准
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
async def rule_based_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
kind = request.get("kind")
|
||||
|
||||
if kind == "shell" and not self.valves.ALLOW_SHELL_COMMANDS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "write" and not self.valves.ALLOW_FILE_WRITE:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
if kind == "url" and not self.valves.ALLOW_URL_ACCESS:
|
||||
return {"kind": "denied-by-rules"}
|
||||
|
||||
return {"kind": "approved", "rules": []}
|
||||
```
|
||||
|
||||
#### 方案 C:通过 Event Emitter 请求用户确认(理想)
|
||||
|
||||
```python
|
||||
async def interactive_permission_handler(
|
||||
request: dict,
|
||||
context: dict
|
||||
) -> dict:
|
||||
"""通过前端请求用户确认"""
|
||||
if not __event_emitter__:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
|
||||
# 发送权限请求到前端
|
||||
response_queue = asyncio.Queue()
|
||||
await __event_emitter__({
|
||||
"type": "permission_request",
|
||||
"data": {
|
||||
"kind": request.get("kind"),
|
||||
"description": request.get("description"),
|
||||
"response_queue": response_queue
|
||||
}
|
||||
})
|
||||
|
||||
# 等待用户响应(带超时)
|
||||
try:
|
||||
user_response = await asyncio.wait_for(
|
||||
response_queue.get(),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if user_response.get("approved"):
|
||||
return {"kind": "approved", "rules": []}
|
||||
else:
|
||||
return {"kind": "denied-interactively-by-user"}
|
||||
except asyncio.TimeoutError:
|
||||
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 🔲 provider(BYOK - Bring Your Own Key)
|
||||
|
||||
**功能:** 使用自己的 API 密钥连接 OpenAI/Azure/Anthropic
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 不使用 GitHub Copilot 配额
|
||||
- 直接连接云服务提供商
|
||||
- 使用 Azure OpenAI 部署
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(
|
||||
default="openai",
|
||||
description="Provider type: openai, azure, anthropic"
|
||||
)
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
PROVIDER_BEARER_TOKEN: str = Field(default="")
|
||||
AZURE_API_VERSION: str = Field(default="2024-10-21")
|
||||
|
||||
def _build_provider_config(self) -> dict | None:
|
||||
"""构建 Provider 配置"""
|
||||
if not self.valves.USE_CUSTOM_PROVIDER:
|
||||
return None
|
||||
|
||||
config = {
|
||||
"type": self.valves.PROVIDER_TYPE,
|
||||
"base_url": self.valves.PROVIDER_BASE_URL,
|
||||
}
|
||||
|
||||
if self.valves.PROVIDER_API_KEY:
|
||||
config["api_key"] = self.valves.PROVIDER_API_KEY
|
||||
|
||||
if self.valves.PROVIDER_BEARER_TOKEN:
|
||||
config["bearer_token"] = self.valves.PROVIDER_BEARER_TOKEN
|
||||
|
||||
if self.valves.PROVIDER_TYPE == "azure":
|
||||
config["azure"] = {
|
||||
"api_version": self.valves.AZURE_API_VERSION
|
||||
}
|
||||
|
||||
# 自动推断 wire_api
|
||||
if self.valves.PROVIDER_TYPE == "anthropic":
|
||||
config["wire_api"] = "responses"
|
||||
else:
|
||||
config["wire_api"] = "completions"
|
||||
|
||||
return config
|
||||
```
|
||||
|
||||
**应用:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. ✅ streaming(已实现)
|
||||
|
||||
**功能:** 流式输出
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
streaming=body.get("stream", False),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 🔲 mcp_servers(MCP 协议)
|
||||
|
||||
**功能:** Model Context Protocol 服务器集成
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 连接外部数据源(数据库、API)
|
||||
- 集成第三方服务
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
MCP_SERVERS_CONFIG: str = Field(
|
||||
default="{}",
|
||||
description="MCP servers configuration (JSON format)"
|
||||
)
|
||||
|
||||
def _parse_mcp_servers(self) -> dict | None:
|
||||
"""解析 MCP 服务器配置"""
|
||||
if not self.valves.MCP_SERVERS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.MCP_SERVERS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"type": "local",
|
||||
"command": "mcp-server-sqlite",
|
||||
"args": ["--db", "/path/to/db.sqlite"],
|
||||
"tools": ["*"]
|
||||
},
|
||||
"weather": {
|
||||
"type": "http",
|
||||
"url": "https://weather-api.example.com/mcp",
|
||||
"tools": ["get_weather", "get_forecast"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. 🔲 custom_agents
|
||||
|
||||
**功能:** 自定义 AI 代理
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 专门化的子代理(如代码审查、文档编写)
|
||||
- 不同的提示词策略
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CUSTOM_AGENTS_CONFIG: str = Field(
|
||||
default="[]",
|
||||
description="Custom agents configuration (JSON array)"
|
||||
)
|
||||
|
||||
def _parse_custom_agents(self) -> list | None:
|
||||
"""解析自定义代理配置"""
|
||||
if not self.valves.CUSTOM_AGENTS_CONFIG:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(self.valves.CUSTOM_AGENTS_CONFIG)
|
||||
except:
|
||||
return None
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "code_reviewer",
|
||||
"display_name": "Code Reviewer",
|
||||
"description": "Reviews code for best practices",
|
||||
"prompt": "You are an expert code reviewer. Focus on security, performance, and maintainability.",
|
||||
"tools": ["read_file", "write_file"],
|
||||
"infer": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. 🔲 config_dir
|
||||
|
||||
**功能:** 自定义配置目录
|
||||
|
||||
**当前支持:**
|
||||
|
||||
- 已有 `WORKSPACE_DIR` 控制工作目录
|
||||
|
||||
**增强方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CONFIG_DIR: str = Field(
|
||||
default="",
|
||||
description="Custom config directory for session state"
|
||||
)
|
||||
|
||||
session_config = SessionConfig(
|
||||
config_dir=self.valves.CONFIG_DIR if self.valves.CONFIG_DIR else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12. 🔲 skill_directories / disabled_skills
|
||||
|
||||
**功能:** Copilot Skills 系统
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 加载自定义技能包
|
||||
- 禁用特定技能
|
||||
|
||||
**集成方案:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
SKILL_DIRECTORIES: str = Field(
|
||||
default="",
|
||||
description="Comma-separated skill directories"
|
||||
)
|
||||
DISABLED_SKILLS: str = Field(
|
||||
default="",
|
||||
description="Comma-separated disabled skills"
|
||||
)
|
||||
|
||||
def _parse_skills_config(self):
|
||||
"""解析技能配置"""
|
||||
skill_dirs = []
|
||||
if self.valves.SKILL_DIRECTORIES:
|
||||
skill_dirs = [
|
||||
d.strip()
|
||||
for d in self.valves.SKILL_DIRECTORIES.split(",")
|
||||
if d.strip()
|
||||
]
|
||||
|
||||
disabled = []
|
||||
if self.valves.DISABLED_SKILLS:
|
||||
disabled = [
|
||||
s.strip()
|
||||
for s in self.valves.DISABLED_SKILLS.split(",")
|
||||
if s.strip()
|
||||
]
|
||||
|
||||
return skill_dirs, disabled
|
||||
|
||||
# 应用
|
||||
skill_dirs, disabled_skills = self._parse_skills_config()
|
||||
session_config = SessionConfig(
|
||||
skill_directories=skill_dirs if skill_dirs else None,
|
||||
disabled_skills=disabled_skills if disabled_skills else None,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13. ✅ infinite_sessions(已实现)
|
||||
|
||||
**功能:** 无限会话与自动上下文压缩
|
||||
|
||||
**当前实现:**
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
INFINITE_SESSION: bool = Field(default=True)
|
||||
COMPACTION_THRESHOLD: float = Field(default=0.8)
|
||||
BUFFER_THRESHOLD: float = Field(default=0.95)
|
||||
|
||||
infinite_session_config = None
|
||||
if self.valves.INFINITE_SESSION:
|
||||
infinite_session_config = {
|
||||
"enabled": True,
|
||||
"background_compaction_threshold": self.valves.COMPACTION_THRESHOLD,
|
||||
"buffer_exhaustion_threshold": self.valves.BUFFER_THRESHOLD,
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
infinite_sessions=infinite_session_config,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 实施优先级建议
|
||||
|
||||
### 🔥 高优先级(立即实现)
|
||||
|
||||
1. **system_message** - 用户最常需要的功能
|
||||
2. **on_permission_request (基于规则)** - 安全性需求
|
||||
|
||||
### 📌 中优先级(下一阶段)
|
||||
|
||||
3. **excluded_tools** - 完善工具管理
|
||||
4. **provider (BYOK)** - 高级用户需求
|
||||
5. **config_dir** - 增强会话管理
|
||||
|
||||
### 📋 低优先级(可选)
|
||||
|
||||
6. **mcp_servers** - 高级集成
|
||||
7. **custom_agents** - 专业化功能
|
||||
8. **skill_directories** - 生态系统功能
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速实施计划
|
||||
|
||||
### Phase 1: 基础增强(1-2小时)
|
||||
|
||||
```python
|
||||
# 添加到 Valves
|
||||
SYSTEM_MESSAGE: str = Field(default="")
|
||||
SYSTEM_MESSAGE_MODE: str = Field(default="append")
|
||||
EXCLUDED_TOOLS: str = Field(default="")
|
||||
|
||||
# 添加到 pipe() 方法
|
||||
system_message_config = self._build_system_message_config()
|
||||
excluded_tools = self._parse_tool_list(self.valves.EXCLUDED_TOOLS)
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
excluded_tools=excluded_tools,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 2: 权限管理(2-3小时)
|
||||
|
||||
```python
|
||||
# 添加权限控制 Valves
|
||||
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||
|
||||
# 实现权限处理器
|
||||
session_config = SessionConfig(
|
||||
on_permission_request=self._create_permission_handler(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### Phase 3: BYOK 支持(3-4小时)
|
||||
|
||||
```python
|
||||
# 添加 Provider Valves
|
||||
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||
PROVIDER_TYPE: str = Field(default="openai")
|
||||
PROVIDER_BASE_URL: str = Field(default="")
|
||||
PROVIDER_API_KEY: str = Field(default="")
|
||||
|
||||
# 实现 Provider 配置
|
||||
session_config = SessionConfig(
|
||||
provider=self._build_provider_config(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 参考资源
|
||||
|
||||
- **SDK 类型定义**: `/opt/homebrew/.../copilot/types.py`
|
||||
- **工具系统**: [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||
- **SDK 文档**: <https://github.com/github/copilot-sdk>
|
||||
|
||||
---
|
||||
|
||||
## ✅ 实施检查清单
|
||||
|
||||
使用此清单跟踪实施进度:
|
||||
|
||||
- [x] session_id
|
||||
- [x] model
|
||||
- [x] tools
|
||||
- [x] streaming
|
||||
- [x] infinite_sessions
|
||||
- [ ] system_message
|
||||
- [ ] available_tools (完善)
|
||||
- [ ] excluded_tools
|
||||
- [ ] on_permission_request
|
||||
- [ ] provider (BYOK)
|
||||
- [ ] mcp_servers
|
||||
- [ ] custom_agents
|
||||
- [ ] config_dir
|
||||
- [ ] skill_directories
|
||||
- [ ] disabled_skills
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** v1.0
|
||||
**日期:** 2026-01-26
|
||||
**更新:** 随功能实施持续更新
|
||||
191
plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md
Normal file
191
plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 🛠️ Custom Tools Usage / 自定义工具使用指南
|
||||
|
||||
## Overview / 概览
|
||||
|
||||
This pipe includes **1 example custom tool** that demonstrates how to use GitHub Copilot SDK's tool calling feature.
|
||||
|
||||
本 Pipe 包含 **1 个示例自定义工具**,展示如何使用 GitHub Copilot SDK 的工具调用功能。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start / 快速开始
|
||||
|
||||
### 1. Enable Tools / 启用工具
|
||||
|
||||
In Valves configuration:
|
||||
在 Valves 配置中:
|
||||
|
||||
```
|
||||
ENABLE_TOOLS: true
|
||||
AVAILABLE_TOOLS: all
|
||||
```
|
||||
|
||||
### 2. Test with Conversations / 测试对话
|
||||
|
||||
Try these examples:
|
||||
尝试这些示例:
|
||||
|
||||
**English:**
|
||||
|
||||
- "Give me a random number between 1 and 100"
|
||||
|
||||
**中文:**
|
||||
|
||||
- "给我一个 1 到 100 之间的随机数"
|
||||
|
||||
---
|
||||
|
||||
## 📦 Included Tools / 内置工具
|
||||
|
||||
### 1. `generate_random_number` / 生成随机数
|
||||
|
||||
**Description:** Generate a random integer
|
||||
**描述:** 生成随机整数
|
||||
|
||||
**Parameters / 参数:**
|
||||
|
||||
- `min` (optional): Minimum value (default: 1)
|
||||
- `max` (optional): Maximum value (default: 100)
|
||||
- `min` (可选): 最小值 (默认: 1)
|
||||
- `max` (可选): 最大值 (默认: 100)
|
||||
|
||||
**Example / 示例:**
|
||||
|
||||
```
|
||||
User: "Give me a random number between 1 and 10"
|
||||
Copilot: [calls generate_random_number with min=1, max=10] "Generated random number: 7"
|
||||
|
||||
用户: "给我一个 1 到 10 之间的随机数"
|
||||
Copilot: [调用 generate_random_number,参数 min=1, max=10] "生成的随机数: 7"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Advanced Configuration / 高级配置
|
||||
|
||||
### Select Specific Tools / 选择特定工具
|
||||
|
||||
Instead of enabling all tools, specify which ones to use:
|
||||
不启用所有工具,而是指定要使用的工具:
|
||||
|
||||
```
|
||||
ENABLE_TOOLS: true
|
||||
AVAILABLE_TOOLS: generate_random_number
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How Tool Calling Works / 工具调用的工作原理
|
||||
|
||||
```
|
||||
1. User asks a question / 用户提问
|
||||
↓
|
||||
2. Copilot decides if it needs a tool / Copilot 决定是否需要工具
|
||||
↓
|
||||
3. If yes, Copilot calls the appropriate tool / 如果需要,调用相应工具
|
||||
↓
|
||||
4. Tool executes and returns result / 工具执行并返回结果
|
||||
↓
|
||||
5. Copilot uses the result to answer / Copilot 使用结果回答
|
||||
```
|
||||
|
||||
### Visual Feedback / 可视化反馈
|
||||
|
||||
When tools are called, you'll see:
|
||||
当工具被调用时,你会看到:
|
||||
|
||||
```
|
||||
🔧 **Calling tool**: `generate_random_number`
|
||||
✅ **Tool `generate_random_number` completed**
|
||||
|
||||
Generated random number: 7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Creating Your Own Tools / 创建自定义工具
|
||||
|
||||
Want to add your own tools? Follow this pattern (module-level tools):
|
||||
想要添加自己的工具?遵循这个模式(模块级工具):
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from copilot import define_tool
|
||||
|
||||
class MyToolParams(BaseModel):
|
||||
param_name: str = Field(description="Parameter description")
|
||||
|
||||
|
||||
@define_tool(description="Clear description of what the tool does and when to use it")
|
||||
async def my_tool(params: MyToolParams) -> str:
|
||||
# Do something
|
||||
result = do_something(params.param_name)
|
||||
return f"Result: {result}"
|
||||
```
|
||||
|
||||
Then register it in `_initialize_custom_tools()`:
|
||||
然后将它添加到 `_initialize_custom_tools()`:
|
||||
|
||||
```python
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
"my_tool": my_tool, # ✅ Add here
|
||||
}
|
||||
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Notes / 重要说明
|
||||
|
||||
### Security / 安全性
|
||||
|
||||
- Tools run in the same process as the pipe
|
||||
- Be careful with tools that execute code or access files
|
||||
- Always validate input parameters
|
||||
|
||||
- 工具在与 Pipe 相同的进程中运行
|
||||
- 谨慎处理执行代码或访问文件的工具
|
||||
- 始终验证输入参数
|
||||
|
||||
### Performance / 性能
|
||||
|
||||
- Tool execution is synchronous during streaming
|
||||
- Long-running tools may cause delays
|
||||
- Consider adding timeouts for external API calls
|
||||
|
||||
- 工具执行在流式传输期间是同步的
|
||||
- 长时间运行的工具可能导致延迟
|
||||
- 考虑为外部 API 调用添加超时
|
||||
|
||||
### Debugging / 调试
|
||||
|
||||
- Enable `DEBUG: true` to see tool events in the browser console
|
||||
- Check tool calls in `🔧 Calling tool` messages
|
||||
- Tool errors are displayed in the response
|
||||
|
||||
- 启用 `DEBUG: true` 在浏览器控制台查看工具事件
|
||||
- 在 `🔧 Calling tool` 消息中检查工具调用
|
||||
- 工具错误会显示在响应中
|
||||
|
||||
---
|
||||
|
||||
## 📖 References / 参考资料
|
||||
|
||||
- [Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||
- [COPILOT_TOOLS_QUICKSTART.md](COPILOT_TOOLS_QUICKSTART.md) - Detailed implementation guide
|
||||
- [JSON Schema](https://json-schema.org/) - For parameter definitions
|
||||
|
||||
---
|
||||
|
||||
**Version:** 0.2.3
|
||||
**Last Updated:** 2026-01-27
|
||||
@@ -0,0 +1,431 @@
|
||||
# GitHub Copilot SDK - Tool 功能实现指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本指南介绍如何在 GitHub Copilot SDK Pipe 中实现 Function/Tool Calling 功能。
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
OpenWebUI Tools/Functions
|
||||
↓ (转换)
|
||||
Copilot SDK Tool Definition
|
||||
↓ (注册)
|
||||
Session Tool Handlers
|
||||
↓ (调用)
|
||||
Tool Execution → Result
|
||||
↓ (返回)
|
||||
Continue Conversation
|
||||
```
|
||||
|
||||
### 核心接口
|
||||
|
||||
#### 1. Tool Definition(工具定义)
|
||||
|
||||
```python
|
||||
from copilot.types import Tool
|
||||
|
||||
tool = Tool(
|
||||
name="get_weather",
|
||||
description="Get current weather for a location",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "City name, e.g., 'San Francisco'"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"],
|
||||
"description": "Temperature unit"
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
},
|
||||
handler=weather_handler # 处理函数
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Tool Handler(处理函数)
|
||||
|
||||
```python
|
||||
from copilot.types import ToolInvocation, ToolResult
|
||||
|
||||
async def weather_handler(invocation: ToolInvocation) -> ToolResult:
|
||||
"""
|
||||
invocation 包含:
|
||||
- session_id: str
|
||||
- tool_call_id: str
|
||||
- tool_name: str
|
||||
- arguments: dict # {"location": "San Francisco", "unit": "celsius"}
|
||||
"""
|
||||
location = invocation["arguments"]["location"]
|
||||
|
||||
# 执行实际逻辑
|
||||
weather_data = await fetch_weather(location)
|
||||
|
||||
# 返回结果
|
||||
return ToolResult(
|
||||
textResultForLlm=f"Weather in {location}: {weather_data['temp']}°C, {weather_data['condition']}",
|
||||
resultType="success", # or "failure"
|
||||
error=None,
|
||||
toolTelemetry={"execution_time_ms": 150}
|
||||
)
|
||||
```
|
||||
|
||||
#### 3. Session Configuration(会话配置)
|
||||
|
||||
```python
|
||||
from copilot.types import SessionConfig
|
||||
|
||||
session_config = SessionConfig(
|
||||
model="claude-sonnet-4.5",
|
||||
tools=[tool1, tool2, tool3], # ✅ 传递工具列表
|
||||
available_tools=["get_weather", "search_web"], # 可选:过滤可用工具
|
||||
excluded_tools=["dangerous_tool"], # 可选:排除工具
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 实现方案
|
||||
|
||||
### 方案 A:桥接 OpenWebUI Tools(推荐)
|
||||
|
||||
#### 1. 添加 Valves 配置
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
ENABLE_TOOLS: bool = Field(
|
||||
default=True,
|
||||
description="Enable OpenWebUI tool integration"
|
||||
)
|
||||
TOOL_TIMEOUT: int = Field(
|
||||
default=30,
|
||||
description="Tool execution timeout (seconds)"
|
||||
)
|
||||
AVAILABLE_TOOLS: str = Field(
|
||||
default="",
|
||||
description="Filter specific tools (comma separated, empty = all)"
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. 实现 Tool 转换器
|
||||
|
||||
```python
|
||||
def _convert_openwebui_tools_to_copilot(
|
||||
self,
|
||||
owui_tools: List[dict],
|
||||
__event_call__=None
|
||||
) -> List[dict]:
|
||||
"""
|
||||
将 OpenWebUI tools 转换为 Copilot SDK 格式
|
||||
|
||||
OpenWebUI Tool 格式:
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather info",
|
||||
"parameters": {...} # JSON Schema
|
||||
}
|
||||
}
|
||||
"""
|
||||
copilot_tools = []
|
||||
|
||||
for tool in owui_tools:
|
||||
if tool.get("type") != "function":
|
||||
continue
|
||||
|
||||
func = tool.get("function", {})
|
||||
tool_name = func.get("name")
|
||||
|
||||
if not tool_name:
|
||||
continue
|
||||
|
||||
# 应用过滤器
|
||||
if self.valves.AVAILABLE_TOOLS:
|
||||
allowed = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
if tool_name not in allowed:
|
||||
continue
|
||||
|
||||
copilot_tools.append({
|
||||
"name": tool_name,
|
||||
"description": func.get("description", ""),
|
||||
"parameters": func.get("parameters", {}),
|
||||
"handler": self._create_tool_handler(tool_name, __event_call__)
|
||||
})
|
||||
|
||||
self._emit_debug_log_sync(
|
||||
f"Registered tool: {tool_name}",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
return copilot_tools
|
||||
```
|
||||
|
||||
#### 3. 实现动态 Tool Handler
|
||||
|
||||
```python
|
||||
def _create_tool_handler(self, tool_name: str, __event_call__=None):
|
||||
"""为每个 tool 创建 handler 函数"""
|
||||
|
||||
async def handler(invocation: dict) -> dict:
|
||||
"""
|
||||
Tool handler 实现
|
||||
|
||||
invocation 结构:
|
||||
{
|
||||
"session_id": "...",
|
||||
"tool_call_id": "...",
|
||||
"tool_name": "get_weather",
|
||||
"arguments": {"location": "Beijing"}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
self._emit_debug_log_sync(
|
||||
f"Tool called: {invocation['tool_name']} with {invocation['arguments']}",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
# 方法 1: 调用 OpenWebUI 内部 Function API
|
||||
result = await self._execute_openwebui_function(
|
||||
function_name=invocation["tool_name"],
|
||||
arguments=invocation["arguments"]
|
||||
)
|
||||
|
||||
# 方法 2: 通过 __event_emitter__ 触发(需要测试)
|
||||
# 方法 3: 直接实现工具逻辑
|
||||
|
||||
return {
|
||||
"textResultForLlm": str(result),
|
||||
"resultType": "success",
|
||||
"error": None,
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
return {
|
||||
"textResultForLlm": "Tool execution timed out.",
|
||||
"resultType": "failure",
|
||||
"error": "timeout",
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
except Exception as e:
|
||||
self._emit_debug_log_sync(
|
||||
f"Tool error: {e}",
|
||||
__event_call__
|
||||
)
|
||||
return {
|
||||
"textResultForLlm": f"Tool execution failed: {str(e)}",
|
||||
"resultType": "failure",
|
||||
"error": str(e),
|
||||
"toolTelemetry": {}
|
||||
}
|
||||
|
||||
return handler
|
||||
```
|
||||
|
||||
#### 4. 集成到 pipe() 方法
|
||||
|
||||
```python
|
||||
async def pipe(
|
||||
self,
|
||||
body: dict,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__event_emitter__=None,
|
||||
__event_call__=None,
|
||||
) -> Union[str, AsyncGenerator]:
|
||||
# ... 现有代码 ...
|
||||
|
||||
# ✅ 提取并转换 tools
|
||||
copilot_tools = []
|
||||
if self.valves.ENABLE_TOOLS and body.get("tools"):
|
||||
copilot_tools = self._convert_openwebui_tools_to_copilot(
|
||||
body["tools"],
|
||||
__event_call__
|
||||
)
|
||||
|
||||
await self._emit_debug_log(
|
||||
f"Enabled {len(copilot_tools)} tools",
|
||||
__event_call__
|
||||
)
|
||||
|
||||
# ✅ 传递给 SessionConfig
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id if chat_id else None,
|
||||
model=real_model_id,
|
||||
streaming=body.get("stream", False),
|
||||
tools=copilot_tools, # ✅ 关键
|
||||
infinite_sessions=infinite_session_config,
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
# ...
|
||||
```
|
||||
|
||||
#### 5. 处理 Tool 调用事件
|
||||
|
||||
```python
|
||||
def stream_response(...):
|
||||
def handler(event):
|
||||
event_type = str(event.type)
|
||||
|
||||
# ✅ Tool 调用开始
|
||||
if "tool_invocation_started" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
yield f"\n🔧 **Calling tool**: `{tool_name}`\n"
|
||||
|
||||
# ✅ Tool 调用完成
|
||||
elif "tool_invocation_completed" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
result = get_event_data(event, "result", "")
|
||||
yield f"\n✅ **Tool result**: {result}\n"
|
||||
|
||||
# ✅ Tool 调用失败
|
||||
elif "tool_invocation_failed" in event_type:
|
||||
tool_name = get_event_data(event, "tool_name", "")
|
||||
error = get_event_data(event, "error", "")
|
||||
yield f"\n❌ **Tool failed**: `{tool_name}` - {error}\n"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案 B:自定义 Tool 实现
|
||||
|
||||
#### Valves 配置
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
CUSTOM_TOOLS: str = Field(
|
||||
default="[]",
|
||||
description="Custom tools JSON: [{name, description, parameters, implementation}]"
|
||||
)
|
||||
```
|
||||
|
||||
#### 工具定义示例
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "calculate",
|
||||
"description": "Perform mathematical calculations",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Math expression, e.g., '2 + 2 * 3'"
|
||||
}
|
||||
},
|
||||
"required": ["expression"]
|
||||
},
|
||||
"implementation": "eval" // 或指定 Python 函数名
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试方案
|
||||
|
||||
### 1. 测试 Tool 定义
|
||||
|
||||
```python
|
||||
# 在 OpenWebUI 中创建一个简单的 Function:
|
||||
# Name: get_time
|
||||
# Description: Get current time
|
||||
# Parameters: {"type": "object", "properties": {}}
|
||||
|
||||
# 测试对话:
|
||||
# User: "What time is it?"
|
||||
# Expected: Copilot 调用 get_time tool,返回当前时间
|
||||
```
|
||||
|
||||
### 2. 测试 Tool 调用链
|
||||
|
||||
```python
|
||||
# User: "Search for Python tutorials and summarize the top 3 results"
|
||||
# Expected Flow:
|
||||
# 1. Copilot calls search_web(query="Python tutorials")
|
||||
# 2. Copilot receives search results
|
||||
# 3. Copilot summarizes top 3
|
||||
# 4. Returns final answer
|
||||
```
|
||||
|
||||
### 3. 测试错误处理
|
||||
|
||||
```python
|
||||
# User: "Call a non-existent tool"
|
||||
# Expected: 返回 "Tool not supported" error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 事件监听
|
||||
|
||||
Tool 相关事件类型:
|
||||
|
||||
- `tool_invocation_started` - Tool 调用开始
|
||||
- `tool_invocation_completed` - Tool 完成
|
||||
- `tool_invocation_failed` - Tool 失败
|
||||
- `tool_parameter_validation_failed` - 参数验证失败
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 安全性
|
||||
|
||||
- ✅ 验证 tool parameters
|
||||
- ✅ 限制执行超时
|
||||
- ✅ 不暴露详细错误信息给 LLM
|
||||
- ❌ 禁止执行危险命令(如 `rm -rf`)
|
||||
|
||||
### 2. 性能
|
||||
|
||||
- ⏱️ 设置合理的 timeout
|
||||
- 🔄 考虑异步执行长时间运行的 tool
|
||||
- 📊 记录 tool 执行时间(toolTelemetry)
|
||||
|
||||
### 3. 调试
|
||||
|
||||
- 🐛 在 DEBUG 模式下记录所有 tool 调用
|
||||
- 📝 记录 arguments 和 results
|
||||
- 🔍 使用前端 console 显示 tool 流程
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资源
|
||||
|
||||
- [GitHub Copilot SDK 官方文档](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Function API](https://docs.openwebui.com/features/plugin-system)
|
||||
- [JSON Schema 规范](https://json-schema.org/)
|
||||
|
||||
---
|
||||
|
||||
## 📝 实现清单
|
||||
|
||||
- [ ] 添加 ENABLE_TOOLS Valve
|
||||
- [ ] 实现 _convert_openwebui_tools_to_copilot()
|
||||
- [ ] 实现 _create_tool_handler()
|
||||
- [ ] 修改 SessionConfig 传递 tools
|
||||
- [ ] 处理 tool 事件流
|
||||
- [ ] 添加调试日志
|
||||
- [ ] 测试基础 tool 调用
|
||||
- [ ] 测试错误处理
|
||||
- [ ] 更新文档和 README
|
||||
- [ ] 同步中文版本
|
||||
|
||||
---
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** v1.0
|
||||
**日期:** 2026-01-26
|
||||
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW.md
Normal file
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW.md
Normal file
@@ -0,0 +1,835 @@
|
||||
# GitHub Copilot SDK Integration Workflow
|
||||
|
||||
**Author:** Fu-Jie
|
||||
**Version:** 0.2.3
|
||||
**Last Updated:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [Request Processing Flow](#request-processing-flow)
|
||||
3. [Session Management](#session-management)
|
||||
4. [Streaming Response Handling](#streaming-response-handling)
|
||||
5. [Event Processing Mechanism](#event-processing-mechanism)
|
||||
6. [Tool Execution Flow](#tool-execution-flow)
|
||||
7. [System Prompt Extraction](#system-prompt-extraction)
|
||||
8. [Configuration Parameters](#configuration-parameters)
|
||||
9. [Key Functions Reference](#key-functions-reference)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Component Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe Interface (Entry Point) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (Main Logic) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. Environment Setup (_setup_env) │ │ │
|
||||
│ │ │ 2. Model Selection (request_model parsing) │ │ │
|
||||
│ │ │ 3. Chat Context Extraction │ │ │
|
||||
│ │ │ 4. System Prompt Extraction │ │ │
|
||||
│ │ │ 5. Session Management (create/resume) │ │ │
|
||||
│ │ │ 6. Streaming/Non-streaming Response │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot Client │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK instance) │ │ │
|
||||
│ │ │ • Session (conversation context) │ │ │
|
||||
│ │ │ • Event Stream (async events) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI Process │
|
||||
│ (Backend Agent) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Pipe Interface**: OpenWebUI's standard entry point
|
||||
2. **Environment Manager**: CLI setup, token validation, environment variables
|
||||
3. **Session Manager**: Persistent conversation state with automatic compaction
|
||||
4. **Event Processor**: Asynchronous streaming event handler
|
||||
5. **Tool System**: Custom tool registration and execution
|
||||
6. **Debug Logger**: Frontend console logging for troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Request Processing Flow
|
||||
|
||||
### Complete Request Lifecycle
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI Request] --> B[pipe Entry Point]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{Setup Environment}
|
||||
D --> E[Parse Model ID]
|
||||
E --> F[Extract Chat Context]
|
||||
F --> G[Extract System Prompt]
|
||||
G --> H{Session Exists?}
|
||||
H -->|Yes| I[Resume Session]
|
||||
H -->|No| J[Create New Session]
|
||||
I --> K[Initialize Tools]
|
||||
J --> K
|
||||
K --> L[Process Images]
|
||||
L --> M{Streaming Mode?}
|
||||
M -->|Yes| N[stream_response]
|
||||
M -->|No| O[send_and_wait]
|
||||
N --> P[Async Event Stream]
|
||||
O --> Q[Direct Response]
|
||||
P --> R[Return to OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### Step-by-Step Breakdown
|
||||
|
||||
#### 1. Environment Setup (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
Priority:
|
||||
1. Check VALVES.CLI_PATH
|
||||
2. Search system PATH
|
||||
3. Auto-install via curl (if not found)
|
||||
4. Set GH_TOKEN environment variables
|
||||
"""
|
||||
```
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Locate Copilot CLI binary
|
||||
- Set `COPILOT_CLI_PATH` environment variable
|
||||
- Configure `GH_TOKEN` for authentication
|
||||
- Apply custom environment variables
|
||||
|
||||
#### 2. Model Selection
|
||||
|
||||
```python
|
||||
# Input: body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. Chat Context Extraction (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# Priority order for chat_id:
|
||||
# 1. __metadata__ (most reliable)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. System Prompt Extraction (`_extract_system_prompt`)
|
||||
|
||||
Multi-source fallback strategy:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. Model database lookup (by model_id)
|
||||
3. `body.params.system`
|
||||
4. Messages with `role="system"`
|
||||
|
||||
#### 5. Session Creation/Resumption
|
||||
|
||||
**New Session:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**Resume Session:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# Session state preserved: history, tools, workspace
|
||||
except Exception:
|
||||
# Fallback to creating new session
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management
|
||||
|
||||
### Infinite Sessions Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Session Lifecycle │
|
||||
│ │
|
||||
│ ┌──────────┐ create ┌──────────┐ resume ┌───────┴───┐
|
||||
│ │ Chat ID │─────────▶ │ Session │ ◀────────│ OpenWebUI │
|
||||
│ └──────────┘ │ State │ └───────────┘
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Context Window Management │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Messages [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token Usage: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Threshold Reached (0.8) │ │ │
|
||||
│ │ │ → Background Compaction Triggered │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Compacted Summary + Recent Messages │ │ │
|
||||
│ │ │ Token Usage: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # Enable infinite sessions
|
||||
background_compaction_threshold=0.8, # Start compaction at 80% token usage
|
||||
buffer_exhaustion_threshold=0.95 # Emergency threshold at 95%
|
||||
)
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **< 80%**: Normal operation, no compaction
|
||||
- **80-95%**: Background compaction (summarize older messages)
|
||||
- **> 95%**: Force compaction before next request
|
||||
|
||||
---
|
||||
|
||||
## Streaming Response Handling
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
Asynchronous event processing with queue-based buffering.
|
||||
|
||||
Flow:
|
||||
1. Start async send task
|
||||
2. Register event handler
|
||||
3. Process events via queue
|
||||
4. Yield chunks to OpenWebUI
|
||||
5. Clean up resources
|
||||
"""
|
||||
```
|
||||
|
||||
### Event Processing Pipeline
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK Event Stream │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Event Handler │
|
||||
│ (Sync Callback) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Async Queue │
|
||||
│ (Thread-safe) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Consumer Loop │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield to OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### State Management During Streaming
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> tags opened
|
||||
"content_sent": False # Main content has started
|
||||
}
|
||||
active_tools = {} # Track concurrent tool executions
|
||||
```
|
||||
|
||||
**State Transitions:**
|
||||
|
||||
1. `reasoning_delta` arrives → `thinking_started = True` → Output: `<think>\n{reasoning}`
|
||||
2. `message_delta` arrives → Close `</think>` if open → `content_sent = True` → Output: `{content}`
|
||||
3. `tool.execution_start` → Output tool indicator (inside/outside `<think>`)
|
||||
4. `session.complete` → Finalize stream
|
||||
|
||||
---
|
||||
|
||||
## Event Processing Mechanism
|
||||
|
||||
### Event Type Reference
|
||||
|
||||
Following official SDK patterns (from `copilot.SessionEventType`):
|
||||
|
||||
| Event Type | Description | Key Data Fields | Handler Action |
|
||||
|-----------|-------------|-----------------|----------------|
|
||||
| `assistant.message_delta` | Main content streaming | `delta_content` | Yield text chunk |
|
||||
| `assistant.reasoning_delta` | Chain-of-thought | `delta_content` | Wrap in `<think>` tags |
|
||||
| `tool.execution_start` | Tool call initiated | `name`, `tool_call_id` | Display tool indicator |
|
||||
| `tool.execution_complete` | Tool finished | `result.content` | Show completion status |
|
||||
| `session.compaction_start` | Context compaction begins | - | Log debug info |
|
||||
| `session.compaction_complete` | Compaction done | - | Log debug info |
|
||||
| `session.error` | Error occurred | `error`, `message` | Emit error notification |
|
||||
|
||||
### Event Handler Implementation
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""Process streaming events following official SDK patterns."""
|
||||
event_type = get_event_type(event) # Handle enum/string types
|
||||
|
||||
# Extract data using safe_get_data_attr (handles dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # Thread-safe enqueue
|
||||
```
|
||||
|
||||
### Official SDK Pattern Compliance
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
Official pattern: event.data.delta_content
|
||||
Handles both dict and object access patterns.
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict access (JSON-like)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# Object attribute (Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution Flow
|
||||
|
||||
### Tool Registration
|
||||
|
||||
```python
|
||||
# 1. Define tool at module level
|
||||
@define_tool(description="Generate a random integer within a specified range.")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"Generated random number: {number}"
|
||||
|
||||
# 2. Register in _initialize_custom_tools
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# Filter based on AVAILABLE_TOOLS valve
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
```
|
||||
|
||||
### Tool Execution Timeline
|
||||
|
||||
```
|
||||
User Message: "Generate a random number between 1 and 100"
|
||||
│
|
||||
▼
|
||||
Model Decision: Use tool `generate_random_number`
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_start
|
||||
│ → Display: "🔧 Running Tool: generate_random_number"
|
||||
▼
|
||||
Tool Function Execution (async)
|
||||
│
|
||||
▼
|
||||
Event: tool.execution_complete
|
||||
│ → Result: "Generated random number: 42"
|
||||
│ → Display: "✅ Tool Completed: 42"
|
||||
▼
|
||||
Model generates response using tool result
|
||||
│
|
||||
▼
|
||||
Event: assistant.message_delta
|
||||
│ → "I generated the number 42 for you."
|
||||
▼
|
||||
Stream Complete
|
||||
```
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
**Before Content:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
Running Tool: generate_random_number...
|
||||
Tool `generate_random_number` Completed. Result: 42
|
||||
</think>
|
||||
|
||||
I generated the number 42 for you.
|
||||
```
|
||||
|
||||
**After Content Started:**
|
||||
|
||||
```markdown
|
||||
The number is
|
||||
|
||||
> 🔧 **Running Tool**: `generate_random_number`
|
||||
|
||||
> ✅ **Tool Completed**: 42
|
||||
|
||||
actually 42.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Prompt Extraction
|
||||
|
||||
### Multi-Source Priority System
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
Priority order:
|
||||
1. metadata.model.params.system (highest)
|
||||
2. Model database lookup
|
||||
3. body.params.system
|
||||
4. messages[role="system"] (fallback)
|
||||
"""
|
||||
```
|
||||
|
||||
### Source 1: Metadata Model Params
|
||||
|
||||
```python
|
||||
# OpenWebUI injects model configuration
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # Priority 1
|
||||
```
|
||||
|
||||
### Source 2: Model Database
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# Try multiple model ID variations
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # From valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### Source 3: Body Params
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### Source 4: System Message
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### Configuration in SessionConfig
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # Append to conversation context
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... other params
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### Valve Definitions
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub Fine-grained Token (requires 'Copilot Requests' permission) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | Default model when dynamic fetching fails |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Path to Copilot CLI binary |
|
||||
| `DEBUG` | bool | `False` | Enable frontend console debug logging |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI log level: none, error, warning, info, debug, all |
|
||||
| `SHOW_THINKING` | bool | `True` | Display model reasoning in `<think>` tags |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | Show session workspace path in debug mode |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | Comma-separated keywords to exclude models |
|
||||
| `WORKSPACE_DIR` | str | `""` | Restricted workspace directory (empty = process cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | Enable automatic context compaction |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | Background compaction at 80% token usage |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | Emergency threshold at 95% |
|
||||
| `TIMEOUT` | int | `300` | Stream chunk timeout (seconds) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | JSON string of custom environment variables |
|
||||
| `ENABLE_TOOLS` | bool | `False` | Enable custom tool system |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | Available tools: "all" or comma-separated list |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Set by _setup_env
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Custom variables (from CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Functions Reference
|
||||
|
||||
### Entry Points
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: OpenWebUI stable entry point
|
||||
- **Returns**: Delegates to `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **Purpose**: Main request processing logic
|
||||
- **Flow**: Setup → Extract → Session → Response
|
||||
- **Returns**: `str` (non-streaming) or `AsyncGenerator` (streaming)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **Purpose**: Dynamic model list fetching
|
||||
- **Returns**: List of available models with multiplier info
|
||||
- **Caching**: Uses `_model_cache` to avoid repeated API calls
|
||||
|
||||
### Session Management
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **Purpose**: Construct SessionConfig object
|
||||
- **Returns**: `SessionConfig` with infinite sessions and tools
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract chat_id with priority fallback
|
||||
- **Returns**: `{"chat_id": str}`
|
||||
|
||||
### Streaming
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **Purpose**: Async streaming event processor
|
||||
- **Yields**: Text chunks to OpenWebUI
|
||||
- **Resources**: Auto-cleanup client and session
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **Purpose**: Sync event callback (inside `stream_response`)
|
||||
- **Action**: Parse event → Enqueue chunks → Update state
|
||||
|
||||
### Helpers
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **Purpose**: Send debug logs to frontend console
|
||||
- **Condition**: Only when `DEBUG=True`
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **Purpose**: Locate CLI, set environment variables
|
||||
- **Side Effects**: Modifies `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **Purpose**: Multi-source system prompt extraction
|
||||
- **Returns**: `(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **Purpose**: Extract text and images from multimodal messages
|
||||
- **Returns**: `(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **Purpose**: Register and filter custom tools
|
||||
- **Returns**: List of tool functions
|
||||
|
||||
### Utility Functions
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **Purpose**: Extract event type string from enum/string
|
||||
- **Handles**: `SessionEventType` enum → `.value` extraction
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **Purpose**: Safe attribute extraction from event.data
|
||||
- **Handles**: Both dict access and object attribute access
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```python
|
||||
# In OpenWebUI Valves UI:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### Debug Output Location
|
||||
|
||||
**Frontend Console:**
|
||||
|
||||
```javascript
|
||||
// Open browser DevTools (F12)
|
||||
// Look for logs with prefix: [Copilot Pipe]
|
||||
console.debug("[Copilot Pipe] Extracted ChatID: abc123 (Source: __metadata__)")
|
||||
```
|
||||
|
||||
**Backend Logs:**
|
||||
|
||||
```python
|
||||
# Python logging output
|
||||
logger.debug(f"[Copilot Pipe] Session resumed: {chat_id}")
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Session Not Resuming
|
||||
|
||||
**Symptom:** New session created every request
|
||||
**Causes:**
|
||||
|
||||
- `chat_id` not extracted correctly
|
||||
- Session expired on Copilot side
|
||||
- `INFINITE_SESSION=False` (sessions not persistent)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted ChatID: <id> (Source: ...)"
|
||||
"Session <id> not found (...), creating new."
|
||||
```
|
||||
|
||||
#### 2. System Prompt Not Applied
|
||||
|
||||
**Symptom:** Model ignores configured system prompt
|
||||
**Causes:**
|
||||
|
||||
- Not found in any of 4 sources
|
||||
- Session resumed (system prompt only set on creation)
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Extracted system prompt from <source> (length: X)"
|
||||
"Configured system message (mode: append)"
|
||||
```
|
||||
|
||||
#### 3. Tools Not Available
|
||||
|
||||
**Symptom:** Model can't use custom tools
|
||||
**Causes:**
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- Tool not registered in `_initialize_custom_tools`
|
||||
- Wrong `AVAILABLE_TOOLS` filter
|
||||
|
||||
**Solution:**
|
||||
|
||||
```python
|
||||
# Check debug logs for:
|
||||
"Enabled X custom tools: ['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Model List Caching
|
||||
|
||||
```python
|
||||
# First request: Fetch from API
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # Cache result
|
||||
|
||||
# Subsequent requests: Use cache
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### Session Persistence
|
||||
|
||||
**Impact:** Eliminates redundant model initialization on every request
|
||||
|
||||
```python
|
||||
# Without session:
|
||||
# Each request: Initialize model → Load context → Generate → Discard
|
||||
|
||||
# With session (chat_id):
|
||||
# First request: Initialize model → Load context → Generate → Save
|
||||
# Later: Resume → Generate (instant)
|
||||
```
|
||||
|
||||
### Streaming vs Non-streaming
|
||||
|
||||
**Streaming:**
|
||||
|
||||
- Lower perceived latency (first token faster)
|
||||
- Better UX for long responses
|
||||
- Resource cleanup via generator exit
|
||||
|
||||
**Non-streaming:**
|
||||
|
||||
- Simpler error handling
|
||||
- Atomic response (no partial output)
|
||||
- Use for short responses
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Protection
|
||||
|
||||
```python
|
||||
# ❌ Never log tokens
|
||||
logger.debug(f"Token: {self.valves.GH_TOKEN}") # DON'T DO THIS
|
||||
|
||||
# ✅ Mask sensitive data
|
||||
logger.debug(f"Token configured: {'*' * 10}")
|
||||
```
|
||||
|
||||
### Workspace Isolation
|
||||
|
||||
```python
|
||||
# Set WORKSPACE_DIR to restrict file access
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI respects this directory
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```python
|
||||
# Validate chat_id format
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"Invalid chat_id format: {chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **Multi-Session Management**: Support multiple parallel sessions per user
|
||||
2. **Session Analytics**: Track token usage, compaction frequency
|
||||
3. **Tool Result Caching**: Avoid redundant tool calls
|
||||
4. **Custom Event Filters**: User-configurable event handling
|
||||
5. **Workspace Templates**: Pre-configured workspace environments
|
||||
6. **Streaming Abort**: Graceful cancellation of long-running requests
|
||||
|
||||
### API Evolution
|
||||
|
||||
Monitoring Copilot SDK updates for:
|
||||
|
||||
- New event types (e.g., `assistant.function_call`)
|
||||
- Enhanced tool capabilities
|
||||
- Improved session serialization
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe Development](https://docs.openwebui.com/)
|
||||
- [Awesome OpenWebUI Project](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
|
||||
---
|
||||
|
||||
**License:** MIT
|
||||
**Maintainer:** Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md
Normal file
835
plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md
Normal file
@@ -0,0 +1,835 @@
|
||||
# GitHub Copilot SDK 集成工作流程
|
||||
|
||||
**作者:** Fu-Jie
|
||||
**版本:** 0.2.3
|
||||
**最后更新:** 2026-01-27
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [架构概览](#架构概览)
|
||||
2. [请求处理流程](#请求处理流程)
|
||||
3. [会话管理](#会话管理)
|
||||
4. [流式响应处理](#流式响应处理)
|
||||
5. [事件处理机制](#事件处理机制)
|
||||
6. [工具执行流程](#工具执行流程)
|
||||
7. [系统提示词提取](#系统提示词提取)
|
||||
8. [配置参数](#配置参数)
|
||||
9. [核心函数参考](#核心函数参考)
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
### 组件图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OpenWebUI │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Pipe 接口 (入口点) │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ _pipe_impl (主逻辑) │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ 1. 环境设置 (_setup_env) │ │ │
|
||||
│ │ │ 2. 模型选择 (request_model 解析) │ │ │
|
||||
│ │ │ 3. 聊天上下文提取 │ │ │
|
||||
│ │ │ 4. 系统提示词提取 │ │ │
|
||||
│ │ │ 5. 会话管理 (创建/恢复) │ │ │
|
||||
│ │ │ 6. 流式/非流式响应 │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ GitHub Copilot 客户端 │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ • CopilotClient (SDK 实例) │ │ │
|
||||
│ │ │ • Session (对话上下文) │ │ │
|
||||
│ │ │ • Event Stream (异步事件流) │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────┼─────────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Copilot CLI 进程 │
|
||||
│ (后端代理) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **Pipe 接口**:OpenWebUI 的标准入口点
|
||||
2. **环境管理器**:CLI 设置、令牌验证、环境变量
|
||||
3. **会话管理器**:持久化对话状态,自动压缩
|
||||
4. **事件处理器**:异步流式事件处理器
|
||||
5. **工具系统**:自定义工具注册和执行
|
||||
6. **调试日志器**:前端控制台日志,用于故障排除
|
||||
|
||||
---
|
||||
|
||||
## 请求处理流程
|
||||
|
||||
### 完整请求生命周期
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[OpenWebUI 请求] --> B[pipe 入口点]
|
||||
B --> C[_pipe_impl]
|
||||
C --> D{设置环境}
|
||||
D --> E[解析模型 ID]
|
||||
E --> F[提取聊天上下文]
|
||||
F --> G[提取系统提示词]
|
||||
G --> H{会话存在?}
|
||||
H -->|是| I[恢复会话]
|
||||
H -->|否| J[创建新会话]
|
||||
I --> K[初始化工具]
|
||||
J --> K
|
||||
K --> L[处理图片]
|
||||
L --> M{流式模式?}
|
||||
M -->|是| N[stream_response]
|
||||
M -->|否| O[send_and_wait]
|
||||
N --> P[异步事件流]
|
||||
O --> Q[直接响应]
|
||||
P --> R[返回到 OpenWebUI]
|
||||
Q --> R
|
||||
```
|
||||
|
||||
### 逐步分解
|
||||
|
||||
#### 1. 环境设置 (`_setup_env`)
|
||||
|
||||
```python
|
||||
def _setup_env(self, __event_call__=None):
|
||||
"""
|
||||
优先级:
|
||||
1. 检查 VALVES.CLI_PATH
|
||||
2. 搜索系统 PATH
|
||||
3. 自动通过 curl 安装(如果未找到)
|
||||
4. 设置 GH_TOKEN 环境变量
|
||||
"""
|
||||
```
|
||||
|
||||
**操作:**
|
||||
|
||||
- 定位 Copilot CLI 二进制文件
|
||||
- 设置 `COPILOT_CLI_PATH` 环境变量
|
||||
- 配置 `GH_TOKEN` 进行身份验证
|
||||
- 应用自定义环境变量
|
||||
|
||||
#### 2. 模型选择
|
||||
|
||||
```python
|
||||
# 输入:body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||
request_model = body.get("model", "")
|
||||
if request_model.startswith(f"{self.id}-"):
|
||||
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||
```
|
||||
|
||||
#### 3. 聊天上下文提取 (`_get_chat_context`)
|
||||
|
||||
```python
|
||||
# chat_id 的优先级顺序:
|
||||
# 1. __metadata__(最可靠)
|
||||
# 2. body["chat_id"]
|
||||
# 3. body["metadata"]["chat_id"]
|
||||
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||
chat_id = chat_ctx.get("chat_id")
|
||||
```
|
||||
|
||||
#### 4. 系统提示词提取 (`_extract_system_prompt`)
|
||||
|
||||
多源回退策略:
|
||||
|
||||
1. `metadata.model.params.system`
|
||||
2. 模型数据库查询(按 model_id)
|
||||
3. `body.params.system`
|
||||
4. 包含 `role="system"` 的消息
|
||||
|
||||
#### 5. 会话创建/恢复
|
||||
|
||||
**新会话:**
|
||||
|
||||
```python
|
||||
session_config = SessionConfig(
|
||||
session_id=chat_id,
|
||||
model=real_model_id,
|
||||
streaming=is_streaming,
|
||||
tools=custom_tools,
|
||||
system_message={"mode": "append", "content": system_prompt_content},
|
||||
infinite_sessions=InfiniteSessionConfig(
|
||||
enabled=True,
|
||||
background_compaction_threshold=0.8,
|
||||
buffer_exhaustion_threshold=0.95
|
||||
)
|
||||
)
|
||||
session = await client.create_session(config=session_config)
|
||||
```
|
||||
|
||||
**恢复会话:**
|
||||
|
||||
```python
|
||||
try:
|
||||
session = await client.resume_session(chat_id)
|
||||
# 会话状态保留:历史、工具、工作区
|
||||
except Exception:
|
||||
# 回退到创建新会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 会话管理
|
||||
|
||||
### 无限会话架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 会话生命周期 │
|
||||
│ │
|
||||
│ ┌──────────┐ 创建 ┌──────────┐ 恢复 ┌───────────┐ │
|
||||
│ │ Chat ID │─────▶ │ Session │ ◀────────│ OpenWebUI │ │
|
||||
│ └──────────┘ │ State │ └───────────┘ │
|
||||
│ └─────┬────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ 上下文窗口管理 │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 消息 [user, assistant, tool_results...] │ │ │
|
||||
│ │ │ Token 使用率: ████████████░░░░ (80%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 达到阈值 (0.8) │ │ │
|
||||
│ │ │ → 后台压缩触发 │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||
│ │ │ 压缩摘要 + 最近消息 │ │ │
|
||||
│ │ │ Token 使用率: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||
│ │ └──────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 配置参数
|
||||
|
||||
```python
|
||||
InfiniteSessionConfig(
|
||||
enabled=True, # 启用无限会话
|
||||
background_compaction_threshold=0.8, # 在 80% token 使用率时开始压缩
|
||||
buffer_exhaustion_threshold=0.95 # 95% 紧急阈值
|
||||
)
|
||||
```
|
||||
|
||||
**行为:**
|
||||
|
||||
- **< 80%**:正常操作,无压缩
|
||||
- **80-95%**:后台压缩(总结旧消息)
|
||||
- **> 95%**:在下一个请求前强制压缩
|
||||
|
||||
---
|
||||
|
||||
## 流式响应处理
|
||||
|
||||
### 事件驱动架构
|
||||
|
||||
```python
|
||||
async def stream_response(
|
||||
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||
) -> AsyncGenerator:
|
||||
"""
|
||||
使用基于队列的缓冲进行异步事件处理。
|
||||
|
||||
流程:
|
||||
1. 启动异步发送任务
|
||||
2. 注册事件处理器
|
||||
3. 通过队列处理事件
|
||||
4. 向 OpenWebUI 产出块
|
||||
5. 清理资源
|
||||
"""
|
||||
```
|
||||
|
||||
### 事件处理管道
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Copilot SDK 事件流 │
|
||||
└────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 事件处理器 │
|
||||
│ (同步回调) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 异步队列 │
|
||||
│ (线程安全) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 消费者循环 │
|
||||
│ (async for) │
|
||||
└────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ yield 到 OpenWebUI │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### 流式传输期间的状态管理
|
||||
|
||||
```python
|
||||
state = {
|
||||
"thinking_started": False, # <think> 标签已打开
|
||||
"content_sent": False # 主内容已开始
|
||||
}
|
||||
active_tools = {} # 跟踪并发工具执行
|
||||
```
|
||||
|
||||
**状态转换:**
|
||||
|
||||
1. `reasoning_delta` 到达 → `thinking_started = True` → 输出:`<think>\n{reasoning}`
|
||||
2. `message_delta` 到达 → 如果打开则关闭 `</think>` → `content_sent = True` → 输出:`{content}`
|
||||
3. `tool.execution_start` → 输出工具指示器(在 `<think>` 内部/外部)
|
||||
4. `session.complete` → 完成流
|
||||
|
||||
---
|
||||
|
||||
## 事件处理机制
|
||||
|
||||
### 事件类型参考
|
||||
|
||||
遵循官方 SDK 模式(来自 `copilot.SessionEventType`):
|
||||
|
||||
| 事件类型 | 描述 | 关键数据字段 | 处理器操作 |
|
||||
|---------|------|-------------|-----------|
|
||||
| `assistant.message_delta` | 主内容流式传输 | `delta_content` | 产出文本块 |
|
||||
| `assistant.reasoning_delta` | 思维链 | `delta_content` | 用 `<think>` 标签包装 |
|
||||
| `tool.execution_start` | 工具调用启动 | `name`, `tool_call_id` | 显示工具指示器 |
|
||||
| `tool.execution_complete` | 工具完成 | `result.content` | 显示完成状态 |
|
||||
| `session.compaction_start` | 上下文压缩开始 | - | 记录调试信息 |
|
||||
| `session.compaction_complete` | 压缩完成 | - | 记录调试信息 |
|
||||
| `session.error` | 发生错误 | `error`, `message` | 发出错误通知 |
|
||||
|
||||
### 事件处理器实现
|
||||
|
||||
```python
|
||||
def handler(event):
|
||||
"""遵循官方 SDK 模式处理流式事件。"""
|
||||
event_type = get_event_type(event) # 处理枚举/字符串类型
|
||||
|
||||
# 使用 safe_get_data_attr 提取数据(处理 dict/object)
|
||||
if event_type == "assistant.message_delta":
|
||||
delta = safe_get_data_attr(event, "delta_content")
|
||||
if delta:
|
||||
queue.put_nowait(delta) # 线程安全入队
|
||||
```
|
||||
|
||||
### 官方 SDK 模式合规性
|
||||
|
||||
```python
|
||||
def safe_get_data_attr(event, attr: str, default=None):
|
||||
"""
|
||||
官方模式:event.data.delta_content
|
||||
处理 dict 和对象访问模式。
|
||||
"""
|
||||
if not hasattr(event, "data") or event.data is None:
|
||||
return default
|
||||
|
||||
data = event.data
|
||||
|
||||
# Dict 访问(类似 JSON)
|
||||
if isinstance(data, dict):
|
||||
return data.get(attr, default)
|
||||
|
||||
# 对象属性(Python SDK)
|
||||
return getattr(data, attr, default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具执行流程
|
||||
|
||||
### 工具注册
|
||||
|
||||
```python
|
||||
# 1. 在模块级别定义工具
|
||||
@define_tool(description="在指定范围内生成随机整数。")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
number = random.randint(params.min, params.max)
|
||||
return f"生成的随机数: {number}"
|
||||
|
||||
# 2. 在 _initialize_custom_tools 中注册
|
||||
def _initialize_custom_tools(self):
|
||||
if not self.valves.ENABLE_TOOLS:
|
||||
return []
|
||||
|
||||
all_tools = {
|
||||
"generate_random_number": generate_random_number,
|
||||
}
|
||||
|
||||
# 根据 AVAILABLE_TOOLS valve 过滤
|
||||
if self.valves.AVAILABLE_TOOLS == "all":
|
||||
return list(all_tools.values())
|
||||
|
||||
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||
return [all_tools[name] for name in enabled if name in all_tools]
|
||||
```
|
||||
|
||||
### 工具执行时间线
|
||||
|
||||
```
|
||||
用户消息:生成一个 1 到 100 之间的随机数
|
||||
│
|
||||
▼
|
||||
模型决策:使用工具 `generate_random_number`
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_start
|
||||
│ → 显示:"🔧 运行工具:generate_random_number"
|
||||
▼
|
||||
工具函数执行(异步)
|
||||
│
|
||||
▼
|
||||
事件:tool.execution_complete
|
||||
│ → 结果:"生成的随机数:42"
|
||||
│ → 显示:"✅ 工具完成:42"
|
||||
▼
|
||||
模型使用工具结果生成响应
|
||||
│
|
||||
▼
|
||||
事件:assistant.message_delta
|
||||
│ → "我为你生成了数字 42。"
|
||||
▼
|
||||
流完成
|
||||
```
|
||||
|
||||
### 视觉指示器
|
||||
|
||||
**内容前:**
|
||||
|
||||
```markdown
|
||||
<think>
|
||||
运行工具:generate_random_number...
|
||||
工具 `generate_random_number` 完成。结果:42
|
||||
</think>
|
||||
|
||||
我为你生成了数字 42。
|
||||
```
|
||||
|
||||
**内容开始后:**
|
||||
|
||||
```markdown
|
||||
数字是
|
||||
|
||||
> 🔧 **运行工具**:`generate_random_number`
|
||||
|
||||
> ✅ **工具完成**:42
|
||||
|
||||
实际上是 42。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 系统提示词提取
|
||||
|
||||
### 多源优先级系统
|
||||
|
||||
```python
|
||||
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||
"""
|
||||
优先级顺序:
|
||||
1. metadata.model.params.system(最高)
|
||||
2. 模型数据库查询
|
||||
3. body.params.system
|
||||
4. messages[role="system"](回退)
|
||||
"""
|
||||
```
|
||||
|
||||
### 来源 1:元数据模型参数
|
||||
|
||||
```python
|
||||
# OpenWebUI 注入模型配置
|
||||
metadata = body.get("metadata", {})
|
||||
meta_model = metadata.get("model", {})
|
||||
meta_params = meta_model.get("params", {})
|
||||
system_prompt = meta_params.get("system") # 优先级 1
|
||||
```
|
||||
|
||||
### 来源 2:模型数据库
|
||||
|
||||
```python
|
||||
from open_webui.models.models import Models
|
||||
|
||||
# 尝试多个模型 ID 变体
|
||||
model_ids_to_try = [
|
||||
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||
real_model_id, # 来自 valves
|
||||
]
|
||||
|
||||
for mid in model_ids_to_try:
|
||||
model_record = Models.get_model_by_id(mid)
|
||||
if model_record and hasattr(model_record, "params"):
|
||||
system_prompt = model_record.params.get("system")
|
||||
if system_prompt:
|
||||
break
|
||||
```
|
||||
|
||||
### 来源 3:Body 参数
|
||||
|
||||
```python
|
||||
body_params = body.get("params", {})
|
||||
system_prompt = body_params.get("system")
|
||||
```
|
||||
|
||||
### 来源 4:系统消息
|
||||
|
||||
```python
|
||||
for msg in messages:
|
||||
if msg.get("role") == "system":
|
||||
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||
break
|
||||
```
|
||||
|
||||
### SessionConfig 中的配置
|
||||
|
||||
```python
|
||||
system_message_config = {
|
||||
"mode": "append", # 追加到对话上下文
|
||||
"content": system_prompt_content
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
system_message=system_message_config,
|
||||
# ... 其他参数
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置参数
|
||||
|
||||
### Valve 定义
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|--------|------|
|
||||
| `GH_TOKEN` | str | `""` | GitHub 精细化令牌(需要 'Copilot Requests' 权限) |
|
||||
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | 动态获取失败时的默认模型 |
|
||||
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Copilot CLI 二进制文件路径 |
|
||||
| `DEBUG` | bool | `False` | 启用前端控制台调试日志 |
|
||||
| `LOG_LEVEL` | str | `"error"` | CLI 日志级别:none、error、warning、info、debug、all |
|
||||
| `SHOW_THINKING` | bool | `True` | 在 `<think>` 标签中显示模型推理 |
|
||||
| `SHOW_WORKSPACE_INFO` | bool | `True` | 在调试模式下显示会话工作区路径 |
|
||||
| `EXCLUDE_KEYWORDS` | str | `""` | 逗号分隔的关键字,用于排除模型 |
|
||||
| `WORKSPACE_DIR` | str | `""` | 限制的工作区目录(空 = 进程 cwd) |
|
||||
| `INFINITE_SESSION` | bool | `True` | 启用自动上下文压缩 |
|
||||
| `COMPACTION_THRESHOLD` | float | `0.8` | 80% token 使用率时后台压缩 |
|
||||
| `BUFFER_THRESHOLD` | float | `0.95` | 95% 紧急阈值 |
|
||||
| `TIMEOUT` | int | `300` | 流块超时(秒) |
|
||||
| `CUSTOM_ENV_VARS` | str | `""` | 自定义环境变量的 JSON 字符串 |
|
||||
| `ENABLE_TOOLS` | bool | `False` | 启用自定义工具系统 |
|
||||
| `AVAILABLE_TOOLS` | str | `"all"` | 可用工具:"all" 或逗号分隔列表 |
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# 由 _setup_env 设置
|
||||
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# 自定义变量(来自 CUSTOM_ENV_VARS valve)
|
||||
export CUSTOM_VAR_1="value1"
|
||||
export CUSTOM_VAR_2="value2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心函数参考
|
||||
|
||||
### 入口点
|
||||
|
||||
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:OpenWebUI 稳定入口点
|
||||
- **返回**:委托给 `_pipe_impl`
|
||||
|
||||
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||
|
||||
- **目的**:主请求处理逻辑
|
||||
- **流程**:设置 → 提取 → 会话 → 响应
|
||||
- **返回**:`str`(非流式)或 `AsyncGenerator`(流式)
|
||||
|
||||
#### `pipes()`
|
||||
|
||||
- **目的**:动态模型列表获取
|
||||
- **返回**:带有倍数信息的可用模型列表
|
||||
- **缓存**:使用 `_model_cache` 避免重复 API 调用
|
||||
|
||||
### 会话管理
|
||||
|
||||
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||
|
||||
- **目的**:构建 SessionConfig 对象
|
||||
- **返回**:带有无限会话和工具的 `SessionConfig`
|
||||
|
||||
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||
|
||||
- **目的**:使用优先级回退提取 chat_id
|
||||
- **返回**:`{"chat_id": str}`
|
||||
|
||||
### 流式传输
|
||||
|
||||
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||
|
||||
- **目的**:异步流式事件处理器
|
||||
- **产出**:文本块到 OpenWebUI
|
||||
- **资源**:自动清理客户端和会话
|
||||
|
||||
#### `handler(event)`
|
||||
|
||||
- **目的**:同步事件回调(在 `stream_response` 内)
|
||||
- **操作**:解析事件 → 入队块 → 更新状态
|
||||
|
||||
### 辅助函数
|
||||
|
||||
#### `_emit_debug_log(message, __event_call__)`
|
||||
|
||||
- **目的**:将调试日志发送到前端控制台
|
||||
- **条件**:仅当 `DEBUG=True` 时
|
||||
|
||||
#### `_setup_env(__event_call__)`
|
||||
|
||||
- **目的**:定位 CLI,设置环境变量
|
||||
- **副作用**:修改 `os.environ`
|
||||
|
||||
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||
|
||||
- **目的**:多源系统提示词提取
|
||||
- **返回**:`(system_prompt_content, source_name)`
|
||||
|
||||
#### `_process_images(messages, __event_call__)`
|
||||
|
||||
- **目的**:从多模态消息中提取文本和图片
|
||||
- **返回**:`(text_content, attachments_list)`
|
||||
|
||||
#### `_initialize_custom_tools()`
|
||||
|
||||
- **目的**:注册和过滤自定义工具
|
||||
- **返回**:工具函数列表
|
||||
|
||||
### 实用函数
|
||||
|
||||
#### `get_event_type(event) -> str`
|
||||
|
||||
- **目的**:从枚举/字符串提取事件类型字符串
|
||||
- **处理**:`SessionEventType` 枚举 → `.value` 提取
|
||||
|
||||
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||
|
||||
- **目的**:从 event.data 安全提取属性
|
||||
- **处理**:dict 访问和对象属性访问
|
||||
|
||||
---
|
||||
|
||||
## 故障排除指南
|
||||
|
||||
### 启用调试模式
|
||||
|
||||
```python
|
||||
# 在 OpenWebUI Valves UI 中:
|
||||
DEBUG = True
|
||||
SHOW_WORKSPACE_INFO = True
|
||||
LOG_LEVEL = "debug"
|
||||
```
|
||||
|
||||
### 调试输出位置
|
||||
|
||||
**前端控制台:**
|
||||
|
||||
```javascript
|
||||
// 打开浏览器开发工具 (F12)
|
||||
// 查找前缀为 [Copilot Pipe] 的日志
|
||||
console.debug("[Copilot Pipe] 提取的 ChatID:abc123(来源:__metadata__)")
|
||||
```
|
||||
|
||||
**后端日志:**
|
||||
|
||||
```python
|
||||
# Python 日志输出
|
||||
logger.debug(f"[Copilot Pipe] 会话已恢复:{chat_id}")
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 会话未恢复
|
||||
|
||||
**症状**:每次请求都创建新会话
|
||||
**原因**:
|
||||
|
||||
- `chat_id` 提取不正确
|
||||
- Copilot 端会话过期
|
||||
- `INFINITE_SESSION=False`(会话不持久)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"提取的 ChatID:<id>(来源:...)"
|
||||
"会话 <id> 未找到(...),正在创建新会话。"
|
||||
```
|
||||
|
||||
#### 2. 系统提示词未应用
|
||||
|
||||
**症状**:模型忽略配置的系统提示词
|
||||
**原因**:
|
||||
|
||||
- 在 4 个来源中均未找到
|
||||
- 会话已恢复(系统提示词仅在创建时设置)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"从 <source> 提取系统提示词(长度:X)"
|
||||
"配置系统消息(模式:append)"
|
||||
```
|
||||
|
||||
#### 3. 工具不可用
|
||||
|
||||
**症状**:模型无法使用自定义工具
|
||||
**原因**:
|
||||
|
||||
- `ENABLE_TOOLS=False`
|
||||
- 工具未在 `_initialize_custom_tools` 中注册
|
||||
- 错误的 `AVAILABLE_TOOLS` 过滤器
|
||||
|
||||
**解决方案**:
|
||||
|
||||
```python
|
||||
# 检查调试日志中的:
|
||||
"已启用 X 个自定义工具:['tool1', 'tool2']"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 模型列表缓存
|
||||
|
||||
```python
|
||||
# 第一次请求:从 API 获取
|
||||
models = await client.list_models()
|
||||
self._model_cache = [...] # 缓存结果
|
||||
|
||||
# 后续请求:使用缓存
|
||||
if self._model_cache:
|
||||
return self._model_cache
|
||||
```
|
||||
|
||||
### 会话持久化
|
||||
|
||||
**影响**:消除每次请求的冗余模型初始化
|
||||
|
||||
```python
|
||||
# 没有会话:
|
||||
# 每次请求:初始化模型 → 加载上下文 → 生成 → 丢弃
|
||||
|
||||
# 有会话(chat_id):
|
||||
# 第一次请求:初始化模型 → 加载上下文 → 生成 → 保存
|
||||
# 后续:恢复 → 生成(即时)
|
||||
```
|
||||
|
||||
### 流式 vs 非流式
|
||||
|
||||
**流式:**
|
||||
|
||||
- 降低感知延迟(首个 token 更快)
|
||||
- 长响应的更好用户体验
|
||||
- 通过生成器退出进行资源清理
|
||||
|
||||
**非流式:**
|
||||
|
||||
- 更简单的错误处理
|
||||
- 原子响应(无部分输出)
|
||||
- 用于短响应
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 令牌保护
|
||||
|
||||
```python
|
||||
# ❌ 永远不要记录令牌
|
||||
logger.debug(f"令牌:{self.valves.GH_TOKEN}") # 不要这样做
|
||||
|
||||
# ✅ 屏蔽敏感数据
|
||||
logger.debug(f"令牌已配置:{'*' * 10}")
|
||||
```
|
||||
|
||||
### 工作区隔离
|
||||
|
||||
```python
|
||||
# 设置 WORKSPACE_DIR 以限制文件访问
|
||||
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||
|
||||
# Copilot CLI 遵守此目录
|
||||
client_config["cwd"] = WORKSPACE_DIR
|
||||
```
|
||||
|
||||
### 输入验证
|
||||
|
||||
```python
|
||||
# 验证 chat_id 格式
|
||||
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||
logger.warning(f"无效的 chat_id 格式:{chat_id}")
|
||||
chat_id = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未来增强
|
||||
|
||||
### 计划功能
|
||||
|
||||
1. **多会话管理**:支持每个用户的多个并行会话
|
||||
2. **会话分析**:跟踪 token 使用率、压缩频率
|
||||
3. **工具结果缓存**:避免冗余工具调用
|
||||
4. **自定义事件过滤器**:用户可配置的事件处理
|
||||
5. **工作区模板**:预配置的工作区环境
|
||||
6. **流式中止**:优雅取消长时间运行的请求
|
||||
|
||||
### API 演进
|
||||
|
||||
监控 Copilot SDK 更新:
|
||||
|
||||
- 新事件类型(例如 `assistant.function_call`)
|
||||
- 增强的工具功能
|
||||
- 改进的会话序列化
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [GitHub Copilot SDK 文档](https://github.com/github/copilot-sdk)
|
||||
- [OpenWebUI Pipe 开发](https://docs.openwebui.com/)
|
||||
- [Awesome OpenWebUI 项目](https://github.com/Fu-Jie/awesome-openwebui)
|
||||
|
||||
---
|
||||
|
||||
**许可证**:MIT
|
||||
**维护者**:Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||
124
plugins/debug/github-copilot-sdk/test_capabilities.py
Normal file
124
plugins/debug/github-copilot-sdk/test_capabilities.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from copilot import CopilotClient, define_tool
|
||||
from copilot.types import SessionConfig
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# Define a simple tool for testing
|
||||
class RandomNumberParams(BaseModel):
|
||||
min: int = Field(description="Minimum value")
|
||||
max: int = Field(description="Maximum value")
|
||||
|
||||
|
||||
@define_tool(description="Generate a random integer within a range.")
|
||||
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||
import random
|
||||
|
||||
return f"Result: {random.randint(params.min, params.max)}"
|
||||
|
||||
|
||||
async def main():
|
||||
print(f"Running tests with Python: {sys.executable}")
|
||||
|
||||
# 1. Setup Client
|
||||
client = CopilotClient({"log_level": "error"})
|
||||
await client.start()
|
||||
|
||||
try:
|
||||
print("\n=== Test 1: Session Creation & Formatting Injection ===")
|
||||
# Use gpt-4o or similar capable model
|
||||
model_id = "gpt-5-mini"
|
||||
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": "You are a test assistant. Always start your response with 'TEST_PREFIX: '.",
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
model=model_id,
|
||||
system_message=system_message_config,
|
||||
tools=[generate_random_number],
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
session_id = session.session_id
|
||||
print(f"Session Created: {session_id}")
|
||||
|
||||
# Test 1.1: Check system prompt effect
|
||||
resp = await session.send_and_wait(
|
||||
{"prompt": "Say hello.", "mode": "immediate"}
|
||||
)
|
||||
content = resp.data.content
|
||||
print(f"Response 1: {content}")
|
||||
|
||||
if "TEST_PREFIX:" in content:
|
||||
print("✅ System prompt injection active.")
|
||||
else:
|
||||
print("⚠️ System prompt injection NOT detected.")
|
||||
|
||||
print("\n=== Test 2: Tool Execution ===")
|
||||
# Test Tool Usage
|
||||
prompt_with_tool = (
|
||||
"Generate a random number between 100 and 200 using the tool."
|
||||
)
|
||||
print(f"Sending: {prompt_with_tool}")
|
||||
|
||||
# We need to listen to events to verify tool execution,
|
||||
# but send_and_wait handles it internally and returns the final answer.
|
||||
# We check if the final answer mentions the result.
|
||||
|
||||
resp_tool = await session.send_and_wait(
|
||||
{"prompt": prompt_with_tool, "mode": "immediate"}
|
||||
)
|
||||
tool_content = resp_tool.data.content
|
||||
print(f"Response 2: {tool_content}")
|
||||
|
||||
if "Result:" in tool_content or any(char.isdigit() for char in tool_content):
|
||||
print("✅ Tool likely executed (numbers found).")
|
||||
else:
|
||||
print("⚠️ Tool execution uncertain.")
|
||||
|
||||
print("\n=== Test 3: Context Retention (Memory) ===")
|
||||
# Store a fact
|
||||
await session.send_and_wait(
|
||||
{"prompt": "My secret code is 'BLUE-42'. Remember it.", "mode": "immediate"}
|
||||
)
|
||||
print("Fact sent.")
|
||||
|
||||
# Retrieve it
|
||||
resp_mem = await session.send_and_wait(
|
||||
{"prompt": "What is my secret code?", "mode": "immediate"}
|
||||
)
|
||||
mem_content = resp_mem.data.content
|
||||
print(f"Response 3: {mem_content}")
|
||||
|
||||
if "BLUE-42" in mem_content:
|
||||
print("✅ Context retention successful.")
|
||||
else:
|
||||
print("⚠️ Context retention failed.")
|
||||
|
||||
# Cleanup
|
||||
await session.destroy()
|
||||
|
||||
print("\n=== Test 4: Resume Session (Simulation) ===")
|
||||
# Note: Actual resuming depends on backend persistence.
|
||||
# The SDK's client.resume_session(id) tries to find it.
|
||||
# Since we destroyed it above, we expect failure or new session logic in real app.
|
||||
# But let's create a new one to persist, close client, and try to resume if process was same?
|
||||
# Actually persistence usually requires the Copilot Agent/Extension host to keep state or file backed.
|
||||
# The Python SDK defaults to file-based workspace in standard generic usage?
|
||||
# Let's just skip complex resume testing for this simple script as it depends on environment (vscode-chat-session vs file).
|
||||
print("Skipping complex resume test in script.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Test Failed: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
print("\nTests Completed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
94
plugins/debug/github-copilot-sdk/test_injection.py
Normal file
94
plugins/debug/github-copilot-sdk/test_injection.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from copilot import CopilotClient
|
||||
from copilot.types import SessionConfig
|
||||
|
||||
# Define the formatting instruction exactly as in the plugin
|
||||
FORMATTING_INSTRUCTION = (
|
||||
"\n\n[Formatting Guidelines]\n"
|
||||
"When providing explanations or descriptions:\n"
|
||||
"- Use clear paragraph breaks (double line breaks)\n"
|
||||
"- Break long sentences into multiple shorter ones\n"
|
||||
"- Use bullet points or numbered lists for multiple items\n"
|
||||
"- Add headings (##, ###) for major sections\n"
|
||||
"- Ensure proper spacing between different topics"
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
print(f"Python executable: {sys.executable}")
|
||||
|
||||
# Check for GH_TOKEN
|
||||
token = os.environ.get("GH_TOKEN")
|
||||
if token:
|
||||
print("GH_TOKEN is set.")
|
||||
else:
|
||||
print(
|
||||
"Warning: GH_TOKEN not found in environment variables. Relying on CLI auth."
|
||||
)
|
||||
|
||||
client_config = {"log_level": "debug"}
|
||||
|
||||
client = CopilotClient(client_config)
|
||||
|
||||
try:
|
||||
print("Starting client...")
|
||||
await client.start()
|
||||
|
||||
# Test 1: Check available models
|
||||
try:
|
||||
models = await client.list_models()
|
||||
print(f"Connection successful. Found {len(models)} models.")
|
||||
model_id = "gpt-5-mini" # User requested model
|
||||
except Exception as e:
|
||||
print(f"Failed to list models: {e}")
|
||||
return
|
||||
|
||||
print(f"\nCreating session with model {model_id} and system injection...")
|
||||
|
||||
system_message_config = {
|
||||
"mode": "append",
|
||||
"content": "You are a helpful assistant." + FORMATTING_INSTRUCTION,
|
||||
}
|
||||
|
||||
session_config = SessionConfig(
|
||||
model=model_id, system_message=system_message_config
|
||||
)
|
||||
|
||||
session = await client.create_session(config=session_config)
|
||||
print(f"Session created: {session.session_id}")
|
||||
|
||||
# Test 2: Ask the model to summarize its instructions
|
||||
prompt = "Please summarize the [Formatting Guidelines] you have been given in a list."
|
||||
|
||||
print(f"\nSending prompt: '{prompt}'")
|
||||
response = await session.send_and_wait({"prompt": prompt, "mode": "immediate"})
|
||||
|
||||
print("\n--- Model Response ---")
|
||||
content = response.data.content if response and response.data else "No content"
|
||||
print(content)
|
||||
print("----------------------")
|
||||
|
||||
required_keywords = ["paragraph", "break", "heading", "spacing", "bullet"]
|
||||
found_keywords = [kw for kw in required_keywords if kw in content.lower()]
|
||||
|
||||
if len(found_keywords) >= 3:
|
||||
print(
|
||||
f"\n✅ SUCCESS: Model summarized the guidelines correctly. Found match for: {found_keywords}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"\n⚠️ UNCERTAIN: Summary might be generic. Found keywords: {found_keywords}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
finally:
|
||||
await client.stop()
|
||||
print("\nClient stopped.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
359
plugins/debug/language-debug/language_debug.py
Normal file
359
plugins/debug/language-debug/language_debug.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""
|
||||
title: UI Language Debugger
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.1.0
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxwYXRoIGQ9Im01IDggNiA2Ii8+CiAgPHBhdGggZD0ibTQgMTQgNi02IDItMiIvPgogIDxwYXRoIGQ9Ik0yIDVoMTIiLz4KICA8cGF0aCBkPSJNNyAyaDEiLz4KICA8cGF0aCBkPSJtMjIgMjItNS0xMC01IDEwIi8+CiAgPHBhdGggZD0iTTE0IDE4aDYiLz4KPC9zdmc+Cg==
|
||||
description: Debug UI language detection in the browser console and on-page panel.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HTML_WRAPPER_TEMPLATE = """
|
||||
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="{user_language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
#main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* STYLES_INSERTION_POINT */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<!-- CONTENT_INSERTION_POINT -->
|
||||
</div>
|
||||
<!-- SCRIPTS_INSERTION_POINT -->
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
CONTENT_TEMPLATE = """
|
||||
<div class="lang-debug-card" id="lang-debug-card-{unique_id}">
|
||||
<div class="lang-debug-header">
|
||||
🧭 UI Language Debugger
|
||||
</div>
|
||||
<div class="lang-debug-body">
|
||||
<div class="lang-debug-row"><span>python.ui_language</span><code id="lang-py-{unique_id}">{python_language}</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.lang</span><code id="lang-html-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.getAttribute('lang')</span><code id="lang-attr-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dir</span><code id="lang-dir-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.body.lang</span><code id="lang-body-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.language</span><code id="lang-nav-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>navigator.languages</span><code id="lang-navs-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.language</span><code id="lang-store-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.locale</span><code id="lang-locale-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.i18n</span><code id="lang-i18n-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>localStorage.settings</span><code id="lang-settings-{unique_id}">-</code></div>
|
||||
<div class="lang-debug-row"><span>document.documentElement.dataset</span><code id="lang-dataset-{unique_id}">-</code></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
STYLE_TEMPLATE = """
|
||||
.lang-debug-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.lang-debug-header {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.lang-debug-body {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.lang-debug-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 0.9em;
|
||||
color: #1f2937;
|
||||
}
|
||||
.lang-debug-row code {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
color: #0f172a;
|
||||
}
|
||||
"""
|
||||
|
||||
SCRIPT_TEMPLATE = """
|
||||
<script>
|
||||
(function() {{
|
||||
const uniqueId = "{unique_id}";
|
||||
const get = (id) => document.getElementById(id + '-' + uniqueId);
|
||||
|
||||
const safe = (value) => {
|
||||
if (value === undefined || value === null || value === "") return "-";
|
||||
if (Array.isArray(value)) return value.join(", ");
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const safeJson = (value) => {
|
||||
try {
|
||||
return value ? JSON.stringify(JSON.parse(value)) : "-";
|
||||
} catch (e) {
|
||||
return value ? String(value) : "-";
|
||||
}
|
||||
};
|
||||
|
||||
const settingsRaw = localStorage.getItem('settings');
|
||||
const i18nRaw = localStorage.getItem('i18n');
|
||||
const localeRaw = localStorage.getItem('locale');
|
||||
|
||||
const payload = {{
|
||||
htmlLang: document.documentElement.lang,
|
||||
htmlAttr: document.documentElement.getAttribute('lang'),
|
||||
htmlDir: document.documentElement.dir,
|
||||
bodyLang: document.body ? document.body.lang : "",
|
||||
navigatorLanguage: navigator.language,
|
||||
navigatorLanguages: navigator.languages,
|
||||
localStorageLanguage: localStorage.getItem('language'),
|
||||
localStorageLocale: localeRaw,
|
||||
localStorageI18n: i18nRaw,
|
||||
localStorageSettings: settingsRaw,
|
||||
htmlDataset: document.documentElement.dataset,
|
||||
}};
|
||||
|
||||
get('lang-html').textContent = safe(payload.htmlLang);
|
||||
get('lang-attr').textContent = safe(payload.htmlAttr);
|
||||
get('lang-dir').textContent = safe(payload.htmlDir);
|
||||
get('lang-body').textContent = safe(payload.bodyLang);
|
||||
get('lang-nav').textContent = safe(payload.navigatorLanguage);
|
||||
get('lang-navs').textContent = safe(payload.navigatorLanguages);
|
||||
get('lang-store').textContent = safe(payload.localStorageLanguage);
|
||||
get('lang-locale').textContent = safe(payload.localStorageLocale);
|
||||
get('lang-i18n').textContent = safeJson(payload.localStorageI18n);
|
||||
get('lang-settings').textContent = safeJson(payload.localStorageSettings);
|
||||
get('lang-dataset').textContent = safe(payload.htmlDataset);
|
||||
|
||||
console.group('🧭 UI Language Debugger');
|
||||
console.log(payload);
|
||||
console.groupEnd();
|
||||
}})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
class Action:
|
||||
class Valves(BaseModel):
|
||||
SHOW_STATUS: bool = Field(
|
||||
default=True,
|
||||
description="Whether to show operation status updates.",
|
||||
)
|
||||
SHOW_DEBUG_LOG: bool = Field(
|
||||
default=True,
|
||||
description="Whether to print debug logs in the browser console.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||
if isinstance(__user__, (list, tuple)):
|
||||
user_data = __user__[0] if __user__ else {}
|
||||
elif isinstance(__user__, dict):
|
||||
user_data = __user__
|
||||
else:
|
||||
user_data = {}
|
||||
|
||||
return {
|
||||
"user_id": user_data.get("id", "unknown_user"),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", ""),
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
self, body: dict, __metadata__: Optional[dict] = None
|
||||
) -> Dict[str, str]:
|
||||
chat_id = ""
|
||||
message_id = ""
|
||||
|
||||
if isinstance(body, dict):
|
||||
chat_id = body.get("chat_id", "")
|
||||
message_id = body.get("id", "")
|
||||
|
||||
if not chat_id or not message_id:
|
||||
body_metadata = body.get("metadata", {})
|
||||
if isinstance(body_metadata, dict):
|
||||
if not chat_id:
|
||||
chat_id = body_metadata.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = body_metadata.get("message_id", "")
|
||||
|
||||
if __metadata__ and isinstance(__metadata__, dict):
|
||||
if not chat_id:
|
||||
chat_id = __metadata__.get("chat_id", "")
|
||||
if not message_id:
|
||||
message_id = __metadata__.get("message_id", "")
|
||||
|
||||
return {
|
||||
"chat_id": str(chat_id).strip(),
|
||||
"message_id": str(message_id).strip(),
|
||||
}
|
||||
|
||||
async def _emit_status(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
description: str,
|
||||
done: bool = False,
|
||||
):
|
||||
if self.valves.SHOW_STATUS and emitter:
|
||||
await emitter(
|
||||
{"type": "status", "data": {"description": description, "done": done}}
|
||||
)
|
||||
|
||||
async def _emit_debug_log(
|
||||
self,
|
||||
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||
title: str,
|
||||
data: dict,
|
||||
):
|
||||
if not self.valves.SHOW_DEBUG_LOG or not emitter:
|
||||
return
|
||||
|
||||
try:
|
||||
js_code = f"""
|
||||
(async function() {{
|
||||
console.group("🛠️ {title}");
|
||||
console.log({json.dumps(data, ensure_ascii=False)});
|
||||
console.groupEnd();
|
||||
}})();
|
||||
"""
|
||||
|
||||
await emitter({"type": "execute", "data": {"code": js_code}})
|
||||
except Exception as e:
|
||||
logger.error("Error emitting debug log: %s", e, exc_info=True)
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html: str,
|
||||
new_content: str,
|
||||
new_styles: str = "",
|
||||
new_scripts: str = "",
|
||||
user_language: str = "en-US",
|
||||
) -> str:
|
||||
if not existing_html:
|
||||
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||
else:
|
||||
base_html = existing_html
|
||||
|
||||
if "<!-- CONTENT_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- CONTENT_INSERTION_POINT -->",
|
||||
f"{new_content}\n <!-- CONTENT_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
if new_styles and "/* STYLES_INSERTION_POINT */" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"/* STYLES_INSERTION_POINT */",
|
||||
f"{new_styles}\n /* STYLES_INSERTION_POINT */",
|
||||
)
|
||||
|
||||
if new_scripts and "<!-- SCRIPTS_INSERTION_POINT -->" in base_html:
|
||||
base_html = base_html.replace(
|
||||
"<!-- SCRIPTS_INSERTION_POINT -->",
|
||||
f"{new_scripts}\n <!-- SCRIPTS_INSERTION_POINT -->",
|
||||
)
|
||||
|
||||
return base_html
|
||||
|
||||
async def action(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[Dict[str, Any]] = None,
|
||||
__event_emitter__: Optional[Any] = None,
|
||||
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||
__metadata__: Optional[dict] = None,
|
||||
__request__: Optional[Any] = None,
|
||||
) -> Optional[dict]:
|
||||
await self._emit_status(__event_emitter__, "Detecting UI language...", False)
|
||||
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: user context",
|
||||
user_ctx,
|
||||
)
|
||||
|
||||
ui_language = ""
|
||||
if __event_call__:
|
||||
try:
|
||||
response = await __event_call__(
|
||||
{
|
||||
"type": "execute",
|
||||
"data": {
|
||||
"code": "return (localStorage.getItem('locale') || localStorage.getItem('language') || (navigator.languages && navigator.languages[0]) || navigator.language || document.documentElement.lang || '')",
|
||||
},
|
||||
}
|
||||
)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Language Debugger: execute response",
|
||||
{"response": response},
|
||||
)
|
||||
if isinstance(response, dict) and "value" in response:
|
||||
ui_language = response.get("value", "") or ""
|
||||
elif isinstance(response, str):
|
||||
ui_language = response
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to read UI language from frontend: %s", e, exc_info=True
|
||||
)
|
||||
|
||||
unique_id = f"lang_{int(__import__('time').time() * 1000)}"
|
||||
content_html = CONTENT_TEMPLATE.replace("{unique_id}", unique_id).replace(
|
||||
"{python_language}", ui_language or "-"
|
||||
)
|
||||
script_html = SCRIPT_TEMPLATE.replace("{unique_id}", unique_id)
|
||||
script_html = script_html.replace("{{", "{").replace("}}", "}")
|
||||
|
||||
final_html = self._merge_html(
|
||||
"",
|
||||
content_html,
|
||||
STYLE_TEMPLATE,
|
||||
script_html,
|
||||
"en",
|
||||
)
|
||||
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] = (
|
||||
body["messages"][-1].get("content", "") + "\n\n" + html_embed_tag
|
||||
)
|
||||
|
||||
await self._emit_status(__event_emitter__, "UI language captured.", True)
|
||||
return body
|
||||
@@ -1,9 +1,13 @@
|
||||
# Async Context Compression Filter
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.1 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
|
||||
|
||||
## What's new in 1.2.2
|
||||
- **Critical Fix**: Resolved `TypeError: 'str' object is not callable` caused by variable name conflict in logging function.
|
||||
- **Compatibility**: Enhanced `params` handling to support Pydantic objects, improving compatibility with different OpenWebUI versions.
|
||||
|
||||
## What's new in 1.2.1
|
||||
|
||||
- **Smart Configuration**: Automatically detects base model settings for custom models and adds `summary_model_max_context` for independent summary limits.
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# 异步上下文压缩过滤器
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.1 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.2 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
||||
|
||||
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
|
||||
|
||||
## 1.2.2 版本更新
|
||||
- **严重错误修复**: 解决了因日志函数变量名冲突导致的 `TypeError: 'str' object is not callable` 错误。
|
||||
- **兼容性增强**: 改进了 `params` 处理逻辑以支持 Pydantic 对象,提高了对不同 OpenWebUI 版本的兼容性。
|
||||
|
||||
## 1.2.1 版本更新
|
||||
|
||||
- **智能配置增强**: 自动检测自定义模型的基础模型配置,并新增 `summary_model_max_context` 参数以独立控制摘要模型的上下文限制。
|
||||
|
||||
@@ -5,7 +5,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
description: Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.
|
||||
version: 1.2.1
|
||||
version: 1.2.2
|
||||
openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
|
||||
license: MIT
|
||||
|
||||
@@ -839,7 +839,7 @@ class Filter:
|
||||
except Exception as e:
|
||||
logger.error(f"Error emitting debug log: {e}")
|
||||
|
||||
async def _log(self, message: str, type: str = "info", event_call=None):
|
||||
async def _log(self, message: str, log_type: str = "info", event_call=None):
|
||||
"""Unified logging to both backend (print) and frontend (console.log)"""
|
||||
# Backend logging
|
||||
if self.valves.debug_mode:
|
||||
@@ -849,11 +849,11 @@ class Filter:
|
||||
if self.valves.show_debug_log and event_call:
|
||||
try:
|
||||
css = "color: #3b82f6;" # Blue default
|
||||
if type == "error":
|
||||
if log_type == "error":
|
||||
css = "color: #ef4444; font-weight: bold;" # Red
|
||||
elif type == "warning":
|
||||
elif log_type == "warning":
|
||||
css = "color: #f59e0b;" # Orange
|
||||
elif type == "success":
|
||||
elif log_type == "success":
|
||||
css = "color: #10b981; font-weight: bold;" # Green
|
||||
|
||||
# Clean message for frontend: remove separators and extra newlines
|
||||
@@ -999,6 +999,7 @@ class Filter:
|
||||
# 2. For base models: check messages for role='system'
|
||||
system_prompt_content = None
|
||||
|
||||
# Try to get from DB (custom model)
|
||||
# Try to get from DB (custom model)
|
||||
try:
|
||||
model_id = body.get("model")
|
||||
@@ -1026,12 +1027,17 @@ class Filter:
|
||||
# Handle case where params is a JSON string
|
||||
if isinstance(params, str):
|
||||
params = json.loads(params)
|
||||
# Convert Pydantic model to dict if needed
|
||||
elif hasattr(params, "model_dump"):
|
||||
params = params.model_dump()
|
||||
elif hasattr(params, "dict"):
|
||||
params = params.dict()
|
||||
|
||||
# Handle dict or Pydantic object
|
||||
# Now params should be a dict
|
||||
if isinstance(params, dict):
|
||||
system_prompt_content = params.get("system")
|
||||
else:
|
||||
# Assume Pydantic model or object
|
||||
# Fallback: try getattr
|
||||
system_prompt_content = getattr(params, "system", None)
|
||||
|
||||
if system_prompt_content:
|
||||
@@ -1050,7 +1056,7 @@ class Filter:
|
||||
if self.valves.show_debug_log and __event_call__:
|
||||
await self._log(
|
||||
f"[Inlet] ❌ Failed to parse model params: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1071,7 +1077,7 @@ class Filter:
|
||||
if self.valves.show_debug_log and __event_call__:
|
||||
await self._log(
|
||||
f"[Inlet] ❌ Error fetching system prompt from DB: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
if self.valves.debug_mode:
|
||||
@@ -1125,7 +1131,7 @@ class Filter:
|
||||
if not chat_id:
|
||||
await self._log(
|
||||
"[Inlet] ❌ Missing chat_id in metadata, skipping compression",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return body
|
||||
@@ -1154,7 +1160,7 @@ class Filter:
|
||||
else:
|
||||
await self._log(
|
||||
f"[Inlet] ⚠️ Invalid Model Configs (Raw: '{raw_config}'): No valid configs parsed. Expected format: 'model_id:threshold:max_context'",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
else:
|
||||
@@ -1258,7 +1264,7 @@ class Filter:
|
||||
if total_tokens > max_context_tokens:
|
||||
await self._log(
|
||||
f"[Inlet] ⚠️ Candidate prompt ({total_tokens} Tokens) exceeds limit ({max_context_tokens}). Reducing history...",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1395,7 +1401,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[Inlet] Applied summary: {system_info} + Head({len(head_messages)} msg, {head_tokens}t) + Summary({summary_tokens}t) + Tail({len(tail_messages)} msg, {tail_tokens}t) = Total({total_section_tokens}t)",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1455,7 +1461,7 @@ class Filter:
|
||||
if total_tokens > max_context_tokens:
|
||||
await self._log(
|
||||
f"[Inlet] ⚠️ Original messages ({total_tokens} Tokens) exceed limit ({max_context_tokens}). Reducing history...",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1523,7 +1529,7 @@ class Filter:
|
||||
if not chat_id:
|
||||
await self._log(
|
||||
"[Outlet] ❌ Missing chat_id in metadata, skipping compression",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return body
|
||||
@@ -1625,7 +1631,7 @@ class Filter:
|
||||
if current_tokens >= compression_threshold_tokens:
|
||||
await self._log(
|
||||
f"[🔍 Background Calculation] ⚡ Compression threshold triggered (Token: {current_tokens} >= {compression_threshold_tokens})",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1648,7 +1654,7 @@ class Filter:
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[🔍 Background Calculation] ❌ Error: {str(e)}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1687,7 +1693,7 @@ class Filter:
|
||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||
await self._log(
|
||||
f"[🤖 Async Summary Task] ⚠️ target_compressed_count is None, estimating: {target_compressed_count}",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1734,7 +1740,7 @@ class Filter:
|
||||
if not summary_model_id:
|
||||
await self._log(
|
||||
"[🤖 Async Summary Task] ⚠️ Summary model does not exist, skipping compression",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return
|
||||
@@ -1765,7 +1771,7 @@ class Filter:
|
||||
excess_tokens = estimated_input_tokens - max_context_tokens
|
||||
await self._log(
|
||||
f"[🤖 Async Summary Task] ⚠️ Middle messages ({middle_tokens} Tokens) + Buffer exceed summary model limit ({max_context_tokens}), need to remove approx {excess_tokens} Tokens",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1822,7 +1828,7 @@ class Filter:
|
||||
if not new_summary:
|
||||
await self._log(
|
||||
"[🤖 Async Summary Task] ⚠️ Summary generation returned empty result, skipping save",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return
|
||||
@@ -1851,7 +1857,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 Async Summary Task] ✅ Complete! New summary length: {len(new_summary)} characters",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
await self._log(
|
||||
@@ -1957,14 +1963,14 @@ class Filter:
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[Status] Error calculating tokens: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[🤖 Async Summary Task] ❌ Error: {str(e)}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -2066,7 +2072,7 @@ Based on the content above, generate the summary:
|
||||
if not model:
|
||||
await self._log(
|
||||
"[🤖 LLM Call] ⚠️ Summary model does not exist, skipping summary generation",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return ""
|
||||
@@ -2133,7 +2139,7 @@ Based on the content above, generate the summary:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 LLM Call] ✅ Successfully received summary",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -2154,7 +2160,7 @@ Based on the content above, generate the summary:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 LLM Call] ❌ {error_message}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
description: 通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。
|
||||
version: 1.2.1
|
||||
version: 1.2.2
|
||||
openwebui_id: 5c0617cb-a9e4-4bd6-a440-d276534ebd18
|
||||
license: MIT
|
||||
|
||||
@@ -787,7 +787,7 @@ class Filter:
|
||||
except Exception as e:
|
||||
print(f"Error emitting debug log: {e}")
|
||||
|
||||
async def _log(self, message: str, type: str = "info", event_call=None):
|
||||
async def _log(self, message: str, log_type: str = "info", event_call=None):
|
||||
"""统一日志输出到后端 (print) 和前端 (console.log)"""
|
||||
# 后端日志
|
||||
if self.valves.debug_mode:
|
||||
@@ -797,11 +797,11 @@ class Filter:
|
||||
if self.valves.show_debug_log and event_call:
|
||||
try:
|
||||
css = "color: #3b82f6;" # 默认蓝色
|
||||
if type == "error":
|
||||
if log_type == "error":
|
||||
css = "color: #ef4444; font-weight: bold;" # 红色
|
||||
elif type == "warning":
|
||||
elif log_type == "warning":
|
||||
css = "color: #f59e0b;" # 橙色
|
||||
elif type == "success":
|
||||
elif log_type == "success":
|
||||
css = "color: #10b981; font-weight: bold;" # 绿色
|
||||
|
||||
# 清理前端消息:移除分隔符和多余换行
|
||||
@@ -948,12 +948,17 @@ class Filter:
|
||||
# 处理 params 是 JSON 字符串的情况
|
||||
if isinstance(params, str):
|
||||
params = json.loads(params)
|
||||
# 转换 Pydantic 模型为字典
|
||||
elif hasattr(params, "model_dump"):
|
||||
params = params.model_dump()
|
||||
elif hasattr(params, "dict"):
|
||||
params = params.dict()
|
||||
|
||||
# 处理字典或 Pydantic 对象
|
||||
# 处理字典
|
||||
if isinstance(params, dict):
|
||||
system_prompt_content = params.get("system")
|
||||
else:
|
||||
# 假设是 Pydantic 模型或对象
|
||||
# 回退:尝试 getattr
|
||||
system_prompt_content = getattr(params, "system", None)
|
||||
|
||||
if system_prompt_content:
|
||||
@@ -972,7 +977,7 @@ class Filter:
|
||||
if self.valves.show_debug_log and __event_call__:
|
||||
await self._log(
|
||||
f"[Inlet] ❌ 解析模型参数失败: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -986,7 +991,7 @@ class Filter:
|
||||
if self.valves.show_debug_log and __event_call__:
|
||||
await self._log(
|
||||
f"[Inlet] ❌ 数据库中未找到模型",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -994,7 +999,7 @@ class Filter:
|
||||
if self.valves.show_debug_log and __event_call__:
|
||||
await self._log(
|
||||
f"[Inlet] ❌ 从数据库获取系统提示词错误: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
if self.valves.debug_mode:
|
||||
@@ -1048,7 +1053,7 @@ class Filter:
|
||||
if not chat_id:
|
||||
await self._log(
|
||||
"[Inlet] ❌ metadata 中缺少 chat_id,跳过压缩",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return body
|
||||
@@ -1154,7 +1159,7 @@ class Filter:
|
||||
if total_tokens > max_context_tokens:
|
||||
await self._log(
|
||||
f"[Inlet] ⚠️ 候选提示词 ({total_tokens} Tokens) 超过上限 ({max_context_tokens})。正在缩减历史记录...",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1290,7 +1295,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[Inlet] 应用摘要: {system_info} + Head({len(head_messages)} 条, {head_tokens}t) + Summary({summary_tokens}t) + Tail({len(tail_messages)} 条, {tail_tokens}t) = Total({total_section_tokens}t)",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1350,7 +1355,7 @@ class Filter:
|
||||
if total_tokens > max_context_tokens:
|
||||
await self._log(
|
||||
f"[Inlet] ⚠️ 原始消息 ({total_tokens} Tokens) 超过上限 ({max_context_tokens})。正在缩减历史记录...",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1420,7 +1425,7 @@ class Filter:
|
||||
if not chat_id:
|
||||
await self._log(
|
||||
"[Outlet] ❌ metadata 中缺少 chat_id,跳过压缩",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return body
|
||||
@@ -1486,7 +1491,7 @@ class Filter:
|
||||
if current_tokens >= compression_threshold_tokens:
|
||||
await self._log(
|
||||
f"[🔍 后台计算] ⚡ 触发压缩阈值 (Token: {current_tokens} >= {compression_threshold_tokens})",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1509,7 +1514,7 @@ class Filter:
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[🔍 后台计算] ❌ 错误: {str(e)}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1546,7 +1551,7 @@ class Filter:
|
||||
target_compressed_count = max(0, len(messages) - self.valves.keep_last)
|
||||
await self._log(
|
||||
f"[🤖 异步摘要任务] ⚠️ target_compressed_count 为 None,进行估算: {target_compressed_count}",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1593,7 +1598,7 @@ class Filter:
|
||||
if not summary_model_id:
|
||||
await self._log(
|
||||
"[🤖 异步摘要任务] ⚠️ 摘要模型不存在,跳过压缩",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return
|
||||
@@ -1624,7 +1629,7 @@ class Filter:
|
||||
excess_tokens = estimated_input_tokens - max_context_tokens
|
||||
await self._log(
|
||||
f"[🤖 异步摘要任务] ⚠️ 中间消息 ({middle_tokens} Tokens) + 缓冲超过摘要模型上限 ({max_context_tokens}),需要移除约 {excess_tokens} Token",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1681,7 +1686,7 @@ class Filter:
|
||||
if not new_summary:
|
||||
await self._log(
|
||||
"[🤖 异步摘要任务] ⚠️ 摘要生成返回空结果,跳过保存",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return
|
||||
@@ -1710,7 +1715,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 异步摘要任务] ✅ 完成!新摘要长度: {len(new_summary)} 字符",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
await self._log(
|
||||
@@ -1821,14 +1826,14 @@ class Filter:
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[Status] 计算 Token 错误: {e}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await self._log(
|
||||
f"[🤖 异步摘要任务] ❌ 错误: {str(e)}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -1928,7 +1933,7 @@ class Filter:
|
||||
if not model:
|
||||
await self._log(
|
||||
"[🤖 LLM 调用] ⚠️ 摘要模型不存在,跳过摘要生成",
|
||||
type="warning",
|
||||
log_type="warning",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
return ""
|
||||
@@ -1995,7 +2000,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 LLM 调用] ✅ 成功接收摘要",
|
||||
type="success",
|
||||
log_type="success",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
@@ -2016,7 +2021,7 @@ class Filter:
|
||||
|
||||
await self._log(
|
||||
f"[🤖 LLM 调用] ❌ {error_message}",
|
||||
type="error",
|
||||
log_type="error",
|
||||
event_call=__event_call__,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
# Folder Memory
|
||||
|
||||
English | [中文](./README_CN.md)
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.1.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
**Folder Memory** (formerly Folder Rule Collector) is an intelligent context filter plugin for OpenWebUI. It automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||
---
|
||||
|
||||
This ensures that all future conversations within that folder share the same evolved context and rules, without manual updates.
|
||||
### 📌 What's new in 0.1.0
|
||||
- **Initial Release**: Automated "Project Rules" management for OpenWebUI folders.
|
||||
- **Folder-Level Persistence**: Automatically updates folder system prompts with extracted rules.
|
||||
- **Optimized Performance**: Runs asynchronously and supports `PRIORITY` configuration for seamless integration with other filters.
|
||||
|
||||
---
|
||||
|
||||
**Folder Memory** is an intelligent context filter plugin for OpenWebUI. It automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -13,6 +20,10 @@ This ensures that all future conversations within that folder share the same evo
|
||||
- **Async Processing**: Runs in the background without blocking the user's chat experience.
|
||||
- **ORM Integration**: Directly updates folder data using OpenWebUI's internal models for reliability.
|
||||
|
||||
## ⚠️ Prerequisites
|
||||
|
||||
- **Conversations must occur inside a folder.** This plugin only triggers when a chat belongs to a folder (i.e., you need to create a folder in OpenWebUI and start a conversation within it).
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
1. Copy `folder_memory.py` to your OpenWebUI `plugins/filters/` directory (or upload via Admin UI).
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
# 文件夹记忆 (Folder Memory)
|
||||
|
||||
[English](./README.md) | 中文
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.1.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
**文件夹记忆 (Folder Memory)** (原名 Folder Rule Collector) 是一个 OpenWebUI 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
---
|
||||
|
||||
### 📌 0.1.0 版本特性
|
||||
- **首个版本发布**:专注于自动化的“项目规则”管理。
|
||||
- **文件夹级持久化**:自动将提取的规则回写到文件夹系统提示词中。
|
||||
- **性能优化**:采用异步处理机制,并支持 `PRIORITY` 配置,确保与其他过滤器(如上下文压缩)完美协作。
|
||||
|
||||
---
|
||||
|
||||
**文件夹记忆 (Folder Memory)** 是一个 OpenWebUI 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
|
||||
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||
|
||||
@@ -13,6 +22,10 @@
|
||||
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||
|
||||
## ⚠️ 前置条件
|
||||
|
||||
- **对话必须在文件夹内进行。** 此插件仅在聊天属于某个文件夹时触发(即您需要先在 OpenWebUI 中创建一个文件夹,并在其内部开始对话)。
|
||||
|
||||
## 📦 安装指南
|
||||
|
||||
1. 将 `folder_memory.py` (或中文版 `folder_memory_cn.py`) 复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||
|
||||
114
plugins/pipes/github-copilot-sdk/README.md
Normal file
114
plugins/pipes/github-copilot-sdk/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# GitHub Copilot SDK Pipe for OpenWebUI
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.2.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that allows you to use GitHub Copilot models (such as `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`) directly within OpenWebUI. It is built upon the official [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk), providing a native integration experience.
|
||||
|
||||
## 🚀 What's New (v0.2.3)
|
||||
|
||||
* **🧩 Per-user Overrides**: Added user-level overrides for `REASONING_EFFORT`, `CLI_PATH`, `DEBUG`, `SHOW_THINKING`, and `MODEL_ID`.
|
||||
* **🧠 Thinking Output Reliability**: Thinking visibility now respects the user setting and is correctly passed into streaming.
|
||||
* **📝 Formatting Enforcement**: Added automatic formatting hints to ensure outputs are well-structured (paragraphs, lists) and addressed "tight output" issues.
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
* **🚀 Official SDK Integration**: Built on the official SDK for stability and reliability.
|
||||
* **🛠️ Custom Tools Support**: Example tools included (random number). Easy to extend with your own tools.
|
||||
* **💬 Multi-turn Conversation**: Automatically concatenates history context so Copilot understands your previous messages.
|
||||
* **🌊 Streaming Output**: Supports typewriter effect for fast responses.
|
||||
* **🖼️ Multimodal Support**: Supports image uploads, automatically converting them to attachments for Copilot (requires model support).
|
||||
* **🛠️ Zero-config Installation**: Automatically detects and downloads the GitHub Copilot CLI, ready to use out of the box.
|
||||
* **🔑 Secure Authentication**: Supports Fine-grained Personal Access Tokens for minimized permissions.
|
||||
* **🐛 Debug Mode**: Built-in detailed log output (browser console) for easy troubleshooting.
|
||||
* **⚠️ Single Node Only**: Due to local session storage, this plugin currently supports single-node OpenWebUI deployment or multi-node with sticky sessions enabled.
|
||||
|
||||
## 📦 Installation & Usage
|
||||
|
||||
### 1. Import Function
|
||||
|
||||
1. Open OpenWebUI.
|
||||
2. Go to **Workspace** -> **Functions**.
|
||||
3. Click **+** (Create Function).
|
||||
4. Paste the content of `github_copilot_sdk.py` (or `github_copilot_sdk_cn.py` for Chinese) completely.
|
||||
5. Save.
|
||||
|
||||
### 2. Configure Valves (Settings)
|
||||
|
||||
Find "GitHub Copilot" in the function list and click the **⚙️ (Valves)** icon to configure:
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| **GH_TOKEN** | **(Required)** Your GitHub Token. | - |
|
||||
| **MODEL_ID** | The model name to use. | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Path to the Copilot CLI. Will download automatically if not found. | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | Whether to enable debug logs (output to browser console). | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI log level: none, error, warning, info, debug, all. | `error` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | Show session workspace path and summary in debug mode. | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | Exclude models containing these keywords (comma separated). | - |
|
||||
| **WORKSPACE_DIR** | Restricted workspace directory for file operations. | - |
|
||||
| **INFINITE_SESSION** | Enable Infinite Sessions (automatic context compaction). | `True` |
|
||||
| **COMPACTION_THRESHOLD** | Background compaction threshold (0.0-1.0). | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | Buffer exhaustion threshold (0.0-1.0). | `0.95` |
|
||||
| **TIMEOUT** | Timeout for each stream chunk (seconds). | `300` |
|
||||
| **CUSTOM_ENV_VARS** | Custom environment variables (JSON format). | - |
|
||||
| **REASONING_EFFORT** | Reasoning effort level: low, medium, high. `xhigh` is supported for gpt-5.2-codex. | `medium` |
|
||||
| **ENFORCE_FORMATTING** | Add formatting instructions to system prompt for better readability. | `True` |
|
||||
| **ENABLE_TOOLS** | Enable custom tools (example: random number). | `False` |
|
||||
| **AVAILABLE_TOOLS** | Available tools: 'all' or comma-separated list. | `all` |
|
||||
|
||||
#### User Valves (per-user overrides)
|
||||
|
||||
These optional settings can be set per user (overrides global Valves):
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | Reasoning effort level (low/medium/high/xhigh). | - |
|
||||
| **CLI_PATH** | Custom path to Copilot CLI. | - |
|
||||
| **DEBUG** | Enable technical debug logs. | `False` |
|
||||
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||
| **MODEL_ID** | Custom model ID. | - |
|
||||
|
||||
### 3. Using Custom Tools (🆕 Optional)
|
||||
|
||||
This pipe includes **1 example tool** to demonstrate tool calling:
|
||||
|
||||
* **🎲 generate_random_number**: Generate random integers
|
||||
|
||||
**To enable:**
|
||||
|
||||
1. Set `ENABLE_TOOLS: true` in Valves
|
||||
2. Try: "Give me a random number"
|
||||
|
||||
**📚 For detailed usage and creating your own tools, see [TOOLS_USAGE.md](TOOLS_USAGE.md)**
|
||||
|
||||
### 4. Get GH_TOKEN
|
||||
|
||||
For security, it is recommended to use a **Fine-grained Personal Access Token**:
|
||||
|
||||
1. Visit [GitHub Token Settings](https://github.com/settings/tokens?type=beta).
|
||||
2. Click **Generate new token**.
|
||||
3. **Repository access**: Select **Public repositories** (Required to access Copilot permissions).
|
||||
4. **Permissions**:
|
||||
* Click **Account permissions**.
|
||||
* Find **Copilot Requests** (It defaults to **Read-only**, no selection needed).
|
||||
5. Generate and copy the Token.
|
||||
|
||||
## 📋 Dependencies
|
||||
|
||||
This Pipe will automatically attempt to install the following dependencies:
|
||||
|
||||
* `github-copilot-sdk` (Python package)
|
||||
* `github-copilot-cli` (Binary file, installed via official script)
|
||||
|
||||
## ⚠️ FAQ
|
||||
|
||||
* **Stuck on "Waiting..."**:
|
||||
* Check if `GH_TOKEN` is correct and has `Copilot Requests` permission.
|
||||
* **Images not recognized**:
|
||||
* Ensure `MODEL_ID` is a model that supports multimodal input.
|
||||
* **Thinking not shown**:
|
||||
* Ensure **streaming is enabled** and the selected model supports reasoning output.
|
||||
* **CLI Installation Failed**:
|
||||
* Ensure the OpenWebUI container has internet access.
|
||||
* You can manually download the CLI and specify `CLI_PATH` in Valves.
|
||||
114
plugins/pipes/github-copilot-sdk/README_CN.md
Normal file
114
plugins/pipes/github-copilot-sdk/README_CN.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# GitHub Copilot SDK 官方管道
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.2.3 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,允许你直接在 OpenWebUI 中使用 GitHub Copilot 模型(如 `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`)。它基于官方 [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk) 构建,提供了原生级的集成体验。
|
||||
|
||||
## 🚀 最新特性 (v0.2.3)
|
||||
|
||||
* **🧩 用户级覆盖**:新增 `REASONING_EFFORT`、`CLI_PATH`、`DEBUG`、`SHOW_THINKING`、`MODEL_ID` 的用户级覆盖。
|
||||
* **🧠 思考输出可靠性**:思考显示会遵循用户设置,并正确传递到流式输出中。
|
||||
* **📝 格式化输出增强**:自动优化输出格式(短句、段落、列表),并解决了在某些界面下显示过于紧凑的问题。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
* **🚀 官方 SDK 集成**:基于官方 SDK,稳定可靠。
|
||||
* **🛠️ 自定义工具支持**:内置示例工具(随机数)。易于扩展自定义工具。
|
||||
* **💬 多轮对话支持**:自动拼接历史上下文,Copilot 能理解你的前文。
|
||||
* **🌊 流式输出 (Streaming)**:支持打字机效果,响应迅速。
|
||||
* **🖼️ 多模态支持**:支持上传图片,自动转换为附件发送给 Copilot(需模型支持)。
|
||||
* **🛠️ 零配置安装**:自动检测并下载 GitHub Copilot CLI,开箱即用。
|
||||
* **🔑 安全认证**:支持 Fine-grained Personal Access Tokens,权限最小化。
|
||||
* **🐛 调试模式**:内置详细的日志输出(浏览器控制台),方便排查问题。
|
||||
* **⚠️ 仅支持单节点**:由于会话状态存储在本地,本插件目前仅支持 OpenWebUI 单节点部署,或开启了会话粘性 (Sticky Session) 的多节点集群。
|
||||
|
||||
## 📦 安装与使用
|
||||
|
||||
### 1. 导入函数
|
||||
|
||||
1. 打开 OpenWebUI。
|
||||
2. 进入 **Workspace** -> **Functions**。
|
||||
3. 点击 **+** (创建函数)。
|
||||
4. 将 `github_copilot_sdk_cn.py` 的内容完整粘贴进去。
|
||||
5. 保存。
|
||||
|
||||
### 2. 配置 Valves (设置)
|
||||
|
||||
在函数列表中找到 "GitHub Copilot",点击 **⚙️ (Valves)** 图标进行配置:
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| **GH_TOKEN** | **(必填)** 你的 GitHub Token。 | - |
|
||||
| **MODEL_ID** | 使用的模型名称。 | `gpt-5-mini` |
|
||||
| **CLI_PATH** | Copilot CLI 的路径。如果未找到会自动下载。 | `/usr/local/bin/copilot` |
|
||||
| **DEBUG** | 是否开启调试日志(输出到浏览器控制台)。 | `False` |
|
||||
| **LOG_LEVEL** | Copilot CLI 日志级别: none, error, warning, info, debug, all。 | `error` |
|
||||
| **SHOW_THINKING** | 是否显示模型推理/思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **SHOW_WORKSPACE_INFO** | 在调试模式下显示会话工作空间路径和摘要。 | `True` |
|
||||
| **EXCLUDE_KEYWORDS** | 排除包含这些关键词的模型 (逗号分隔)。 | - |
|
||||
| **WORKSPACE_DIR** | 文件操作的受限工作目录。 | - |
|
||||
| **INFINITE_SESSION** | 启用无限会话 (自动上下文压缩)。 | `True` |
|
||||
| **COMPACTION_THRESHOLD** | 后台压缩阈值 (0.0-1.0)。 | `0.8` |
|
||||
| **BUFFER_THRESHOLD** | 缓冲耗尽阈值 (0.0-1.0)。 | `0.95` |
|
||||
| **TIMEOUT** | 流式数据块超时时间 (秒)。 | `300` |
|
||||
| **CUSTOM_ENV_VARS** | 自定义环境变量 (JSON 格式)。 | - |
|
||||
| **ENABLE_TOOLS** | 启用自定义工具 (示例:随机数)。 | `False` |
|
||||
| **AVAILABLE_TOOLS** | 可用工具: 'all' 或逗号分隔列表。 | `all` |
|
||||
| **REASONING_EFFORT** | 推理强度级别:low, medium, high。`gpt-5.2-codex`额外支持`xhigh`。 | `medium` |
|
||||
| **ENFORCE_FORMATTING** | 是否强制添加格式化指导,以提高输出可读性。 | `True` |
|
||||
|
||||
#### 用户 Valves(按用户覆盖)
|
||||
|
||||
以下设置可按用户单独配置(覆盖全局 Valves):
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| **REASONING_EFFORT** | 推理强度级别(low/medium/high/xhigh)。 | - |
|
||||
| **CLI_PATH** | 自定义 Copilot CLI 路径。 | - |
|
||||
| **DEBUG** | 是否启用技术调试日志。 | `False` |
|
||||
| **SHOW_THINKING** | 是否显示思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||
| **MODEL_ID** | 自定义模型 ID。 | - |
|
||||
|
||||
### 3. 使用自定义工具 (🆕 可选)
|
||||
|
||||
本 Pipe 内置了 **1 个示例工具**来展示工具调用功能:
|
||||
|
||||
* **🎲 generate_random_number**:生成随机整数
|
||||
|
||||
**启用方法:**
|
||||
|
||||
1. 在 Valves 中设置 `ENABLE_TOOLS: true`
|
||||
2. 尝试问:“给我一个随机数”
|
||||
|
||||
**📚 详细使用说明和创建自定义工具,请参阅 [TOOLS_USAGE.md](TOOLS_USAGE.md)**
|
||||
|
||||
### 4. 获取 GH_TOKEN
|
||||
|
||||
为了安全起见,推荐使用 **Fine-grained Personal Access Token**:
|
||||
|
||||
1. 访问 [GitHub Token Settings](https://github.com/settings/tokens?type=beta)。
|
||||
2. 点击 **Generate new token**。
|
||||
3. **Repository access**: 选择 **Public repositories** (必须选择此项才能看到 Copilot 权限)。
|
||||
4. **Permissions**:
|
||||
* 点击 **Account permissions**。
|
||||
* 找到 **Copilot Requests** (默认即为 **Read-only**,无需手动修改)。
|
||||
5. 生成并复制 Token。
|
||||
|
||||
## 📋 依赖说明
|
||||
|
||||
该 Pipe 会自动尝试安装以下依赖(如果环境中缺失):
|
||||
|
||||
* `github-copilot-sdk` (Python 包)
|
||||
* `github-copilot-cli` (二进制文件,通过官方脚本安装)
|
||||
|
||||
## ⚠️ 常见问题
|
||||
|
||||
* **一直显示 "Waiting..."**:
|
||||
* 检查 `GH_TOKEN` 是否正确且拥有 `Copilot Requests` 权限。
|
||||
* **图片无法识别**:
|
||||
* 确保 `MODEL_ID` 是支持多模态的模型。
|
||||
* **CLI 安装失败**:
|
||||
* 确保 OpenWebUI 容器有外网访问权限。
|
||||
* 你可以手动下载 CLI 并挂载到容器中,然后在 Valves 中指定 `CLI_PATH`。
|
||||
* **看不到思考过程**:
|
||||
* 确认已开启**流式输出**,且所选模型支持推理输出。
|
||||
BIN
plugins/pipes/github-copilot-sdk/github_copilot_sdk.png
Normal file
BIN
plugins/pipes/github-copilot-sdk/github_copilot_sdk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
1449
plugins/pipes/github-copilot-sdk/github_copilot_sdk.py
Normal file
1449
plugins/pipes/github-copilot-sdk/github_copilot_sdk.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.png
Normal file
BIN
plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 474 KiB |
1553
plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.py
Normal file
1553
plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -93,7 +93,11 @@ def scan_plugins_directory(plugins_dir: str) -> list[dict[str, Any]]:
|
||||
return plugins
|
||||
|
||||
# Walk through all subdirectories
|
||||
for root, _dirs, files in os.walk(plugins_path):
|
||||
for root, dirs, files in os.walk(plugins_path):
|
||||
# Exclude debug directory from scan
|
||||
if "debug" in dirs:
|
||||
dirs.remove("debug")
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and not file.startswith("__"):
|
||||
# Skip specific files that should not trigger release
|
||||
@@ -110,6 +114,11 @@ def scan_plugins_directory(plugins_dir: str) -> list[dict[str, Any]]:
|
||||
if metadata:
|
||||
# Determine plugin type from directory structure
|
||||
rel_path = os.path.relpath(file_path, plugins_dir)
|
||||
|
||||
# Normalize file_path to always start with "plugins/" for consistent ID comparison
|
||||
# regardless of where we scan from (/tmp/old_repo or ./plugins)
|
||||
metadata["file_path"] = os.path.join("plugins", rel_path)
|
||||
|
||||
parts = rel_path.split(os.sep)
|
||||
if len(parts) > 0:
|
||||
metadata["type"] = parts[0] # actions, filters, pipes, etc.
|
||||
@@ -139,42 +148,45 @@ def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[
|
||||
print(f"Error parsing {previous_file}", file=sys.stderr)
|
||||
return {"added": current, "updated": [], "removed": []}
|
||||
|
||||
# Create lookup dictionaries by title
|
||||
# Helper to extract title/version from either simple dict or raw post object
|
||||
# Create lookup dictionaries by file_path (fallback to title)
|
||||
# Helper to extract title/version/file_path from either simple dict or raw post object
|
||||
def get_info(p):
|
||||
if "data" in p and "function" in p["data"]:
|
||||
# It's a raw post object
|
||||
manifest = p["data"]["function"].get("meta", {}).get("manifest", {})
|
||||
title = manifest.get("title") or p.get("title")
|
||||
version = manifest.get("version", "0.0.0")
|
||||
return title, version, p
|
||||
file_path = p.get("file_path")
|
||||
return title, version, file_path, p
|
||||
else:
|
||||
# It's a simple dict
|
||||
return p.get("title"), p.get("version"), p
|
||||
return p.get("title"), p.get("version"), p.get("file_path"), p
|
||||
|
||||
current_by_title = {}
|
||||
current_by_key = {}
|
||||
for p in current:
|
||||
title, _, _ = get_info(p)
|
||||
if title:
|
||||
current_by_title[title] = p
|
||||
title, _, file_path, _ = get_info(p)
|
||||
key = file_path or title
|
||||
if key:
|
||||
current_by_key[key] = p
|
||||
|
||||
previous_by_title = {}
|
||||
previous_by_key = {}
|
||||
for p in previous:
|
||||
title, _, _ = get_info(p)
|
||||
if title:
|
||||
previous_by_title[title] = p
|
||||
title, _, file_path, _ = get_info(p)
|
||||
key = file_path or title
|
||||
if key:
|
||||
previous_by_key[key] = p
|
||||
|
||||
result = {"added": [], "updated": [], "removed": []}
|
||||
|
||||
# Find added and updated plugins
|
||||
for title, plugin in current_by_title.items():
|
||||
curr_title, curr_ver, _ = get_info(plugin)
|
||||
for key, plugin in current_by_key.items():
|
||||
curr_title, curr_ver, _file_path, _ = get_info(plugin)
|
||||
|
||||
if title not in previous_by_title:
|
||||
if key not in previous_by_key:
|
||||
result["added"].append(plugin)
|
||||
else:
|
||||
prev_plugin = previous_by_title[title]
|
||||
_, prev_ver, _ = get_info(prev_plugin)
|
||||
prev_plugin = previous_by_key[key]
|
||||
_, prev_ver, _prev_file_path, _ = get_info(prev_plugin)
|
||||
|
||||
if curr_ver != prev_ver:
|
||||
result["updated"].append(
|
||||
@@ -185,8 +197,8 @@ def compare_versions(current: list[dict], previous_file: str) -> dict[str, list[
|
||||
)
|
||||
|
||||
# Find removed plugins
|
||||
for title, plugin in previous_by_title.items():
|
||||
if title not in current_by_title:
|
||||
for key, plugin in previous_by_key.items():
|
||||
if key not in current_by_key:
|
||||
result["removed"].append(plugin)
|
||||
|
||||
return result
|
||||
@@ -217,6 +229,23 @@ def format_markdown_table(plugins: list[dict]) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _get_readme_url(file_path: str) -> str:
|
||||
"""
|
||||
Generate GitHub README URL from plugin file path.
|
||||
从插件文件路径生成 GitHub README 链接。
|
||||
"""
|
||||
if not file_path:
|
||||
return ""
|
||||
# Extract plugin directory (e.g., plugins/filters/folder-memory/folder_memory.py -> plugins/filters/folder-memory)
|
||||
from pathlib import Path
|
||||
|
||||
plugin_dir = Path(file_path).parent
|
||||
# Convert to GitHub URL
|
||||
return (
|
||||
f"https://github.com/Fu-Jie/awesome-openwebui/blob/main/{plugin_dir}/README.md"
|
||||
)
|
||||
|
||||
|
||||
def format_release_notes(
|
||||
comparison: dict[str, list], ignore_removed: bool = False
|
||||
) -> str:
|
||||
@@ -229,9 +258,12 @@ def format_release_notes(
|
||||
if comparison["added"]:
|
||||
lines.append("### 新增插件 / New Plugins")
|
||||
for plugin in comparison["added"]:
|
||||
readme_url = _get_readme_url(plugin.get("file_path", ""))
|
||||
lines.append(f"- **{plugin['title']}** v{plugin['version']}")
|
||||
if plugin.get("description"):
|
||||
lines.append(f" - {plugin['description']}")
|
||||
if readme_url:
|
||||
lines.append(f" - 📖 [README / 文档]({readme_url})")
|
||||
lines.append("")
|
||||
|
||||
if comparison["updated"]:
|
||||
@@ -258,7 +290,10 @@ def format_release_notes(
|
||||
)
|
||||
prev_ver = prev_manifest.get("version") or prev.get("version")
|
||||
|
||||
readme_url = _get_readme_url(curr.get("file_path", ""))
|
||||
lines.append(f"- **{curr_title}**: v{prev_ver} → v{curr_ver}")
|
||||
if readme_url:
|
||||
lines.append(f" - 📖 [README / 文档]({readme_url})")
|
||||
lines.append("")
|
||||
|
||||
if comparison["removed"] and not ignore_removed:
|
||||
|
||||
@@ -473,10 +473,27 @@ class OpenWebUICommunityClient:
|
||||
# 查找 README
|
||||
readme_content = self._find_readme(file_path)
|
||||
|
||||
# 获取远程帖子信息(提前获取,用于判断是否需要上传图片)
|
||||
remote_post = None
|
||||
if post_id:
|
||||
remote_post = self.get_post(post_id)
|
||||
|
||||
# 查找并上传图片
|
||||
media_urls = None
|
||||
image_path = self._find_image(file_path)
|
||||
if image_path:
|
||||
|
||||
# 决定是否上传图片
|
||||
should_upload_image = True
|
||||
if remote_post:
|
||||
remote_media = remote_post.get("media", [])
|
||||
if remote_media and len(remote_media) > 0:
|
||||
# 远程已有图片,跳过上传以避免覆盖(防止出现空白图片问题)
|
||||
print(
|
||||
f" ℹ️ Remote post already has images. Skipping auto-upload to preserve existing media."
|
||||
)
|
||||
should_upload_image = False
|
||||
|
||||
if image_path and should_upload_image:
|
||||
print(f" Found image: {os.path.basename(image_path)}")
|
||||
image_url = self.upload_image(image_path)
|
||||
if image_url:
|
||||
@@ -500,7 +517,8 @@ class OpenWebUICommunityClient:
|
||||
post_id = existing_post.get("id")
|
||||
print(f" Found existing post: {title} (ID: {post_id})")
|
||||
self._inject_id_to_file(file_path, post_id)
|
||||
# post_id 已设置,后续将进入更新流程
|
||||
# post_id 已设置,重新获取 remote_post 以便后续版本检查
|
||||
remote_post = self.get_post(post_id)
|
||||
|
||||
else:
|
||||
# 2. 如果没找到,且允许自动创建,则创建
|
||||
@@ -522,11 +540,6 @@ class OpenWebUICommunityClient:
|
||||
return True, f"Created new post (ID: {new_post_id})"
|
||||
return False, "Failed to create new post"
|
||||
|
||||
# 获取远程帖子信息(只需获取一次)
|
||||
remote_post = None
|
||||
if post_id:
|
||||
remote_post = self.get_post(post_id)
|
||||
|
||||
# 版本检查(仅对更新有效)
|
||||
if not force and local_version and remote_post:
|
||||
remote_version = (
|
||||
|
||||
@@ -23,7 +23,11 @@ from openwebui_community_client import get_client
|
||||
def find_existing_plugins(plugins_dir: str) -> list:
|
||||
"""查找所有已发布的插件文件(有 openwebui_id 的)"""
|
||||
plugins = []
|
||||
for root, _, files in os.walk(plugins_dir):
|
||||
for root, dirs, files in os.walk(plugins_dir):
|
||||
# Exclude debug directory
|
||||
if "debug" in dirs:
|
||||
dirs.remove("debug")
|
||||
|
||||
for file in files:
|
||||
if file.endswith(".py") and not file.startswith("__"):
|
||||
file_path = os.path.join(root, file)
|
||||
|
||||
Reference in New Issue
Block a user