Compare commits
85 Commits
v2026.01.1
...
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 | ||
|
|
ba92649a98 | ||
|
|
d2276dcaae | ||
|
|
25c9d20f3d | ||
|
|
0d853577df | ||
|
|
f91f3d8692 | ||
|
|
0f7cad8dfa | ||
|
|
db1a1e7ef0 | ||
|
|
e7de80a059 | ||
|
|
0d8c4e048e | ||
|
|
014a5a9d1f | ||
|
|
a6dd970859 | ||
|
|
aac730f5b1 | ||
|
|
ff95d9328e | ||
|
|
afe1d8cf52 | ||
|
|
67b819f3de | ||
|
|
9b6acb6b95 | ||
|
|
a9a59e1e34 | ||
|
|
5b05397356 | ||
|
|
7a7dbc0cfa | ||
|
|
6ac0ba6efe | ||
|
|
d3d008efb4 | ||
|
|
4f1528128a | ||
|
|
93c4326206 | ||
|
|
0fca7fe524 | ||
|
|
afdcab10c6 | ||
|
|
f8cc5eabe6 |
@@ -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`
|
||||
|
||||
116
.github/copilot-instructions.md
vendored
116
.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)
|
||||
@@ -822,11 +904,27 @@ Filter 实例是**单例 (Singleton)**。
|
||||
|
||||
#### Commit Message 规范
|
||||
使用 Conventional Commits 格式 (`feat`, `fix`, `docs`, etc.)。
|
||||
**必须**在提交标题与正文中清晰描述变更内容,确保在 Release 页面可读且可追踪。
|
||||
|
||||
要求:
|
||||
- 标题必须包含“做了什么”与影响范围(避免含糊词)。
|
||||
- 正文必须列出关键变更点(1-3 条),与实际改动一一对应。
|
||||
- 若影响用户或插件行为,必须在正文标明影响与迁移说明。
|
||||
|
||||
推荐格式:
|
||||
- `feat(actions): add export settings panel`
|
||||
- `fix(filters): handle empty metadata to avoid crash`
|
||||
- `docs(plugins): update bilingual README structure`
|
||||
|
||||
正文示例:
|
||||
- Add valves for export format selection
|
||||
- Update README/README_CN to include What's New section
|
||||
- Migration: default TITLE_SOURCE changed to chat_title
|
||||
|
||||
### 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()}')"
|
||||
|
||||
29
README.md
29
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-18 00:08
|
||||
> 🕐 Auto-updated: 2026-01-28 09:37
|
||||
|
||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **119** | **113** | **25** |
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **165** | **167** | **35** |
|
||||
|
||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| **16** | **1659** | **19990** | **99** | **126** |
|
||||
| **19** | **2553** | **29974** | **150** | **198** |
|
||||
|
||||
### 🔥 Top 6 Popular Plugins
|
||||
|
||||
> 🕐 Auto-updated: 2026-01-18 00:08
|
||||
> 🕐 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 | 507 | 4641 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 228 | 2285 | 2026-01-17 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 202 | 751 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 174 | 1906 | 2026-01-17 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 134 | 1255 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 132 | 2269 | 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,11 +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
|
||||
@@ -100,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.
|
||||
|
||||
27
README_CN.md
27
README_CN.md
@@ -7,28 +7,28 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
<!-- STATS_START -->
|
||||
## 📊 社区统计
|
||||
|
||||
> 🕐 自动更新于 2026-01-18 00:08
|
||||
> 🕐 自动更新于 2026-01-28 09:37
|
||||
|
||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **119** | **113** | **25** |
|
||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **165** | **167** | **35** |
|
||||
|
||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| **16** | **1659** | **19990** | **99** | **126** |
|
||||
| **19** | **2553** | **29974** | **150** | **198** |
|
||||
|
||||
### 🔥 热门插件 Top 6
|
||||
|
||||
> 🕐 自动更新于 2026-01-18 00:08
|
||||
> 🕐 自动更新于 2026-01-28 09:37
|
||||
|
||||
| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |
|
||||
|:---:|------|:---:|:---:|:---:|:---:|
|
||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 507 | 4641 | 2026-01-17 |
|
||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 228 | 2285 | 2026-01-17 |
|
||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 202 | 751 | 2026-01-07 |
|
||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 174 | 1906 | 2026-01-17 |
|
||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 134 | 1255 | 2026-01-17 |
|
||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 132 | 2269 | 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,17 +49,22 @@ 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`): 自动从对话中提取项目规则并注入到文件夹系统提示词中。
|
||||
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
|
||||
- **Gemini Multimodal Filter** (`web_gemini_multimodel_filter`): 为任意模型提供多模态能力(PDF、Office、视频等),支持智能路由和字幕精修。
|
||||
- **Markdown Normalizer** (`markdown_normalizer`): 修复 LLM 输出中常见的 Markdown 格式问题。
|
||||
- **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)
|
||||
@@ -106,6 +112,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
||||
### 贡献代码
|
||||
|
||||
如果你有优质的提示词或插件想要分享:
|
||||
|
||||
1. Fork 本仓库。
|
||||
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
||||
3. 提交 Pull Request。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "downloads",
|
||||
"message": "1.7k",
|
||||
"message": "2.6k",
|
||||
"color": "blue",
|
||||
"namedLogo": "openwebui"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "followers",
|
||||
"message": "119",
|
||||
"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": "113",
|
||||
"message": "167",
|
||||
"color": "orange"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "upvotes",
|
||||
"message": "99",
|
||||
"message": "150",
|
||||
"color": "brightgreen"
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"total_posts": 16,
|
||||
"total_downloads": 1659,
|
||||
"total_views": 19990,
|
||||
"total_upvotes": 99,
|
||||
"total_posts": 19,
|
||||
"total_downloads": 2553,
|
||||
"total_views": 29974,
|
||||
"total_upvotes": 150,
|
||||
"total_downvotes": 2,
|
||||
"total_saves": 126,
|
||||
"total_comments": 23,
|
||||
"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": 507,
|
||||
"views": 4641,
|
||||
"upvotes": 14,
|
||||
"saves": 28,
|
||||
"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": 228,
|
||||
"views": 2285,
|
||||
"upvotes": 11,
|
||||
"saves": 16,
|
||||
"comments": 2,
|
||||
"downloads": 441,
|
||||
"views": 4010,
|
||||
"upvotes": 19,
|
||||
"saves": 28,
|
||||
"comments": 10,
|
||||
"created_at": "2025-12-28",
|
||||
"updated_at": "2026-01-17",
|
||||
"updated_at": "2026-01-27",
|
||||
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
||||
},
|
||||
{
|
||||
@@ -50,10 +52,10 @@
|
||||
"version": "0.3.7",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
||||
"downloads": 202,
|
||||
"views": 751,
|
||||
"upvotes": 3,
|
||||
"saves": 5,
|
||||
"downloads": 265,
|
||||
"views": 1099,
|
||||
"upvotes": 4,
|
||||
"saves": 6,
|
||||
"comments": 0,
|
||||
"created_at": "2025-05-30",
|
||||
"updated_at": "2026-01-07",
|
||||
@@ -63,16 +65,16 @@
|
||||
"title": "Async Context Compression",
|
||||
"slug": "async_context_compression_b1655bc8",
|
||||
"type": "action",
|
||||
"version": "1.1.3",
|
||||
"version": "1.2.2",
|
||||
"author": "Fu-Jie",
|
||||
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||
"downloads": 174,
|
||||
"views": 1906,
|
||||
"upvotes": 7,
|
||||
"saves": 18,
|
||||
"downloads": 240,
|
||||
"views": 2593,
|
||||
"upvotes": 9,
|
||||
"saves": 27,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-01-17",
|
||||
"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": 134,
|
||||
"views": 1255,
|
||||
"upvotes": 6,
|
||||
"saves": 15,
|
||||
"downloads": 240,
|
||||
"views": 1951,
|
||||
"upvotes": 8,
|
||||
"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": 132,
|
||||
"views": 2269,
|
||||
"upvotes": 8,
|
||||
"saves": 10,
|
||||
"downloads": 179,
|
||||
"views": 2805,
|
||||
"upvotes": 11,
|
||||
"saves": 13,
|
||||
"comments": 2,
|
||||
"created_at": "2025-12-30",
|
||||
"updated_at": "2026-01-17",
|
||||
@@ -111,34 +113,18 @@
|
||||
"title": "Markdown Normalizer",
|
||||
"slug": "markdown_normalizer_baaa8732",
|
||||
"type": "action",
|
||||
"version": "1.2.2",
|
||||
"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": 63,
|
||||
"views": 1746,
|
||||
"upvotes": 9,
|
||||
"saves": 15,
|
||||
"downloads": 164,
|
||||
"views": 2910,
|
||||
"upvotes": 10,
|
||||
"saves": 22,
|
||||
"comments": 5,
|
||||
"created_at": "2026-01-12",
|
||||
"updated_at": "2026-01-17",
|
||||
"updated_at": "2026-01-19",
|
||||
"url": "https://openwebui.com/posts/markdown_normalizer_baaa8732"
|
||||
},
|
||||
{
|
||||
"title": "导出为 Word (增强版)",
|
||||
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
||||
"type": "action",
|
||||
"version": "0.4.3",
|
||||
"author": "Fu-Jie",
|
||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||
"downloads": 62,
|
||||
"views": 1262,
|
||||
"upvotes": 10,
|
||||
"saves": 3,
|
||||
"comments": 1,
|
||||
"created_at": "2026-01-04",
|
||||
"updated_at": "2026-01-17",
|
||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||
},
|
||||
{
|
||||
"title": "Deep Dive",
|
||||
"slug": "deep_dive_c0b846e4",
|
||||
@@ -146,31 +132,63 @@
|
||||
"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": 58,
|
||||
"views": 605,
|
||||
"upvotes": 3,
|
||||
"saves": 5,
|
||||
"downloads": 96,
|
||||
"views": 880,
|
||||
"upvotes": 4,
|
||||
"saves": 8,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-08",
|
||||
"updated_at": "2026-01-08",
|
||||
"url": "https://openwebui.com/posts/deep_dive_c0b846e4"
|
||||
},
|
||||
{
|
||||
"title": "导出为 Word (增强版)",
|
||||
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
||||
"type": "action",
|
||||
"version": "0.4.3",
|
||||
"author": "Fu-Jie",
|
||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||
"downloads": 90,
|
||||
"views": 1710,
|
||||
"upvotes": 11,
|
||||
"saves": 4,
|
||||
"comments": 4,
|
||||
"created_at": "2026-01-04",
|
||||
"updated_at": "2026-01-17",
|
||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||
},
|
||||
{
|
||||
"title": "📊 智能信息图 (AntV Infographic)",
|
||||
"slug": "智能信息图_e04a48ff",
|
||||
"type": "action",
|
||||
"version": "1.4.9",
|
||||
"version": "1.5.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||
"downloads": 41,
|
||||
"views": 654,
|
||||
"upvotes": 5,
|
||||
"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": 22,
|
||||
"views": 392,
|
||||
"upvotes": 2,
|
||||
"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": 430,
|
||||
"upvotes": 4,
|
||||
"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.1.3",
|
||||
"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": 340,
|
||||
"upvotes": 4,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2025-11-08",
|
||||
"updated_at": "2026-01-17",
|
||||
"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,15 +260,31 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Fu-Jie",
|
||||
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
||||
"downloads": 6,
|
||||
"views": 254,
|
||||
"upvotes": 2,
|
||||
"downloads": 10,
|
||||
"views": 321,
|
||||
"upvotes": 3,
|
||||
"saves": 1,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-08",
|
||||
"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,8 +293,8 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 46,
|
||||
"upvotes": 0,
|
||||
"views": 101,
|
||||
"upvotes": 1,
|
||||
"saves": 0,
|
||||
"comments": 0,
|
||||
"created_at": "2026-01-14",
|
||||
@@ -259,9 +309,9 @@
|
||||
"author": "",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"views": 1154,
|
||||
"upvotes": 11,
|
||||
"saves": 7,
|
||||
"views": 1291,
|
||||
"upvotes": 12,
|
||||
"saves": 8,
|
||||
"comments": 2,
|
||||
"created_at": "2026-01-10",
|
||||
"updated_at": "2026-01-10",
|
||||
@@ -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": 119,
|
||||
"following": 2,
|
||||
"total_points": 113,
|
||||
"post_points": 97,
|
||||
"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-18 00:08
|
||||
> 📅 Updated: 2026-01-28 09:37
|
||||
|
||||
## 📈 Overview
|
||||
|
||||
| Metric | Value |
|
||||
|------|------|
|
||||
| 📝 Total Posts | 16 |
|
||||
| ⬇️ Total Downloads | 1659 |
|
||||
| 👁️ Total Views | 19990 |
|
||||
| 👍 Total Upvotes | 99 |
|
||||
| 💾 Total Saves | 126 |
|
||||
| 💬 Total Comments | 23 |
|
||||
| 📝 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 | 507 | 4641 | 14 | 28 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 228 | 2285 | 11 | 16 | 2026-01-17 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 202 | 751 | 3 | 5 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 174 | 1906 | 7 | 18 | 2026-01-17 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 134 | 1255 | 6 | 15 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 132 | 2269 | 8 | 10 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.2 | 63 | 1746 | 9 | 15 | 2026-01-17 |
|
||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 62 | 1262 | 10 | 3 | 2026-01-17 |
|
||||
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 58 | 605 | 3 | 5 | 2026-01-08 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 41 | 654 | 5 | 0 | 2026-01-17 |
|
||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 22 | 392 | 2 | 1 | 2026-01-17 |
|
||||
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 16 | 430 | 4 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 14 | 340 | 4 | 1 | 2026-01-17 |
|
||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 254 | 2 | 1 | 2026-01-08 |
|
||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 46 | 0 | 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 | 1154 | 11 | 7 | 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-18 00:08
|
||||
> 📅 更新时间: 2026-01-28 09:37
|
||||
|
||||
## 📈 总览
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 📝 发布数量 | 16 |
|
||||
| ⬇️ 总下载量 | 1659 |
|
||||
| 👁️ 总浏览量 | 19990 |
|
||||
| 👍 总点赞数 | 99 |
|
||||
| 💾 总收藏数 | 126 |
|
||||
| 💬 总评论数 | 23 |
|
||||
| 📝 发布数量 | 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 | 507 | 4641 | 14 | 28 | 2026-01-17 |
|
||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 228 | 2285 | 11 | 16 | 2026-01-17 |
|
||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 202 | 751 | 3 | 5 | 2026-01-07 |
|
||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 174 | 1906 | 7 | 18 | 2026-01-17 |
|
||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 134 | 1255 | 6 | 15 | 2026-01-17 |
|
||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 132 | 2269 | 8 | 10 | 2026-01-17 |
|
||||
| 7 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.2 | 63 | 1746 | 9 | 15 | 2026-01-17 |
|
||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 62 | 1262 | 10 | 3 | 2026-01-17 |
|
||||
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 58 | 605 | 3 | 5 | 2026-01-08 |
|
||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 41 | 654 | 5 | 0 | 2026-01-17 |
|
||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 22 | 392 | 2 | 1 | 2026-01-17 |
|
||||
| 12 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 16 | 430 | 4 | 1 | 2026-01-17 |
|
||||
| 13 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 14 | 340 | 4 | 1 | 2026-01-17 |
|
||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 254 | 2 | 1 | 2026-01-08 |
|
||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 46 | 0 | 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 | 1154 | 11 | 7 | 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
|
||||
@@ -421,4 +468,4 @@ Refer to this document for common event types and structures, and explore Open W
|
||||
|
||||
---
|
||||
|
||||
**Happy event-driven coding in Open WebUI! 🚀**
|
||||
**Happy event-driven coding in Open WebUI! 🚀**
|
||||
|
||||
@@ -1,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.1.3</span>
|
||||
<span class="version-badge">v1.2.2</span>
|
||||
|
||||
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
||||
|
||||
@@ -34,6 +34,12 @@ This is especially useful for:
|
||||
- :material-check-all: **Open WebUI v0.7.x Compatibility**: Dynamic DB session handling
|
||||
- :material-account-convert: **Improved Compatibility**: Summary role changed to `assistant`
|
||||
- :material-shield-check: **Enhanced Stability**: Resolved race conditions in state management
|
||||
- :material-ruler: **Preflight Context Check**: Validates context fit before sending
|
||||
- :material-format-align-justify: **Structure-Aware Trimming**: Preserves document structure
|
||||
- :material-content-cut: **Native Tool Output Trimming**: Trims verbose tool outputs (Note: Non-native tool outputs are not fully injected into context)
|
||||
- :material-chart-bar: **Detailed Token Logging**: Granular token breakdown
|
||||
- :material-account-search: **Smart Model Matching**: Inherit config from base models
|
||||
- :material-image-off: **Multimodal Support**: Images are preserved but tokens are **NOT** calculated
|
||||
|
||||
---
|
||||
|
||||
@@ -64,10 +70,14 @@ graph TD
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `token_threshold` | integer | `4000` | Trigger compression above this token count |
|
||||
| `preserve_recent` | integer | `5` | Number of recent messages to keep uncompressed |
|
||||
| `summary_model` | string | `"auto"` | Model to use for summarization |
|
||||
| `compression_ratio` | float | `0.3` | Target compression ratio |
|
||||
| `compression_threshold_tokens` | integer | `64000` | Trigger compression above this token count |
|
||||
| `max_context_tokens` | integer | `128000` | Hard limit for context |
|
||||
| `keep_first` | integer | `1` | Always keep the first N messages |
|
||||
| `keep_last` | integer | `6` | Always keep the last N messages |
|
||||
| `summary_model` | string | `None` | Model to use for summarization |
|
||||
| `summary_model_max_context` | integer | `0` | Max context tokens for summary model |
|
||||
| `max_summary_tokens` | integer | `16384` | Maximum tokens for the summary |
|
||||
| `enable_tool_output_trimming` | boolean | `false` | Enable trimming of large tool outputs |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Async Context Compression(异步上下文压缩)
|
||||
|
||||
<span class="category-badge filter">Filter</span>
|
||||
<span class="version-badge">v1.1.3</span>
|
||||
<span class="version-badge">v1.2.2</span>
|
||||
|
||||
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
||||
|
||||
@@ -34,6 +34,12 @@ Async Context Compression 过滤器通过以下方式帮助管理长对话的 to
|
||||
- :material-check-all: **Open WebUI v0.7.x 兼容性**:动态数据库会话处理
|
||||
- :material-account-convert: **兼容性提升**:摘要角色改为 `assistant`
|
||||
- :material-shield-check: **稳定性增强**:解决状态管理竞态条件
|
||||
- :material-ruler: **预检上下文检查**:发送前验证上下文是否超限
|
||||
- :material-format-align-justify: **结构感知裁剪**:保留文档结构的智能裁剪
|
||||
- :material-content-cut: **原生工具输出裁剪**:自动裁剪冗长的工具输出(注意:非原生工具调用输出不会完整注入上下文)
|
||||
- :material-chart-bar: **详细 Token 日志**:提供细粒度的 Token 统计
|
||||
- :material-account-search: **智能模型匹配**:自定义模型自动继承基础模型配置
|
||||
- :material-image-off: **多模态支持**:图片内容保留但 Token **不参与计算**
|
||||
|
||||
---
|
||||
|
||||
@@ -64,10 +70,14 @@ graph TD
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|---------|-------------|
|
||||
| `token_threshold` | integer | `4000` | 超过该 token 数触发压缩 |
|
||||
| `preserve_recent` | integer | `5` | 保留不压缩的最近消息数量 |
|
||||
| `summary_model` | string | `"auto"` | 用于摘要的模型 |
|
||||
| `compression_ratio` | float | `0.3` | 目标压缩比例 |
|
||||
| `compression_threshold_tokens` | integer | `64000` | 超过该 token 数触发压缩 |
|
||||
| `max_context_tokens` | integer | `128000` | 上下文硬性上限 |
|
||||
| `keep_first` | integer | `1` | 始终保留的前 N 条消息 |
|
||||
| `keep_last` | integer | `6` | 始终保留的后 N 条消息 |
|
||||
| `summary_model` | string | `None` | 用于摘要的模型 |
|
||||
| `summary_model_max_context` | integer | `0` | 摘要模型的最大上下文 Token 数 |
|
||||
| `max_summary_tokens` | integer | `16384` | 摘要的最大 token 数 |
|
||||
| `enable_tool_output_trimming` | boolean | `false` | 启用长工具输出裁剪 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
57
docs/plugins/filters/folder-memory.md
Normal file
57
docs/plugins/filters/folder-memory.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Extraction**: Analyzes chat history every N messages to extract project rules.
|
||||
- **Non-destructive Injection**: Updates only the specific "Project Rules" block in the system prompt, preserving other instructions.
|
||||
- **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).
|
||||
2. Enable the filter in your **Settings** -> **Filters**.
|
||||
3. (Optional) Configure the triggering threshold (default: every 10 messages).
|
||||
|
||||
## Configuration (Valves)
|
||||
|
||||
| Valve | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `PRIORITY` | `20` | Priority level for the filter operations. |
|
||||
| `MESSAGE_TRIGGER_COUNT` | `10` | The number of messages required to trigger a rule analysis. |
|
||||
| `MODEL_ID` | `""` | The model used to generate rules. If empty, uses the current chat model. |
|
||||
| `RULES_BLOCK_TITLE` | `## 📂 Project Rules` | The title displayed above the injected rules block. |
|
||||
| `SHOW_DEBUG_LOG` | `False` | Show detailed debug logs in the browser console. |
|
||||
| `UPDATE_ROOT_FOLDER` | `False` | If enabled, finds and updates the root folder rules instead of the current subfolder. |
|
||||
|
||||
## How It Works
|
||||
|
||||

|
||||
|
||||
1. **Trigger**: When a conversation reaches `MESSAGE_TRIGGER_COUNT` (e.g., 10, 20 messages).
|
||||
2. **Analysis**: The plugin sends the recent conversation + existing rules to the LLM.
|
||||
3. **Synthesis**: The LLM merges new insights with old rules, removing obsolete ones.
|
||||
4. **Update**: The new rule set replaces the `<!-- OWUI_PROJECT_RULES_START -->` block in the folder's system prompt.
|
||||
|
||||
## Roadmap
|
||||
|
||||
See [ROADMAP](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/ROADMAP.md) for future plans, including "Project Knowledge" collection.
|
||||
57
docs/plugins/filters/folder-memory.zh.md
Normal file
57
docs/plugins/filters/folder-memory.zh.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 文件夹记忆 (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 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
|
||||
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **自动提取**:每隔 N 条消息分析一次聊天记录,提取项目规则。
|
||||
- **无损注入**:仅更新系统提示词中的特定“项目规则”块,保留其他指令。
|
||||
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- **对话必须在文件夹内进行。** 此插件仅在聊天属于某个文件夹时触发(即您需要先在 OpenWebUI 中创建一个文件夹,并在其内部开始对话)。
|
||||
|
||||
## 安装指南
|
||||
|
||||
1. 将 `folder_memory.py` (或中文版 `folder_memory_cn.py`) 复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||
2. 在 **设置** -> **过滤器** 中启用该插件。
|
||||
3. (可选)配置触发阈值(默认:每 10 条消息)。
|
||||
|
||||
## 配置 (Valves)
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `PRIORITY` | `20` | 过滤器操作的优先级。 |
|
||||
| `MESSAGE_TRIGGER_COUNT` | `10` | 触发规则分析的消息数量阈值。 |
|
||||
| `MODEL_ID` | `""` | 用于生成规则的模型 ID。若为空,则使用当前对话模型。 |
|
||||
| `RULES_BLOCK_TITLE` | `## 📂 项目规则` | 显示在注入规则块上方的标题。 |
|
||||
| `SHOW_DEBUG_LOG` | `False` | 在浏览器控制台显示详细调试日志。 |
|
||||
| `UPDATE_ROOT_FOLDER` | `False` | 如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。 |
|
||||
|
||||
## 工作原理
|
||||
|
||||

|
||||
|
||||
1. **触发**:当对话达到 `MESSAGE_TRIGGER_COUNT`(例如 10、20 条消息)时。
|
||||
2. **分析**:插件将最近的对话 + 现有规则发送给 LLM。
|
||||
3. **综合**:LLM 将新见解与旧规则合并,移除过时的规则。
|
||||
4. **更新**:新的规则集替换文件夹系统提示词中的 `<!-- OWUI_PROJECT_RULES_START -->` 块。
|
||||
|
||||
## 路线图
|
||||
|
||||
查看 [ROADMAP](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/ROADMAP.md) 了解未来计划,包括“项目知识”收集功能。
|
||||
@@ -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.1.3
|
||||
**Version:** 1.2.2
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
||||
|
||||
@@ -36,7 +36,15 @@ Filters act as middleware in the message pipeline:
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](context-enhancement.md)
|
||||
|
||||
- :material-folder-refresh:{ .lg .middle } **Folder Memory**
|
||||
|
||||
---
|
||||
|
||||
Automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||
|
||||
**Version:** 0.1.0
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](folder-memory.md)
|
||||
|
||||
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||
|
||||
@@ -44,7 +52,7 @@ Filters act as middleware in the message pipeline:
|
||||
|
||||
Fixes common Markdown formatting issues in LLM outputs, including Mermaid syntax, code blocks, and LaTeX formulas.
|
||||
|
||||
**Version:** 1.2.3
|
||||
**Version:** 1.2.4
|
||||
|
||||
[:octicons-arrow-right-24: Documentation](markdown_normalizer.md)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Filter 充当消息管线中的中间件:
|
||||
|
||||
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
||||
|
||||
**版本:** 1.1.3
|
||||
**版本:** 1.2.2
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
||||
|
||||
@@ -36,7 +36,15 @@ Filter 充当消息管线中的中间件:
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
|
||||
|
||||
- :material-folder-refresh:{ .lg .middle } **Folder Memory**
|
||||
|
||||
---
|
||||
|
||||
自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
|
||||
**版本:** 0.1.0
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](folder-memory.zh.md)
|
||||
|
||||
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||
|
||||
@@ -44,7 +52,7 @@ Filter 充当消息管线中的中间件:
|
||||
|
||||
修复 LLM 输出中常见的 Markdown 格式问题,包括 Mermaid 语法、代码块和 LaTeX 公式。
|
||||
|
||||
**版本:** 1.2.3
|
||||
**版本:** 1.2.4
|
||||
|
||||
[:octicons-arrow-right-24: 查看文档](markdown_normalizer.zh.md)
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ A content normalizer filter for Open WebUI that fixes common Markdown formatting
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.2.4
|
||||
|
||||
* **Documentation Updates**: Synchronized version numbers across all documentation and code files.
|
||||
|
||||
### v1.2.3
|
||||
|
||||
* **List Marker Protection Enhancement**: Fixed a bug where list markers (`*`) followed by plain text and emphasis were having their spaces incorrectly stripped (e.g., `* U16 forward` became `*U16 forward`).
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.4
|
||||
|
||||
* **文档更新**: 同步了所有文档和代码文件的版本号。
|
||||
|
||||
### v1.2.3
|
||||
|
||||
* **列表标记保护增强**: 修复了列表标记 (`*`) 后跟普通文本和强调标记时,空格被错误剥离的问题(例如 `* U16 前锋` 变成 `*U16 前锋`)。
|
||||
|
||||
@@ -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 渲染:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 234 KiB |
@@ -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,26 +1,34 @@
|
||||
# Async Context Compression Filter
|
||||
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.1.3 | **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.
|
||||
- **Performance & Refactoring**: Optimized threshold parsing with caching, removed redundant code, and improved LLM response handling (JSONResponse support).
|
||||
- **Bug Fixes & Modernization**: Fixed `datetime` deprecation warnings, corrected type annotations, and replaced print statements with proper logging.
|
||||
|
||||
## What's new in 1.2.0
|
||||
|
||||
- **Preflight Context Check**: Before sending to the model, validates that total tokens fit within the context window. Automatically trims or drops oldest messages if exceeded.
|
||||
- **Structure-Aware Assistant Trimming**: When context exceeds the limit, long AI responses are intelligently collapsed while preserving their structure (headers H1-H6, first line, last line).
|
||||
- **Native Tool Output Trimming**: Detects and trims native tool outputs (`function_calling: "native"`), extracting only the final answer. Enable via `enable_tool_output_trimming`. **Note**: Non-native tool outputs are not fully injected into context.
|
||||
- **Consolidated Status Notifications**: Unified "Context Usage" and "Context Summary Updated" notifications with appended warnings (e.g., `| ⚠️ High Usage`) for clearer feedback.
|
||||
- **Context Usage Warning**: Emits a warning notification when context usage exceeds 90%.
|
||||
- **Enhanced Header Detection**: Optimized regex (`^#{1,6}\s+`) to avoid false positives like `#hashtag`.
|
||||
- **Detailed Token Logging**: Logs now show token breakdown for System, Head, Summary, and Tail sections with total.
|
||||
|
||||
## What's new in 1.1.3
|
||||
- **Improved Compatibility**: Changed summary injection role from `user` to `assistant` for better compatibility across different LLMs.
|
||||
- **Enhanced Stability**: Fixed a race condition in state management that could cause "inlet state not found" warnings in high-concurrency scenarios.
|
||||
- **Bug Fixes**: Corrected default model handling to prevent misleading logs when no model is specified.
|
||||
|
||||
## What's new in 1.1.2
|
||||
|
||||
- **Open WebUI v0.7.x Compatibility**: Resolved a critical database session binding error affecting Open WebUI v0.7.x users. The plugin now dynamically discovers the database engine and session context, ensuring compatibility across versions.
|
||||
- **Enhanced Error Reporting**: Errors during background summary generation are now reported via both the status bar and browser console.
|
||||
- **Robust Model Handling**: Improved handling of missing or invalid model IDs to prevent crashes.
|
||||
|
||||
## What's new in 1.1.1
|
||||
|
||||
- **Frontend Debugging**: Added `show_debug_log` option to print debug info to the browser console (F12).
|
||||
- **Optimized Compression**: Improved token calculation logic to prevent aggressive truncation of history, ensuring more context is retained.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -31,6 +39,12 @@ This filter reduces token consumption in long conversations through intelligent
|
||||
- ✅ Persistent storage via Open WebUI's shared database connection (PostgreSQL, SQLite, etc.).
|
||||
- ✅ Flexible retention policy to keep the first and last N messages.
|
||||
- ✅ Smart injection of historical summaries back into the context.
|
||||
- ✅ Structure-aware trimming that preserves document structure (headers, intro, conclusion).
|
||||
- ✅ Native tool output trimming for cleaner context when using function calling.
|
||||
- ✅ Real-time context usage monitoring with warning notifications (>90%).
|
||||
- ✅ Detailed token logging for precise debugging and optimization.
|
||||
- ✅ **Smart Model Matching**: Automatically inherits configuration from base models for custom presets.
|
||||
- ⚠ **Multimodal Support**: Images are preserved but their tokens are **NOT** calculated. Please adjust thresholds accordingly.
|
||||
|
||||
---
|
||||
|
||||
@@ -61,9 +75,11 @@ It is recommended to keep this filter early in the chain so it runs before filte
|
||||
| `keep_first` | `1` | Always keep the first N messages (protects system prompts). |
|
||||
| `keep_last` | `6` | Always keep the last N messages to preserve recent context. |
|
||||
| `summary_model` | `None` | Model for summaries. Strongly recommended to set a fast, economical model (e.g., `gemini-2.5-flash`, `deepseek-v3`). Falls back to the current chat model when empty. |
|
||||
| `max_summary_tokens` | `4000` | Maximum tokens for the generated summary. |
|
||||
| `summary_model_max_context` | `0` | Max context tokens for the summary model. If 0, falls back to `model_thresholds` or global `max_context_tokens`. |
|
||||
| `max_summary_tokens` | `16384` | Maximum tokens for the generated summary. |
|
||||
| `summary_temperature` | `0.3` | Randomness for summary generation. Lower is more deterministic. |
|
||||
| `model_thresholds` | `{}` | Per-model overrides for `compression_threshold_tokens` and `max_context_tokens` (useful for mixed models). |
|
||||
| `enable_tool_output_trimming` | `false` | When enabled and `function_calling: "native"` is active, trims verbose tool outputs to extract only the final answer. |
|
||||
| `debug_mode` | `true` | Log verbose debug info. Set to `false` in production. |
|
||||
| `show_debug_log` | `false` | Print debug logs to browser console (F12). Useful for frontend debugging. |
|
||||
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
# 异步上下文压缩过滤器
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.1.3 | **项目:** [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` 参数以独立控制摘要模型的上下文限制。
|
||||
- **性能优化与重构**: 重构了阈值解析逻辑并增加缓存,移除了冗余的处理代码,并增强了 LLM 响应处理(支持 JSONResponse)。
|
||||
- **稳定性改进**: 修复了 `datetime` 弃用警告,修正了类型注解,并将 print 语句替换为标准日志记录。
|
||||
|
||||
## 1.2.0 版本更新
|
||||
|
||||
- **预检上下文检查 (Preflight Context Check)**: 在发送给模型之前,验证总 Token 是否符合上下文窗口。如果超出,自动裁剪或丢弃最旧的消息。
|
||||
- **结构感知助手裁剪 (Structure-Aware Assistant Trimming)**: 当上下文超出限制时,智能折叠过长的 AI 回复,同时保留其结构(标题 H1-H6、首行、尾行)。
|
||||
- **原生工具输出裁剪 (Native Tool Output Trimming)**: 检测并裁剪原生工具输出 (`function_calling: "native"`),仅提取最终答案。通过 `enable_tool_output_trimming` 启用。**注意**:非原生工具调用输出不会完整注入上下文。
|
||||
- **统一状态通知**: 统一了“上下文使用情况”和“上下文摘要更新”的通知,并附加警告(例如 `| ⚠️ 高负载`),反馈更清晰。
|
||||
- **上下文使用警告**: 当上下文使用率超过 90% 时发出警告通知。
|
||||
- **增强的标题检测**: 优化了正则表达式 (`^#{1,6}\s+`) 以避免误判(如 `#hashtag`)。
|
||||
- **详细 Token 日志**: 日志现在显示 System、Head、Summary 和 Tail 部分的 Token 细分及总计。
|
||||
|
||||
## 1.1.3 版本更新
|
||||
- **兼容性提升**: 将摘要注入角色从 `user` 改为 `assistant`,以提高在不同 LLM 之间的兼容性。
|
||||
- **稳定性增强**: 修复了状态管理中的竞态条件,解决了高并发场景下可能出现的“无法获取 inlet 状态”警告。
|
||||
- **Bug 修复**: 修正了默认模型处理逻辑,防止在未指定模型时产生误导性日志。
|
||||
|
||||
## 1.1.2 版本更新
|
||||
|
||||
- **Open WebUI v0.7.x 兼容性**: 修复了影响 Open WebUI v0.7.x 用户的严重数据库会话绑定错误。插件现在动态发现数据库引擎和会话上下文,确保跨版本兼容性。
|
||||
- **增强错误报告**: 后台摘要生成过程中的错误现在会通过状态栏和浏览器控制台同时报告。
|
||||
- **健壮的模型处理**: 改进了对缺失或无效模型 ID 的处理,防止程序崩溃。
|
||||
|
||||
## 1.1.1 版本更新
|
||||
|
||||
- **前端调试**: 新增 `show_debug_log` 选项,支持在浏览器控制台 (F12) 打印调试信息。
|
||||
- **压缩优化**: 优化 Token 计算逻辑,防止历史记录被过度截断,保留更多上下文。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -33,6 +41,12 @@
|
||||
- ✅ **持久化存储**: 复用 Open WebUI 共享数据库连接,自动支持 PostgreSQL/SQLite 等。
|
||||
- ✅ **灵活保留策略**: 可配置保留对话头部和尾部消息,确保关键信息连贯。
|
||||
- ✅ **智能注入**: 将历史摘要智能注入到新上下文中。
|
||||
- ✅ **结构感知裁剪**: 智能折叠过长消息,保留文档骨架(标题、首尾)。
|
||||
- ✅ **原生工具输出裁剪**: 支持裁剪冗长的工具调用输出。
|
||||
- ✅ **实时监控**: 实时监控上下文使用情况,超过 90% 发出警告。
|
||||
- ✅ **详细日志**: 提供精确的 Token 统计日志,便于调试。
|
||||
- ✅ **智能模型匹配**: 自定义模型自动继承基础模型的阈值配置。
|
||||
- ⚠ **多模态支持**: 图片内容会被保留,但其 Token **不参与计算**。请相应调整阈值。
|
||||
|
||||
详细的工作原理和流程请参考 [工作流程指南](WORKFLOW_GUIDE_CN.md)。
|
||||
|
||||
@@ -74,6 +88,7 @@
|
||||
| 参数 | 默认值 | 描述 |
|
||||
| :-------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `summary_model` | `None` | 用于生成摘要的模型 ID。**强烈建议**配置快速、经济、上下文窗口大的模型(如 `gemini-2.5-flash`、`deepseek-v3`)。留空则尝试复用当前对话模型。 |
|
||||
| `summary_model_max_context` | `0` | 摘要模型的最大上下文 Token 数。如果为 0,则回退到 `model_thresholds` 或全局 `max_context_tokens`。 |
|
||||
| `max_summary_tokens` | `16384` | 生成摘要时允许的最大 Token 数。 |
|
||||
| `summary_temperature` | `0.1` | 控制摘要生成的随机性,较低的值结果更稳定。 |
|
||||
|
||||
@@ -100,15 +115,12 @@
|
||||
}
|
||||
```
|
||||
|
||||
#### `debug_mode`
|
||||
|
||||
- **默认值**: `true`
|
||||
- **描述**: 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。
|
||||
|
||||
#### `show_debug_log`
|
||||
|
||||
- **默认值**: `false`
|
||||
- **描述**: 是否在浏览器控制台 (F12) 打印调试日志。便于前端调试。
|
||||
| 参数 | 默认值 | 描述 |
|
||||
| :----------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enable_tool_output_trimming` | `false` | 启用时,若 `function_calling: "native"` 激活,将裁剪冗长的工具输出以仅提取最终答案。 |
|
||||
| `debug_mode` | `true` | 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。 |
|
||||
| `show_debug_log` | `false` | 是否在浏览器控制台 (F12) 打印调试日志。便于前端调试。 |
|
||||
| `show_token_usage_status` | `true` | 是否在对话结束时显示 Token 使用情况的状态通知。 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
60
plugins/filters/folder-memory/README.md
Normal file
60
plugins/filters/folder-memory/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Automatic Extraction**: Analyzes chat history every N messages to extract project rules.
|
||||
- **Non-destructive Injection**: Updates only the specific "Project Rules" block in the system prompt, preserving other instructions.
|
||||
- **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).
|
||||
2. Enable the filter in your **Settings** -> **Filters**.
|
||||
3. (Optional) Configure the triggering threshold (default: every 10 messages).
|
||||
|
||||
## ⚙️ Configuration (Valves)
|
||||
|
||||
| Valve | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `PRIORITY` | `20` | Priority level for the filter operations. |
|
||||
| `MESSAGE_TRIGGER_COUNT` | `10` | The number of messages required to trigger a rule analysis. |
|
||||
| `MODEL_ID` | `""` | The model used to generate rules. If empty, uses the current chat model. |
|
||||
| `RULES_BLOCK_TITLE` | `## 📂 Project Rules` | The title displayed above the injected rules block. |
|
||||
| `SHOW_DEBUG_LOG` | `False` | Show detailed debug logs in the browser console. |
|
||||
| `UPDATE_ROOT_FOLDER` | `False` | If enabled, finds and updates the root folder rules instead of the current subfolder. |
|
||||
|
||||
## 🛠️ How It Works
|
||||
|
||||

|
||||
|
||||
1. **Trigger**: When a conversation reaches `MESSAGE_TRIGGER_COUNT` (e.g., 10, 20 messages).
|
||||
2. **Analysis**: The plugin sends the recent conversation + existing rules to the LLM.
|
||||
3. **Synthesis**: The LLM merges new insights with old rules, removing obsolete ones.
|
||||
4. **Update**: The new rule set replaces the `<!-- OWUI_PROJECT_RULES_START -->` block in the folder's system prompt.
|
||||
|
||||
## ⚠️ Notes
|
||||
|
||||
- This plugin modifies the `system_prompt` of your folders.
|
||||
- It uses a specific marker `<!-- OWUI_PROJECT_RULES_START -->` to locate its content. Do not manually remove these markers if you want the plugin to continue managing that section.
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
See [ROADMAP.md](./ROADMAP.md) for future plans, including "Project Knowledge" collection.
|
||||
62
plugins/filters/folder-memory/README_CN.md
Normal file
62
plugins/filters/folder-memory/README_CN.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 文件夹记忆 (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 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||
|
||||
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **自动提取**:每隔 N 条消息分析一次聊天记录,提取项目规则。
|
||||
- **无损注入**:仅更新系统提示词中的特定“项目规则”块,保留其他指令。
|
||||
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||
|
||||
## ⚠️ 前置条件
|
||||
|
||||
- **对话必须在文件夹内进行。** 此插件仅在聊天属于某个文件夹时触发(即您需要先在 OpenWebUI 中创建一个文件夹,并在其内部开始对话)。
|
||||
|
||||
## 📦 安装指南
|
||||
|
||||
1. 将 `folder_memory.py` (或中文版 `folder_memory_cn.py`) 复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||
2. 在 **设置** -> **过滤器** 中启用该插件。
|
||||
3. (可选)配置触发阈值(默认:每 10 条消息)。
|
||||
|
||||
## ⚙️ 配置 (Valves)
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| `PRIORITY` | `20` | 过滤器操作的优先级。 |
|
||||
| `MESSAGE_TRIGGER_COUNT` | `10` | 触发规则分析的消息数量阈值。 |
|
||||
| `MODEL_ID` | `""` | 用于生成规则的模型 ID。若为空,则使用当前对话模型。 |
|
||||
| `RULES_BLOCK_TITLE` | `## 📂 项目规则` | 显示在注入规则块上方的标题。 |
|
||||
| `SHOW_DEBUG_LOG` | `False` | 在浏览器控制台显示详细调试日志。 |
|
||||
| `UPDATE_ROOT_FOLDER` | `False` | 如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。 |
|
||||
|
||||
## 🛠️ 工作原理
|
||||
|
||||

|
||||
|
||||
1. **触发**:当对话达到 `MESSAGE_TRIGGER_COUNT`(例如 10、20 条消息)时。
|
||||
2. **分析**:插件将最近的对话 + 现有规则发送给 LLM。
|
||||
3. **综合**:LLM 将新见解与旧规则合并,移除过时的规则。
|
||||
4. **更新**:新的规则集替换文件夹系统提示词中的 `<!-- OWUI_PROJECT_RULES_START -->` 块。
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- 此插件会修改文件夹的 `system_prompt`。
|
||||
- 它使用特定标记 `<!-- OWUI_PROJECT_RULES_START -->` 来定位内容。如果您希望插件继续管理该部分,请勿手动删除这些标记。
|
||||
|
||||
## 🗺️ 路线图
|
||||
|
||||
查看 [ROADMAP.md](./ROADMAP.md) 了解未来计划,包括“项目知识”收集功能。
|
||||
10
plugins/filters/folder-memory/ROADMAP.md
Normal file
10
plugins/filters/folder-memory/ROADMAP.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Roadmap
|
||||
|
||||
## Future Features
|
||||
|
||||
### 🧠 Project Knowledge (Planned)
|
||||
In future versions, we plan to introduce "Project Knowledge" collection. Unlike "Rules" which are strict instructions, "Knowledge" will capture reusable information, consensus, and context that helps the LLM understand the project better.
|
||||
|
||||
- **Knowledge Extraction**: Automatically extract reusable knowledge (terminology, style guides, business logic) from conversations.
|
||||
- **Long-term Memory**: Use the entire folder's chat history as a corpus for knowledge generation.
|
||||
- **Context Injection**: Inject summarized knowledge into the system prompt alongside rules.
|
||||
BIN
plugins/filters/folder-memory/folder-memory-demo.png
Normal file
BIN
plugins/filters/folder-memory/folder-memory-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 459 KiB |
483
plugins/filters/folder-memory/folder_memory.py
Normal file
483
plugins/filters/folder-memory/folder_memory.py
Normal file
@@ -0,0 +1,483 @@
|
||||
"""
|
||||
title: 📂 Folder Memory
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.1.0
|
||||
description: Automatically extracts project rules from conversations and injects them into the folder's system prompt.
|
||||
requirements:
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, List
|
||||
from fastapi import Request
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.models.folders import Folders, FolderUpdateForm
|
||||
from open_webui.models.chats import Chats
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Markers for rule injection
|
||||
RULES_BLOCK_START = "<!-- OWUI_PROJECT_RULES_START -->"
|
||||
RULES_BLOCK_END = "<!-- OWUI_PROJECT_RULES_END -->"
|
||||
|
||||
# System Prompt for Rule Generation
|
||||
SYSTEM_PROMPT_RULE_GENERATOR = """
|
||||
You are a project rule extractor. Your task is to extract "Project Rules" from the conversation and merge them with existing rules.
|
||||
|
||||
### Input
|
||||
1. **Existing Rules**: Current rules in the folder system prompt.
|
||||
2. **Conversation**: Recent chat history.
|
||||
|
||||
### Goal
|
||||
Synthesize a concise list of rules that apply to this project/folder.
|
||||
- **Remove** rules that are no longer relevant or were one-off instructions.
|
||||
- **Add** new consistent requirements found in the conversation.
|
||||
- **Merge** similar rules.
|
||||
- **Format**: Concise bullet points (Markdown).
|
||||
|
||||
### Output Format
|
||||
ONLY output the rules list as Markdown bullet points. Do not include any intro/outro text.
|
||||
Example:
|
||||
- Always use Python 3.11 for type hinting.
|
||||
- Docstrings must follow Google style.
|
||||
- Commit messages should be in English.
|
||||
"""
|
||||
|
||||
|
||||
class Filter:
|
||||
class Valves(BaseModel):
|
||||
PRIORITY: int = Field(
|
||||
default=20, description="Priority level for the filter operations."
|
||||
)
|
||||
SHOW_DEBUG_LOG: bool = Field(
|
||||
default=False, description="Show debug logs in console."
|
||||
)
|
||||
MESSAGE_TRIGGER_COUNT: int = Field(
|
||||
default=10, description="Analyze rules after every N messages in a chat."
|
||||
)
|
||||
MODEL_ID: str = Field(
|
||||
default="",
|
||||
description="Model used for rule extraction. If empty, uses the current chat model.",
|
||||
)
|
||||
RULES_BLOCK_TITLE: str = Field(
|
||||
default="## 📂 Project Rules",
|
||||
description="Title displayed above the rules block.",
|
||||
)
|
||||
UPDATE_ROOT_FOLDER: bool = Field(
|
||||
default=False,
|
||||
description="If enabled, finds and updates the root folder rules instead of the current subfolder.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
# ==================== Helper Methods ====================
|
||||
|
||||
def _get_user_context(self, __user__: Optional[dict]) -> Dict[str, str]:
|
||||
"""Safely extracts user context information."""
|
||||
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", ""),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", "en-US"),
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
self, body: dict, __metadata__: Optional[dict] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Unified extraction of chat context information (chat_id, message_id)."""
|
||||
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_debug_log(self, __event_emitter__, title: str, data: dict):
|
||||
if self.valves.SHOW_DEBUG_LOG and __event_emitter__:
|
||||
try:
|
||||
# Flat log format as requested
|
||||
js_code = f"""
|
||||
console.log("[Folder Memory] {title}", {json.dumps(data, ensure_ascii=False)});
|
||||
"""
|
||||
await __event_emitter__({"type": "execute", "data": {"code": js_code}})
|
||||
except Exception as e:
|
||||
logger.error(f"Error emitting log: {e}")
|
||||
|
||||
async def _emit_status(
|
||||
self, __event_emitter__, description: str, done: bool = False
|
||||
):
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(
|
||||
{"type": "status", "data": {"description": description, "done": done}}
|
||||
)
|
||||
|
||||
def _get_folder_id(self, body: dict) -> Optional[str]:
|
||||
# 1. Try retrieving folder_id specifically from metadata
|
||||
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||
if "folder_id" in body["metadata"]:
|
||||
return body["metadata"]["folder_id"]
|
||||
|
||||
# 2. Check regular body chat object if available
|
||||
if "chat" in body and isinstance(body["chat"], dict):
|
||||
if "folder_id" in body["chat"]:
|
||||
return body["chat"]["folder_id"]
|
||||
|
||||
# 3. Try fallback via Chat ID (Most reliable)
|
||||
chat_id = body.get("chat_id")
|
||||
if not chat_id:
|
||||
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||
chat_id = body["metadata"].get("chat_id")
|
||||
|
||||
if chat_id:
|
||||
try:
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
if chat and chat.folder_id:
|
||||
return chat.folder_id
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch chat {chat_id}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _extract_existing_rules(self, system_prompt: str) -> str:
|
||||
pattern = re.compile(
|
||||
re.escape(RULES_BLOCK_START) + r"([\s\S]*?)" + re.escape(RULES_BLOCK_END)
|
||||
)
|
||||
match = pattern.search(system_prompt)
|
||||
if match:
|
||||
# Remove title if it's inside the block
|
||||
content = match.group(1).strip()
|
||||
# Simple cleanup of the title if user formatted it inside
|
||||
title_pat = re.compile(r"^#+\s+.*$", re.MULTILINE)
|
||||
return title_pat.sub("", content).strip()
|
||||
return ""
|
||||
|
||||
def _inject_rules(self, system_prompt: str, new_rules: str, title: str) -> str:
|
||||
new_block_content = f"\n{title}\n\n{new_rules}\n"
|
||||
new_block = f"{RULES_BLOCK_START}{new_block_content}{RULES_BLOCK_END}"
|
||||
|
||||
system_prompt = system_prompt or ""
|
||||
pattern = re.compile(
|
||||
re.escape(RULES_BLOCK_START) + r"[\s\S]*?" + re.escape(RULES_BLOCK_END)
|
||||
)
|
||||
|
||||
if pattern.search(system_prompt):
|
||||
return pattern.sub(new_block, system_prompt).strip()
|
||||
else:
|
||||
# Append if not found
|
||||
if system_prompt:
|
||||
return f"{system_prompt}\n\n{new_block}"
|
||||
else:
|
||||
return new_block
|
||||
|
||||
async def _generate_new_rules(
|
||||
self,
|
||||
current_rules: str,
|
||||
messages: List[Dict],
|
||||
user_id: str,
|
||||
__request__: Request,
|
||||
) -> str:
|
||||
# Prepare context
|
||||
conversation_text = "\n".join(
|
||||
[
|
||||
f"{msg['role'].upper()}: {msg['content']}"
|
||||
for msg in messages[-20:] # Analyze last 20 messages context
|
||||
]
|
||||
)
|
||||
|
||||
prompt = f"""
|
||||
Existing Rules:
|
||||
{current_rules if current_rules else "None"}
|
||||
|
||||
Conversation Excerpt:
|
||||
{conversation_text}
|
||||
|
||||
Please output the updated Project Rules:
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"model": self.valves.MODEL_ID,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT_RULE_GENERATOR},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
try:
|
||||
# We need a user object for permission checks in generate_chat_completion
|
||||
user = Users.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return current_rules
|
||||
|
||||
completion = await generate_chat_completion(__request__, payload, user)
|
||||
if "choices" in completion and len(completion["choices"]) > 0:
|
||||
content = completion["choices"][0]["message"]["content"].strip()
|
||||
# Basic validation: ensure it looks like a list
|
||||
if (
|
||||
content.startswith("-")
|
||||
or content.startswith("*")
|
||||
or content.startswith("1.")
|
||||
):
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"Rule generation failed: {e}")
|
||||
|
||||
return current_rules
|
||||
|
||||
async def _process_rules_update(
|
||||
self,
|
||||
folder_id: str,
|
||||
body: dict,
|
||||
user_id: str,
|
||||
__request__: Request,
|
||||
__event_emitter__,
|
||||
):
|
||||
try:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Start Processing",
|
||||
{"step": "start", "initial_folder_id": folder_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
# 1. Fetch Folder Data (ORM)
|
||||
initial_folder = Folders.get_folder_by_id_and_user_id(folder_id, user_id)
|
||||
if not initial_folder:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Error: Initial folder not found",
|
||||
{
|
||||
"step": "fetch_initial_folder",
|
||||
"initial_folder_id": folder_id,
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
# Subfolder handling logic
|
||||
target_folder = initial_folder
|
||||
if self.valves.UPDATE_ROOT_FOLDER:
|
||||
# Traverse up until a folder with no parent_id is found
|
||||
while target_folder and getattr(target_folder, "parent_id", None):
|
||||
try:
|
||||
parent = Folders.get_folder_by_id_and_user_id(
|
||||
target_folder.parent_id, user_id
|
||||
)
|
||||
if parent:
|
||||
target_folder = parent
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Warning: Failed to traverse parent folder",
|
||||
{"step": "traverse_root", "error": str(e)},
|
||||
)
|
||||
break
|
||||
|
||||
target_folder_id = target_folder.id
|
||||
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Target Folder Resolved",
|
||||
{
|
||||
"step": "target_resolved",
|
||||
"target_folder_id": target_folder_id,
|
||||
"target_folder_name": target_folder.name,
|
||||
"is_root_update": target_folder_id != folder_id,
|
||||
},
|
||||
)
|
||||
|
||||
existing_data = target_folder.data if target_folder.data else {}
|
||||
existing_sys_prompt = existing_data.get("system_prompt", "")
|
||||
|
||||
# 2. Extract Existing Rules
|
||||
current_rules_content = self._extract_existing_rules(existing_sys_prompt)
|
||||
|
||||
# 3. Generate New Rules
|
||||
await self._emit_status(
|
||||
__event_emitter__, "Analyzing project rules...", done=False
|
||||
)
|
||||
|
||||
messages = body.get("messages", [])
|
||||
new_rules_content = await self._generate_new_rules(
|
||||
current_rules_content, messages, user_id, __request__
|
||||
)
|
||||
|
||||
rules_changed = new_rules_content != current_rules_content
|
||||
|
||||
# 4. If no change, skip
|
||||
if not rules_changed:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"No Changes",
|
||||
{
|
||||
"step": "check_changes",
|
||||
"reason": "content_identical_or_generation_failed",
|
||||
},
|
||||
)
|
||||
await self._emit_status(
|
||||
__event_emitter__,
|
||||
"Rule analysis complete: No new content.",
|
||||
done=True,
|
||||
)
|
||||
return
|
||||
|
||||
# 5. Inject Rules into System Prompt
|
||||
updated_sys_prompt = existing_sys_prompt
|
||||
if rules_changed:
|
||||
updated_sys_prompt = self._inject_rules(
|
||||
updated_sys_prompt,
|
||||
new_rules_content,
|
||||
self.valves.RULES_BLOCK_TITLE,
|
||||
)
|
||||
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Ready to Update DB",
|
||||
{"step": "pre_db_update", "target_folder_id": target_folder_id},
|
||||
)
|
||||
|
||||
# 6. Update Folder (ORM) - Only update 'data' field
|
||||
existing_data["system_prompt"] = updated_sys_prompt
|
||||
|
||||
updated_folder = Folders.update_folder_by_id_and_user_id(
|
||||
target_folder_id,
|
||||
user_id,
|
||||
FolderUpdateForm(data=existing_data),
|
||||
)
|
||||
|
||||
if not updated_folder:
|
||||
raise Exception("Update folder failed (ORM returned None)")
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "Rule analysis complete: Rules updated.", done=True
|
||||
)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Rule Generation Process & Change Details",
|
||||
{
|
||||
"step": "success",
|
||||
"folder_id": target_folder_id,
|
||||
"target_is_root": target_folder_id != folder_id,
|
||||
"model_used": self.valves.MODEL_ID,
|
||||
"analyzed_messages_count": len(messages),
|
||||
"old_rules_length": len(current_rules_content),
|
||||
"new_rules_length": len(new_rules_content),
|
||||
"changes_digest": {
|
||||
"old_rules_preview": (
|
||||
current_rules_content[:100] + "..."
|
||||
if current_rules_content
|
||||
else "None"
|
||||
),
|
||||
"new_rules_preview": (
|
||||
new_rules_content[:100] + "..."
|
||||
if new_rules_content
|
||||
else "None"
|
||||
),
|
||||
},
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Async rule processing error: {e}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "Failed to update rules.", done=True
|
||||
)
|
||||
# Emit error to console for debugging
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Execution Error",
|
||||
{"error": str(e), "folder_id": folder_id},
|
||||
)
|
||||
|
||||
# ==================== Filter Hooks ====================
|
||||
|
||||
async def inlet(
|
||||
self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None
|
||||
) -> dict:
|
||||
return body
|
||||
|
||||
async def outlet(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[dict] = None,
|
||||
__event_emitter__=None,
|
||||
__request__: Optional[Request] = None,
|
||||
) -> dict:
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
chat_ctx = self._get_chat_context(body)
|
||||
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# Trigger logic: Message Count threshold
|
||||
if len(messages) % self.valves.MESSAGE_TRIGGER_COUNT != 0:
|
||||
return body
|
||||
|
||||
folder_id = self._get_folder_id(body)
|
||||
if not folder_id:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"Skipping Analysis",
|
||||
{
|
||||
"reason": "Chat does not belong to any folder",
|
||||
"chat_id": chat_ctx.get("chat_id"),
|
||||
},
|
||||
)
|
||||
return body
|
||||
|
||||
# User Info
|
||||
user_id = user_ctx.get("user_id")
|
||||
if not user_id:
|
||||
return body
|
||||
|
||||
# Async Task
|
||||
if self.valves.MODEL_ID == "":
|
||||
self.valves.MODEL_ID = body.get("model", "")
|
||||
|
||||
asyncio.create_task(
|
||||
self._process_rules_update(
|
||||
folder_id, body, user_id, __request__, __event_emitter__
|
||||
)
|
||||
)
|
||||
|
||||
return body
|
||||
470
plugins/filters/folder-memory/folder_memory_cn.py
Normal file
470
plugins/filters/folder-memory/folder_memory_cn.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""
|
||||
title: 📂 文件夹记忆 (Folder Memory)
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 0.1.0
|
||||
description: 自动从对话中提取项目规则,并将其注入到文件夹的系统提示词中。
|
||||
requirements:
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, List
|
||||
from fastapi import Request
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from open_webui.utils.chat import generate_chat_completion
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.models.folders import Folders, FolderUpdateForm
|
||||
from open_webui.models.chats import Chats
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 规则注入标记
|
||||
RULES_BLOCK_START = "<!-- OWUI_PROJECT_RULES_START -->"
|
||||
RULES_BLOCK_END = "<!-- OWUI_PROJECT_RULES_END -->"
|
||||
|
||||
# 规则生成系统提示词
|
||||
SYSTEM_PROMPT_RULE_GENERATOR = """
|
||||
你是一个项目规则提取器。你的任务是从对话中提取“项目规则”,并与现有规则合并。
|
||||
|
||||
### 输入
|
||||
1. **现有规则 (Existing Rules)**:当前文件夹系统提示词中的规则。
|
||||
2. **对话片段 (Conversation)**:最近的聊天记录。
|
||||
|
||||
### 目标
|
||||
综合生成一份适用于当前项目/文件夹的简洁规则列表。
|
||||
- **移除** 不再相关或仅是一次性指令的规则。
|
||||
- **添加** 对话中发现的新的、一致性的要求。
|
||||
- **合并** 相似的规则。
|
||||
- **格式**:简洁的 Markdown 项目符号列表。
|
||||
|
||||
### 输出格式
|
||||
仅输出 Markdown 项目符号列表形式的规则。不要包含任何开头或结尾的说明文字。
|
||||
示例:
|
||||
- 始终使用 Python 3.11 进行类型提示。
|
||||
- 文档字符串必须遵循 Google 风格。
|
||||
- 提交信息必须使用英文。
|
||||
"""
|
||||
|
||||
|
||||
class Filter:
|
||||
class Valves(BaseModel):
|
||||
PRIORITY: int = Field(default=20, description="过滤器操作的优先级。")
|
||||
SHOW_DEBUG_LOG: bool = Field(
|
||||
default=False, description="在控制台显示调试日志。"
|
||||
)
|
||||
MESSAGE_TRIGGER_COUNT: int = Field(
|
||||
default=10, description="每隔 N 条消息分析一次规则。"
|
||||
)
|
||||
MODEL_ID: str = Field(
|
||||
default="", description="用于提取规则的模型 ID。为空则使用当前对话模型。"
|
||||
)
|
||||
RULES_BLOCK_TITLE: str = Field(
|
||||
default="## 📂 项目规则", description="显示在规则块上方的标题。"
|
||||
)
|
||||
UPDATE_ROOT_FOLDER: bool = Field(
|
||||
default=False,
|
||||
description="如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
|
||||
# ==================== 辅助方法 ====================
|
||||
|
||||
def _get_user_context(self, __user__: Optional[dict]) -> 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", ""),
|
||||
"user_name": user_data.get("name", "User"),
|
||||
"user_language": user_data.get("language", "zh-CN"),
|
||||
}
|
||||
|
||||
def _get_chat_context(
|
||||
self, body: dict, __metadata__: Optional[dict] = None
|
||||
) -> Dict[str, str]:
|
||||
"""统一提取聊天上下文信息 (chat_id, message_id)。"""
|
||||
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_debug_log(self, __event_emitter__, title: str, data: dict):
|
||||
if self.valves.SHOW_DEBUG_LOG and __event_emitter__:
|
||||
try:
|
||||
# 按照用户要求的格式输出展平的日志
|
||||
js_code = f"""
|
||||
console.log("[Folder Memory] {title}", {json.dumps(data, ensure_ascii=False)});
|
||||
"""
|
||||
await __event_emitter__({"type": "execute", "data": {"code": js_code}})
|
||||
except Exception as e:
|
||||
logger.error(f"发出日志错误: {e}")
|
||||
|
||||
async def _emit_status(
|
||||
self, __event_emitter__, description: str, done: bool = False
|
||||
):
|
||||
if __event_emitter__:
|
||||
await __event_emitter__(
|
||||
{"type": "status", "data": {"description": description, "done": done}}
|
||||
)
|
||||
|
||||
def _get_folder_id(self, body: dict) -> Optional[str]:
|
||||
# 1. 尝试从 metadata 获取 folder_id
|
||||
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||
if "folder_id" in body["metadata"]:
|
||||
return body["metadata"]["folder_id"]
|
||||
|
||||
# 2. 检查 chat 对象
|
||||
if "chat" in body and isinstance(body["chat"], dict):
|
||||
if "folder_id" in body["chat"]:
|
||||
return body["chat"]["folder_id"]
|
||||
|
||||
# 3. 尝试通过 Chat ID 查找 (最可靠的方法)
|
||||
chat_id = body.get("chat_id")
|
||||
if not chat_id:
|
||||
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||
chat_id = body["metadata"].get("chat_id")
|
||||
|
||||
if chat_id:
|
||||
try:
|
||||
chat = Chats.get_chat_by_id(chat_id)
|
||||
if chat and chat.folder_id:
|
||||
return chat.folder_id
|
||||
except Exception as e:
|
||||
logger.error(f"获取聊天信息失败 chat_id={chat_id}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _extract_existing_rules(self, system_prompt: str) -> str:
|
||||
pattern = re.compile(
|
||||
re.escape(RULES_BLOCK_START) + r"([\s\S]*?)" + re.escape(RULES_BLOCK_END)
|
||||
)
|
||||
match = pattern.search(system_prompt)
|
||||
if match:
|
||||
# 如果标题在块内,将其移除以便纯净合并
|
||||
content = match.group(1).strip()
|
||||
title_pat = re.compile(r"^#+\s+.*$", re.MULTILINE)
|
||||
return title_pat.sub("", content).strip()
|
||||
return ""
|
||||
|
||||
def _inject_rules(self, system_prompt: str, new_rules: str, title: str) -> str:
|
||||
new_block_content = f"\n{title}\n\n{new_rules}\n"
|
||||
new_block = f"{RULES_BLOCK_START}{new_block_content}{RULES_BLOCK_END}"
|
||||
|
||||
system_prompt = system_prompt or ""
|
||||
pattern = re.compile(
|
||||
re.escape(RULES_BLOCK_START) + r"[\s\S]*?" + re.escape(RULES_BLOCK_END)
|
||||
)
|
||||
|
||||
if pattern.search(system_prompt):
|
||||
# 替换现有块
|
||||
return pattern.sub(new_block, system_prompt).strip()
|
||||
else:
|
||||
# 追加到末尾
|
||||
if system_prompt:
|
||||
return f"{system_prompt}\n\n{new_block}"
|
||||
else:
|
||||
return new_block
|
||||
|
||||
async def _generate_new_rules(
|
||||
self,
|
||||
current_rules: str,
|
||||
messages: List[Dict],
|
||||
user_id: str,
|
||||
__request__: Request,
|
||||
) -> str:
|
||||
# 准备上下文
|
||||
conversation_text = "\n".join(
|
||||
[
|
||||
f"{msg['role'].upper()}: {msg['content']}"
|
||||
for msg in messages[-20:] # 分析最近 20 条消息上下文
|
||||
]
|
||||
)
|
||||
|
||||
prompt = f"""
|
||||
Existing Rules (现有规则):
|
||||
{current_rules if current_rules else "无"}
|
||||
|
||||
Conversation Excerpt (对话片段):
|
||||
{conversation_text}
|
||||
|
||||
Please output the updated Project Rules (请输出更新后的项目规则):
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"model": self.valves.MODEL_ID,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT_RULE_GENERATOR},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
try:
|
||||
# 需要用户对象进行权限检查
|
||||
user = Users.get_user_by_id(user_id)
|
||||
if not user:
|
||||
return current_rules
|
||||
|
||||
completion = await generate_chat_completion(__request__, payload, user)
|
||||
if "choices" in completion and len(completion["choices"]) > 0:
|
||||
content = completion["choices"][0]["message"]["content"].strip()
|
||||
# 简单验证:确保看起来像个列表
|
||||
if (
|
||||
content.startswith("-")
|
||||
or content.startswith("*")
|
||||
or content.startswith("1.")
|
||||
):
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"规则生成失败: {e}")
|
||||
|
||||
return current_rules
|
||||
|
||||
async def _process_rules_update(
|
||||
self,
|
||||
folder_id: str,
|
||||
body: dict,
|
||||
user_id: str,
|
||||
__request__: Request,
|
||||
__event_emitter__,
|
||||
):
|
||||
try:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"开始处理",
|
||||
{"step": "start", "initial_folder_id": folder_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
# 1. 获取文件夹数据 (ORM)
|
||||
initial_folder = Folders.get_folder_by_id_and_user_id(folder_id, user_id)
|
||||
if not initial_folder:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"错误:未找到初始文件夹",
|
||||
{
|
||||
"step": "fetch_initial_folder",
|
||||
"initial_folder_id": folder_id,
|
||||
"user_id": user_id,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
# 处理子文件夹逻辑:决定是更新当前文件夹还是根文件夹
|
||||
target_folder = initial_folder
|
||||
if self.valves.UPDATE_ROOT_FOLDER:
|
||||
# 向上遍历直到找到没有 parent_id 的根文件夹
|
||||
while target_folder and getattr(target_folder, "parent_id", None):
|
||||
try:
|
||||
parent = Folders.get_folder_by_id_and_user_id(
|
||||
target_folder.parent_id, user_id
|
||||
)
|
||||
if parent:
|
||||
target_folder = parent
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"警告:向上查找父文件夹失败",
|
||||
{"step": "traverse_root", "error": str(e)},
|
||||
)
|
||||
break
|
||||
|
||||
target_folder_id = target_folder.id
|
||||
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"定目标文件夹",
|
||||
{
|
||||
"step": "target_resolved",
|
||||
"target_folder_id": target_folder_id,
|
||||
"target_folder_name": target_folder.name,
|
||||
"is_root_update": target_folder_id != folder_id,
|
||||
},
|
||||
)
|
||||
|
||||
existing_data = target_folder.data if target_folder.data else {}
|
||||
existing_sys_prompt = existing_data.get("system_prompt", "")
|
||||
|
||||
# 2. 提取现有规则
|
||||
current_rules_content = self._extract_existing_rules(existing_sys_prompt)
|
||||
|
||||
# 3. 生成新规则
|
||||
await self._emit_status(
|
||||
__event_emitter__, "正在分析项目规则...", done=False
|
||||
)
|
||||
|
||||
messages = body.get("messages", [])
|
||||
new_rules_content = await self._generate_new_rules(
|
||||
current_rules_content, messages, user_id, __request__
|
||||
)
|
||||
|
||||
rules_changed = new_rules_content != current_rules_content
|
||||
|
||||
# 如果生成结果无变更
|
||||
if not rules_changed:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"无变更",
|
||||
{
|
||||
"step": "check_changes",
|
||||
"reason": "content_identical_or_generation_failed",
|
||||
},
|
||||
)
|
||||
await self._emit_status(
|
||||
__event_emitter__, "规则分析完成:无新增内容。", done=True
|
||||
)
|
||||
return
|
||||
|
||||
# 5. 注入规则到 System Prompt
|
||||
updated_sys_prompt = existing_sys_prompt
|
||||
if rules_changed:
|
||||
updated_sys_prompt = self._inject_rules(
|
||||
updated_sys_prompt,
|
||||
new_rules_content,
|
||||
self.valves.RULES_BLOCK_TITLE,
|
||||
)
|
||||
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"准备更新数据库",
|
||||
{"step": "pre_db_update", "target_folder_id": target_folder_id},
|
||||
)
|
||||
|
||||
# 6. 更新文件夹 (ORM) - 仅更新 'data' 字段
|
||||
existing_data["system_prompt"] = updated_sys_prompt
|
||||
|
||||
updated_folder = Folders.update_folder_by_id_and_user_id(
|
||||
target_folder_id,
|
||||
user_id,
|
||||
FolderUpdateForm(data=existing_data),
|
||||
)
|
||||
|
||||
if not updated_folder:
|
||||
raise Exception("Update folder failed (ORM returned None)")
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "规则分析完成:规则已更新。", done=True
|
||||
)
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"规则生成过程和变更详情",
|
||||
{
|
||||
"step": "success",
|
||||
"folder_id": target_folder_id,
|
||||
"target_is_root": target_folder_id != folder_id,
|
||||
"model_used": self.valves.MODEL_ID,
|
||||
"analyzed_messages_count": len(messages),
|
||||
"old_rules_length": len(current_rules_content),
|
||||
"new_rules_length": len(new_rules_content),
|
||||
"changes_digest": {
|
||||
"old_rules_preview": (
|
||||
current_rules_content[:100] + "..."
|
||||
if current_rules_content
|
||||
else "None"
|
||||
),
|
||||
"new_rules_preview": (
|
||||
new_rules_content[:100] + "..."
|
||||
if new_rules_content
|
||||
else "None"
|
||||
),
|
||||
},
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"异步规则处理错误: {e}")
|
||||
await self._emit_status(__event_emitter__, "更新规则失败。", done=True)
|
||||
# 在控制台也输出错误信息,方便调试
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__, "执行出错", {"error": str(e), "folder_id": folder_id}
|
||||
)
|
||||
|
||||
# ==================== Filter Hooks ====================
|
||||
|
||||
async def inlet(
|
||||
self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None
|
||||
) -> dict:
|
||||
return body
|
||||
|
||||
async def outlet(
|
||||
self,
|
||||
body: dict,
|
||||
__user__: Optional[dict] = None,
|
||||
__event_emitter__=None,
|
||||
__request__: Optional[Request] = None,
|
||||
) -> dict:
|
||||
user_ctx = self._get_user_context(__user__)
|
||||
chat_ctx = self._get_chat_context(body)
|
||||
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# 触发逻辑:消息计数阈值
|
||||
if len(messages) % self.valves.MESSAGE_TRIGGER_COUNT != 0:
|
||||
return body
|
||||
|
||||
folder_id = self._get_folder_id(body)
|
||||
if not folder_id:
|
||||
await self._emit_debug_log(
|
||||
__event_emitter__,
|
||||
"跳过分析",
|
||||
{"reason": "对话不属于任何文件夹", "chat_id": chat_ctx.get("chat_id")},
|
||||
)
|
||||
return body
|
||||
|
||||
# 用户信息
|
||||
user_id = user_ctx.get("user_id")
|
||||
if not user_id:
|
||||
return body
|
||||
|
||||
# 异步任务
|
||||
if self.valves.MODEL_ID == "":
|
||||
self.valves.MODEL_ID = body.get("model", "")
|
||||
|
||||
asyncio.create_task(
|
||||
self._process_rules_update(
|
||||
folder_id, body, user_id, __request__, __event_emitter__
|
||||
)
|
||||
)
|
||||
|
||||
return body
|
||||
@@ -1,6 +1,6 @@
|
||||
# Markdown Normalizer Filter
|
||||
|
||||
**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
|
||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||
|
||||
A content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
||||
|
||||
@@ -43,7 +43,7 @@ A content normalizer filter for Open WebUI that fixes common Markdown formatting
|
||||
* `enable_heading_fix`: Fix missing space in headings.
|
||||
* `enable_table_fix`: Fix missing closing pipe in tables.
|
||||
* `enable_xml_tag_cleanup`: Cleanup leftover XML tags.
|
||||
* `enable_emphasis_spacing_fix`: Fix extra spaces in emphasis (default: True).
|
||||
* `enable_emphasis_spacing_fix`: Fix extra spaces in emphasis (default: False).
|
||||
* `show_status`: Show status notification when fixes are applied.
|
||||
* `show_debug_log`: Print debug logs to browser console.
|
||||
|
||||
@@ -53,6 +53,10 @@ A content normalizer filter for Open WebUI that fixes common Markdown formatting
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.2.4
|
||||
|
||||
* **Documentation Updates**: Synchronized version numbers across all documentation and code files.
|
||||
|
||||
### v1.2.3
|
||||
|
||||
* **List Marker Protection Enhancement**: Fixed a bug where list markers (`*`) followed by plain text and emphasis were having their spaces incorrectly stripped (e.g., `* U16 forward` became `*U16 forward`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Markdown 格式化过滤器 (Markdown Normalizer)
|
||||
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.2 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.4 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||
|
||||
这是一个用于 Open WebUI 的内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.4
|
||||
|
||||
* **文档更新**: 同步了所有文档和代码文件的版本号。
|
||||
|
||||
### v1.2.3
|
||||
|
||||
* **列表标记保护增强**: 修复了列表标记 (`*`) 后跟普通文本和强调标记时,空格被错误剥离的问题(例如 `* U16 前锋` 变成 `*U16 前锋`)。
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Markdown Normalizer
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 1.2.3
|
||||
version: 1.2.4
|
||||
openwebui_id: baaa8732-9348-40b7-8359-7e009660e23c
|
||||
description: A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.
|
||||
"""
|
||||
@@ -43,7 +43,7 @@ class NormalizerConfig:
|
||||
)
|
||||
enable_table_fix: bool = True # Fix missing closing pipe in tables
|
||||
enable_xml_tag_cleanup: bool = True # Cleanup leftover XML tags
|
||||
enable_emphasis_spacing_fix: bool = True # Fix spaces inside **emphasis**
|
||||
enable_emphasis_spacing_fix: bool = False # Fix spaces inside **emphasis**
|
||||
|
||||
# Custom cleaner functions (for advanced extension)
|
||||
custom_cleaners: List[Callable[[str], str]] = field(default_factory=list)
|
||||
@@ -564,7 +564,7 @@ class Filter:
|
||||
default=True, description="Cleanup leftover XML tags"
|
||||
)
|
||||
enable_emphasis_spacing_fix: bool = Field(
|
||||
default=True,
|
||||
default=False,
|
||||
description="Fix spaces inside **emphasis** (e.g. ** text ** -> **text**)",
|
||||
)
|
||||
show_status: bool = Field(
|
||||
@@ -695,6 +695,15 @@ class Filter:
|
||||
if self._contains_html(content):
|
||||
return body
|
||||
|
||||
# Skip if content contains tool output markers (native function calling)
|
||||
# Pattern: """...""" or tool_call_id or <details type="tool_calls"...>
|
||||
if (
|
||||
'"""' in content
|
||||
or "tool_call_id" in content
|
||||
or '<details type="tool_calls"' in content
|
||||
):
|
||||
return body
|
||||
|
||||
# Configure normalizer based on valves
|
||||
config = NormalizerConfig(
|
||||
enable_escape_fix=self.valves.enable_escape_fix,
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Markdown 格式修复器 (Markdown Normalizer)
|
||||
author: Fu-Jie
|
||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||
funding_url: https://github.com/open-webui
|
||||
version: 1.2.3
|
||||
version: 1.2.4
|
||||
description: 内容规范化过滤器,修复 LLM 输出中常见的 Markdown 格式问题,如损坏的代码块、LaTeX 公式、Mermaid 图表和列表格式。
|
||||
"""
|
||||
|
||||
@@ -24,6 +24,9 @@ class NormalizerConfig:
|
||||
"""配置类,用于启用/禁用特定的规范化规则"""
|
||||
|
||||
enable_escape_fix: bool = True # 修复过度的转义字符
|
||||
enable_escape_fix_in_code_blocks: bool = (
|
||||
False # 在代码块内部应用转义修复 (默认:关闭,以确保安全)
|
||||
)
|
||||
enable_thought_tag_fix: bool = True # 规范化思维链标签
|
||||
enable_details_tag_fix: bool = True # 规范化 <details> 标签(类似思维链标签)
|
||||
enable_code_block_fix: bool = True # 修复代码块格式
|
||||
@@ -35,7 +38,7 @@ class NormalizerConfig:
|
||||
enable_heading_fix: bool = True # 修复标题中缺失的空格 (#Header -> # Header)
|
||||
enable_table_fix: bool = True # 修复表格中缺失的闭合管道符
|
||||
enable_xml_tag_cleanup: bool = True # 清理残留的 XML 标签
|
||||
enable_emphasis_spacing_fix: bool = True # 修复 **强调内容** 中的多余空格
|
||||
enable_emphasis_spacing_fix: bool = False # 修复 **强调内容** 中的多余空格
|
||||
|
||||
# 自定义清理函数 (用于高级扩展)
|
||||
custom_cleaners: List[Callable[[str], str]] = field(default_factory=list)
|
||||
@@ -239,12 +242,27 @@ class ContentNormalizer:
|
||||
return content
|
||||
|
||||
def _fix_escape_characters(self, content: str) -> str:
|
||||
"""Fix excessive escape characters"""
|
||||
content = content.replace("\\r\\n", "\n")
|
||||
content = content.replace("\\n", "\n")
|
||||
content = content.replace("\\t", "\t")
|
||||
content = content.replace("\\\\", "\\")
|
||||
return content
|
||||
"""修复过度的转义字符
|
||||
|
||||
如果 enable_escape_fix_in_code_blocks 为 False (默认),此方法将仅修复代码块外部的转义字符,
|
||||
以避免破坏有效的代码示例 (例如,带有 \\n 的 JSON 字符串、正则表达式模式等)。
|
||||
"""
|
||||
if self.config.enable_escape_fix_in_code_blocks:
|
||||
# 全局应用 (原始行为)
|
||||
content = content.replace("\\r\\n", "\n")
|
||||
content = content.replace("\\n", "\n")
|
||||
content = content.replace("\\t", "\t")
|
||||
content = content.replace("\\\\", "\\")
|
||||
return content
|
||||
else:
|
||||
# 仅在代码块外部应用 (安全模式)
|
||||
parts = content.split("```")
|
||||
for i in range(0, len(parts), 2): # 偶数索引是 Markdown 文本 (非代码)
|
||||
parts[i] = parts[i].replace("\\r\\n", "\n")
|
||||
parts[i] = parts[i].replace("\\n", "\n")
|
||||
parts[i] = parts[i].replace("\\t", "\t")
|
||||
parts[i] = parts[i].replace("\\\\", "\\")
|
||||
return "```".join(parts)
|
||||
|
||||
def _fix_thought_tags(self, content: str) -> str:
|
||||
"""Normalize thought tags: unify naming and fix spacing"""
|
||||
@@ -501,6 +519,10 @@ class Filter:
|
||||
enable_escape_fix: bool = Field(
|
||||
default=True, description="修复过度的转义字符 (\\n, \\t 等)"
|
||||
)
|
||||
enable_escape_fix_in_code_blocks: bool = Field(
|
||||
default=False,
|
||||
description="在代码块内部应用转义修复 (⚠️ 警告:可能会破坏有效的代码,如 JSON 字符串或正则模式。默认:关闭,以确保安全)",
|
||||
)
|
||||
enable_thought_tag_fix: bool = Field(
|
||||
default=True, description="规范化思维链标签 (<think> -> <thought>)"
|
||||
)
|
||||
@@ -539,7 +561,7 @@ class Filter:
|
||||
default=True, description="清理残留的 XML 标签"
|
||||
)
|
||||
enable_emphasis_spacing_fix: bool = Field(
|
||||
default=True,
|
||||
default=False,
|
||||
description="修复强调语法中的多余空格 (例如 ** 文本 ** -> **文本**)",
|
||||
)
|
||||
show_status: bool = Field(default=True, description="应用修复时显示状态通知")
|
||||
@@ -682,13 +704,23 @@ class Filter:
|
||||
content = last.get("content", "") or ""
|
||||
|
||||
if last.get("role") == "assistant" and isinstance(content, str):
|
||||
# Skip if content looks like HTML to avoid breaking it
|
||||
# 如果内容看起来像 HTML,则跳过以避免破坏它
|
||||
if self._contains_html(content):
|
||||
return body
|
||||
|
||||
# Configure normalizer based on valves
|
||||
# 如果内容包含工具输出标记 (原生函数调用),则跳过
|
||||
# 模式:"""...""" 或 tool_call_id 或 <details type="tool_calls"...>
|
||||
if (
|
||||
'"""' in content
|
||||
or "tool_call_id" in content
|
||||
or '<details type="tool_calls"' in content
|
||||
):
|
||||
return body
|
||||
|
||||
# 根据 Valves 配置 Normalizer
|
||||
config = NormalizerConfig(
|
||||
enable_escape_fix=self.valves.enable_escape_fix,
|
||||
enable_escape_fix_in_code_blocks=self.valves.enable_escape_fix_in_code_blocks,
|
||||
enable_thought_tag_fix=self.valves.enable_thought_tag_fix,
|
||||
enable_details_tag_fix=self.valves.enable_details_tag_fix,
|
||||
enable_code_block_fix=self.valves.enable_code_block_fix,
|
||||
|
||||
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