feat: Implement configurable OpenWebUI base URL for deployment scripts and update documentation.

This commit is contained in:
fujie
2026-03-11 23:32:00 +08:00
parent 5fe66a5803
commit fdf95a2825
13 changed files with 203 additions and 128 deletions

1
.gitignore vendored
View File

@@ -142,3 +142,4 @@ logs/
# OpenWebUI specific # OpenWebUI specific
# Add any specific ignores for OpenWebUI plugins if needed # Add any specific ignores for OpenWebUI plugins if needed
.git-worktrees/ .git-worktrees/
plugins/filters/auth_model_info/

View File

@@ -1886,19 +1886,9 @@ class Filter:
""" """
Check if compression should be skipped. Check if compression should be skipped.
Returns True if: Returns True if:
1. The base model includes 'copilot_sdk'
""" """
# Check if base model includes copilot_sdk if body.get("is_copilot_model", False):
if __model__:
base_model_id = __model__.get("base_model_id", "")
if "copilot_sdk" in base_model_id.lower():
return True
# Also check model in body
model_id = body.get("model", "")
if "copilot_sdk" in model_id.lower():
return True return True
return False return False
async def inlet( async def inlet(

View File

@@ -114,6 +114,7 @@ class Filter:
# Check if it's a Copilot model # Check if it's a Copilot model
is_copilot_model = self._is_copilot_model(current_model) is_copilot_model = self._is_copilot_model(current_model)
body["is_copilot_model"] = is_copilot_model
await self._emit_debug_log( await self._emit_debug_log(
__event_emitter__, __event_emitter__,

View File

@@ -9,6 +9,7 @@ This directory contains automated scripts for deploying plugins in development t
1. **OpenWebUI Running**: Make sure OpenWebUI is running locally (default `http://localhost:3000`) 1. **OpenWebUI Running**: Make sure OpenWebUI is running locally (default `http://localhost:3000`)
2. **API Key**: You need a valid OpenWebUI API key 2. **API Key**: You need a valid OpenWebUI API key
3. **Environment File**: Create a `.env` file in this directory containing your API key: 3. **Environment File**: Create a `.env` file in this directory containing your API key:
``` ```
api_key=sk-xxxxxxxxxxxxx api_key=sk-xxxxxxxxxxxxx
``` ```
@@ -42,12 +43,14 @@ python deploy_filter.py --list
Used to deploy Filter-type plugins (such as message filtering, context compression, etc.). Used to deploy Filter-type plugins (such as message filtering, context compression, etc.).
**Key Features**: **Key Features**:
- ✅ Auto-extracts metadata from Python files (version, author, description, etc.) - ✅ Auto-extracts metadata from Python files (version, author, description, etc.)
- ✅ Attempts to update existing plugins, creates if not found - ✅ Attempts to update existing plugins, creates if not found
- ✅ Supports multiple Filter plugin management - ✅ Supports multiple Filter plugin management
- ✅ Detailed error messages and connection diagnostics - ✅ Detailed error messages and connection diagnostics
**Usage**: **Usage**:
```bash ```bash
# Deploy async_context_compression (default) # Deploy async_context_compression (default)
python deploy_filter.py python deploy_filter.py
@@ -62,6 +65,7 @@ python deploy_filter.py -l
``` ```
**Workflow**: **Workflow**:
1. Load API key from `.env` 1. Load API key from `.env`
2. Find target Filter plugin directory 2. Find target Filter plugin directory
3. Read Python source file 3. Read Python source file
@@ -76,6 +80,7 @@ python deploy_filter.py -l
Used to deploy Pipe-type plugins (such as GitHub Copilot SDK). Used to deploy Pipe-type plugins (such as GitHub Copilot SDK).
**Usage**: **Usage**:
```bash ```bash
python deploy_pipe.py python deploy_pipe.py
``` ```
@@ -101,6 +106,7 @@ Create a dedicated long-term API key in OpenWebUI Settings for deployment purpos
**Cause**: OpenWebUI is not running or port is different **Cause**: OpenWebUI is not running or port is different
**Solution**: **Solution**:
- Make sure OpenWebUI is running - Make sure OpenWebUI is running
- Check which port OpenWebUI is actually listening on (usually 3000) - Check which port OpenWebUI is actually listening on (usually 3000)
- Edit the URL in the script if needed - Edit the URL in the script if needed
@@ -110,6 +116,7 @@ Create a dedicated long-term API key in OpenWebUI Settings for deployment purpos
**Cause**: `.env` file was not created **Cause**: `.env` file was not created
**Solution**: **Solution**:
```bash ```bash
echo "api_key=sk-your-api-key-here" > .env echo "api_key=sk-your-api-key-here" > .env
``` ```
@@ -119,6 +126,7 @@ echo "api_key=sk-your-api-key-here" > .env
**Cause**: Filter directory name is incorrect **Cause**: Filter directory name is incorrect
**Solution**: **Solution**:
```bash ```bash
# List all available Filters # List all available Filters
python deploy_filter.py --list python deploy_filter.py --list
@@ -129,6 +137,7 @@ python deploy_filter.py --list
**Cause**: API key is invalid or expired **Cause**: API key is invalid or expired
**Solution**: **Solution**:
1. Verify your API key is valid 1. Verify your API key is valid
2. Generate a new API key 2. Generate a new API key
3. Update the `.env` file 3. Update the `.env` file
@@ -177,7 +186,8 @@ python deploy_filter.py async-context-compression
## Security Considerations ## Security Considerations
⚠️ **Important**: ⚠️ **Important**:
- ✅ Add `.env` file to `.gitignore` (avoid committing sensitive info) - ✅ Add `.env` file to `.gitignore` (avoid committing sensitive info)
- ✅ Never commit API keys to version control - ✅ Never commit API keys to version control
- ✅ Use only on trusted networks - ✅ Use only on trusted networks

View File

@@ -7,6 +7,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
## 📋 New Files ## 📋 New Files
### 1. **deploy_filter.py** — Filter Plugin Deployment Script ### 1. **deploy_filter.py** — Filter Plugin Deployment Script
- **Location**: `scripts/deploy_filter.py` - **Location**: `scripts/deploy_filter.py`
- **Function**: Auto-deploy Filter-type plugins to local OpenWebUI instance - **Function**: Auto-deploy Filter-type plugins to local OpenWebUI instance
- **Features**: - **Features**:
@@ -19,6 +20,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- **Code Lines**: ~300 - **Code Lines**: ~300
### 2. **DEPLOYMENT_GUIDE.md** — Complete Deployment Guide ### 2. **DEPLOYMENT_GUIDE.md** — Complete Deployment Guide
- **Location**: `scripts/DEPLOYMENT_GUIDE.md` - **Location**: `scripts/DEPLOYMENT_GUIDE.md`
- **Contents**: - **Contents**:
- Prerequisites and quick start - Prerequisites and quick start
@@ -28,6 +30,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- Step-by-step workflow examples - Step-by-step workflow examples
### 3. **QUICK_START.md** — Quick Reference Card ### 3. **QUICK_START.md** — Quick Reference Card
- **Location**: `scripts/QUICK_START.md` - **Location**: `scripts/QUICK_START.md`
- **Contents**: - **Contents**:
- One-line deployment command - One-line deployment command
@@ -37,6 +40,7 @@ Added a complete local deployment toolchain for the `async_context_compression`
- CI/CD integration examples - CI/CD integration examples
### 4. **test_deploy_filter.py** — Unit Test Suite ### 4. **test_deploy_filter.py** — Unit Test Suite
- **Location**: `tests/scripts/test_deploy_filter.py` - **Location**: `tests/scripts/test_deploy_filter.py`
- **Test Coverage**: - **Test Coverage**:
- ✅ Filter file discovery (3 tests) - ✅ Filter file discovery (3 tests)
@@ -138,6 +142,7 @@ openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
``` ```
**Supported Metadata Fields**: **Supported Metadata Fields**:
- `title` — Filter display name ✅ - `title` — Filter display name ✅
- `id` — Unique identifier ✅ - `id` — Unique identifier ✅
- `author` — Author name ✅ - `author` — Author name ✅
@@ -335,17 +340,20 @@ Metadata Extraction and Delivery
### Debugging Tips ### Debugging Tips
1. **Enable Verbose Logging**: 1. **Enable Verbose Logging**:
```bash ```bash
python deploy_filter.py 2>&1 | tee deploy.log python deploy_filter.py 2>&1 | tee deploy.log
``` ```
2. **Test API Connection**: 2. **Test API Connection**:
```bash ```bash
curl -X GET http://localhost:3000/api/v1/functions \ curl -X GET http://localhost:3000/api/v1/functions \
-H "Authorization: Bearer $API_KEY" -H "Authorization: Bearer $API_KEY"
``` ```
3. **Verify .env File**: 3. **Verify .env File**:
```bash ```bash
grep "api_key=" scripts/.env grep "api_key=" scripts/.env
``` ```

View File

@@ -73,12 +73,14 @@ python deploy_async_context_compression.py
``` ```
**Features**: **Features**:
- ✅ Optimized specifically for async_context_compression - ✅ Optimized specifically for async_context_compression
- ✅ Clear deployment steps and confirmation - ✅ Clear deployment steps and confirmation
- ✅ Friendly error messages - ✅ Friendly error messages
- ✅ Shows next steps after successful deployment - ✅ Shows next steps after successful deployment
**Sample Output**: **Sample Output**:
``` ```
====================================================================== ======================================================================
🚀 Deploying Async Context Compression Filter Plugin 🚀 Deploying Async Context Compression Filter Plugin
@@ -117,6 +119,7 @@ python deploy_filter.py --list
``` ```
**Features**: **Features**:
- ✅ Generic Filter deployment tool - ✅ Generic Filter deployment tool
- ✅ Supports multiple plugins - ✅ Supports multiple plugins
- ✅ Auto metadata extraction - ✅ Auto metadata extraction
@@ -142,6 +145,7 @@ python deploy_tool.py openwebui-skills-manager
``` ```
**Features**: **Features**:
- ✅ Supports Tools plugin deployment - ✅ Supports Tools plugin deployment
- ✅ Auto-detects `Tools` class definition - ✅ Auto-detects `Tools` class definition
- ✅ Smart update/create logic - ✅ Smart update/create logic
@@ -290,6 +294,7 @@ git status # should not show .env
``` ```
**Solution**: **Solution**:
```bash ```bash
# 1. Check if OpenWebUI is running # 1. Check if OpenWebUI is running
curl http://localhost:3000 curl http://localhost:3000
@@ -309,6 +314,7 @@ curl http://localhost:3000
``` ```
**Solution**: **Solution**:
```bash ```bash
echo "api_key=sk-your-api-key" > .env echo "api_key=sk-your-api-key" > .env
cat .env # verify file created cat .env # verify file created
@@ -321,6 +327,7 @@ cat .env # verify file created
``` ```
**Solution**: **Solution**:
```bash ```bash
# List all available Filters # List all available Filters
python deploy_filter.py --list python deploy_filter.py --list
@@ -337,6 +344,7 @@ python deploy_filter.py async-context-compression
``` ```
**Solution**: **Solution**:
```bash ```bash
# 1. Verify API key is correct # 1. Verify API key is correct
grep "api_key=" .env grep "api_key=" .env
@@ -370,7 +378,7 @@ python deploy_async_context_compression.py
### Method 2: Verify in OpenWebUI ### Method 2: Verify in OpenWebUI
1. Open OpenWebUI: http://localhost:3000 1. Open OpenWebUI: <http://localhost:3000>
2. Go to Settings → Filters 2. Go to Settings → Filters
3. Check if 'Async Context Compression' is listed 3. Check if 'Async Context Compression' is listed
4. Verify version number is correct (should be latest) 4. Verify version number is correct (should be latest)
@@ -380,6 +388,7 @@ python deploy_async_context_compression.py
1. Open a new conversation 1. Open a new conversation
2. Enable 'Async Context Compression' Filter 2. Enable 'Async Context Compression' Filter
3. Have multiple-turn conversation and verify compression/summarization works 3. Have multiple-turn conversation and verify compression/summarization works
## 💡 Advanced Usage ## 💡 Advanced Usage
### Automated Deploy & Test ### Automated Deploy & Test
@@ -473,4 +482,3 @@ Newly created deployment-related files:
**Last Updated**: 2026-03-09 **Last Updated**: 2026-03-09
**Script Status**: ✅ Ready for production **Script Status**: ✅ Ready for production
**Test Coverage**: 10/10 passed ✅ **Test Coverage**: 10/10 passed ✅

View File

@@ -5,6 +5,7 @@
**Yes, re-deploying automatically updates the plugin!** **Yes, re-deploying automatically updates the plugin!**
The deployment script uses a **smart two-stage strategy**: The deployment script uses a **smart two-stage strategy**:
1. 🔄 **Try UPDATE First** (if plugin exists) 1. 🔄 **Try UPDATE First** (if plugin exists)
2. 📝 **Auto CREATE** (if update fails — plugin doesn't exist) 2. 📝 **Auto CREATE** (if update fails — plugin doesn't exist)
@@ -54,6 +55,7 @@ if response.status_code == 200:
``` ```
**What Happens**: **What Happens**:
- Send **POST** to `/api/v1/functions/id/{filter_id}/update` - Send **POST** to `/api/v1/functions/id/{filter_id}/update`
- If returns **HTTP 200**, plugin exists and update succeeded - If returns **HTTP 200**, plugin exists and update succeeded
- Includes: - Includes:
@@ -84,6 +86,7 @@ if response.status_code != 200:
``` ```
**What Happens**: **What Happens**:
- If update fails (HTTP ≠ 200), auto-attempt create - If update fails (HTTP ≠ 200), auto-attempt create
- Send **POST** to `/api/v1/functions/create` - Send **POST** to `/api/v1/functions/create`
- Uses **same payload** (code, metadata identical) - Uses **same payload** (code, metadata identical)
@@ -103,6 +106,7 @@ $ python deploy_async_context_compression.py
``` ```
**What Happens**: **What Happens**:
1. Try UPDATE → fails (HTTP 404 — plugin doesn't exist) 1. Try UPDATE → fails (HTTP 404 — plugin doesn't exist)
2. Auto-try CREATE → succeeds (HTTP 200) 2. Auto-try CREATE → succeeds (HTTP 200)
3. Plugin created in OpenWebUI 3. Plugin created in OpenWebUI
@@ -121,6 +125,7 @@ $ python deploy_async_context_compression.py
``` ```
**What Happens**: **What Happens**:
1. Read modified code 1. Read modified code
2. Try UPDATE → succeeds (HTTP 200 — plugin exists) 2. Try UPDATE → succeeds (HTTP 200 — plugin exists)
3. Plugin in OpenWebUI updated to latest code 3. Plugin in OpenWebUI updated to latest code
@@ -147,6 +152,7 @@ $ python deploy_async_context_compression.py
``` ```
**Characteristics**: **Characteristics**:
- 🚀 Each update takes only 5 seconds - 🚀 Each update takes only 5 seconds
- 📝 Each is an incremental update - 📝 Each is an incremental update
- ✅ No need to restart OpenWebUI - ✅ No need to restart OpenWebUI
@@ -181,11 +187,13 @@ version: 1.3.0
``` ```
**Each deployment**: **Each deployment**:
1. Script reads version from docstring 1. Script reads version from docstring
2. Sends this version in manifest to OpenWebUI 2. Sends this version in manifest to OpenWebUI
3. If you change version in code, deployment updates to new version 3. If you change version in code, deployment updates to new version
**Best Practice**: **Best Practice**:
```bash ```bash
# 1. Modify code # 1. Modify code
vim async_context_compression.py vim async_context_compression.py
@@ -300,6 +308,7 @@ Usually **not needed** because:
4. ✅ Failures auto-rollback 4. ✅ Failures auto-rollback
但如果真的需要控制,可以: 但如果真的需要控制,可以:
- 手动修改脚本 (修改 `deploy_filter.py`) - 手动修改脚本 (修改 `deploy_filter.py`)
- 或分别使用 UPDATE/CREATE 的具体 API 端点 - 或分别使用 UPDATE/CREATE 的具体 API 端点
@@ -323,6 +332,7 @@ Usually **not needed** because:
### Q: 可以同时部署多个插件吗? ### Q: 可以同时部署多个插件吗?
**可以!** **可以!**
```bash ```bash
python deploy_filter.py async-context-compression python deploy_filter.py async-context-compression
python deploy_filter.py folder-memory python deploy_filter.py folder-memory
@@ -337,6 +347,7 @@ python deploy_filter.py context_enhancement_filter
--- ---
**总结**: 部署脚本的更新机制完全自动化,开发者只需修改代码,每次运行 `deploy_async_context_compression.py` 就会自动: **总结**: 部署脚本的更新机制完全自动化,开发者只需修改代码,每次运行 `deploy_async_context_compression.py` 就会自动:
1. ✅ 创建(第一次)或更新(后续)插件 1. ✅ 创建(第一次)或更新(后续)插件
2. ✅ 从代码提取最新的元数据和版本号 2. ✅ 从代码提取最新的元数据和版本号
3. ✅ 立即生效,无需重启 OpenWebUI 3. ✅ 立即生效,无需重启 OpenWebUI

View File

@@ -11,9 +11,9 @@ Usage:
To get started: To get started:
1. Create .env file with your OpenWebUI API key: 1. Create .env file with your OpenWebUI API key:
echo "api_key=sk-your-key-here" > .env echo "api_key=sk-your-key-here" > .env
2. Make sure OpenWebUI is running on localhost:3000 2. Make sure OpenWebUI is running on localhost:3000
3. Run this script: 3. Run this script:
python deploy_async_context_compression.py python deploy_async_context_compression.py
""" """
@@ -34,10 +34,10 @@ def main():
print("🚀 Deploying Async Context Compression Filter Plugin") print("🚀 Deploying Async Context Compression Filter Plugin")
print("=" * 70) print("=" * 70)
print() print()
# Deploy the filter # Deploy the filter
success = deploy_filter("async-context-compression") success = deploy_filter("async-context-compression")
if success: if success:
print() print()
print("=" * 70) print("=" * 70)
@@ -63,7 +63,7 @@ def main():
print(" • Check network connectivity") print(" • Check network connectivity")
print() print()
return 1 return 1
return 0 return 0

View File

@@ -49,53 +49,78 @@ def _load_api_key() -> str:
raise ValueError("api_key not found in .env file.") raise ValueError("api_key not found in .env file.")
def _load_openwebui_base_url() -> str:
"""Load OpenWebUI base URL from .env file or environment.
Checks in order:
1. OPENWEBUI_BASE_URL in .env
2. OPENWEBUI_BASE_URL environment variable
3. Default to http://localhost:3000
"""
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("OPENWEBUI_BASE_URL="):
url = line.split("=", 1)[1].strip()
if url:
return url
# Try environment variable
url = os.environ.get("OPENWEBUI_BASE_URL")
if url:
return url
# Default
return "http://localhost:3000"
def _find_filter_file(filter_name: str) -> Optional[Path]: def _find_filter_file(filter_name: str) -> Optional[Path]:
"""Find the main Python file for a filter. """Find the main Python file for a filter.
Args: Args:
filter_name: Directory name of the filter (e.g., 'async-context-compression') filter_name: Directory name of the filter (e.g., 'async-context-compression')
Returns: Returns:
Path to the main Python file, or None if not found. Path to the main Python file, or None if not found.
""" """
filter_dir = FILTERS_DIR / filter_name filter_dir = FILTERS_DIR / filter_name
if not filter_dir.exists(): if not filter_dir.exists():
return None return None
# Try to find a .py file matching the filter name # Try to find a .py file matching the filter name
py_files = list(filter_dir.glob("*.py")) py_files = list(filter_dir.glob("*.py"))
# Prefer a file with the filter name (with hyphens converted to underscores) # Prefer a file with the filter name (with hyphens converted to underscores)
preferred_name = filter_name.replace("-", "_") + ".py" preferred_name = filter_name.replace("-", "_") + ".py"
for py_file in py_files: for py_file in py_files:
if py_file.name == preferred_name: if py_file.name == preferred_name:
return py_file return py_file
# Otherwise, return the first .py file (usually the only one) # Otherwise, return the first .py file (usually the only one)
if py_files: if py_files:
return py_files[0] return py_files[0]
return None return None
def _extract_metadata(content: str) -> Dict[str, Any]: def _extract_metadata(content: str) -> Dict[str, Any]:
"""Extract metadata from the plugin docstring. """Extract metadata from the plugin docstring.
Args: Args:
content: Python file content content: Python file content
Returns: Returns:
Dictionary with extracted metadata (title, author, version, etc.) Dictionary with extracted metadata (title, author, version, etc.)
""" """
metadata = {} metadata = {}
# Extract docstring # Extract docstring
match = re.search(r'"""(.*?)"""', content, re.DOTALL) match = re.search(r'"""(.*?)"""', content, re.DOTALL)
if not match: if not match:
return metadata return metadata
docstring = match.group(1) docstring = match.group(1)
# Extract key-value pairs # Extract key-value pairs
for line in docstring.split("\n"): for line in docstring.split("\n"):
line = line.strip() line = line.strip()
@@ -104,7 +129,7 @@ def _extract_metadata(content: str) -> Dict[str, Any]:
key = parts[0].strip().lower() key = parts[0].strip().lower()
value = parts[1].strip() value = parts[1].strip()
metadata[key] = value metadata[key] = value
return metadata return metadata
@@ -112,13 +137,13 @@ def _build_filter_payload(
filter_name: str, file_path: Path, content: str, metadata: Dict[str, Any] filter_name: str, file_path: Path, content: str, metadata: Dict[str, Any]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Build the payload for the filter update/create API. """Build the payload for the filter update/create API.
Args: Args:
filter_name: Directory name of the filter filter_name: Directory name of the filter
file_path: Path to the plugin file file_path: Path to the plugin file
content: File content content: File content
metadata: Extracted metadata metadata: Extracted metadata
Returns: Returns:
Payload dictionary ready for API submission Payload dictionary ready for API submission
""" """
@@ -126,12 +151,14 @@ def _build_filter_payload(
filter_id = metadata.get("id", filter_name).replace("-", "_") filter_id = metadata.get("id", filter_name).replace("-", "_")
title = metadata.get("title", filter_name) title = metadata.get("title", filter_name)
author = metadata.get("author", "Fu-Jie") author = metadata.get("author", "Fu-Jie")
author_url = metadata.get("author_url", "https://github.com/Fu-Jie/openwebui-extensions") author_url = metadata.get(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
funding_url = metadata.get("funding_url", "https://github.com/open-webui") funding_url = metadata.get("funding_url", "https://github.com/open-webui")
description = metadata.get("description", f"Filter plugin: {title}") description = metadata.get("description", f"Filter plugin: {title}")
version = metadata.get("version", "1.0.0") version = metadata.get("version", "1.0.0")
openwebui_id = metadata.get("openwebui_id", "") openwebui_id = metadata.get("openwebui_id", "")
payload = { payload = {
"id": filter_id, "id": filter_id,
"name": title, "name": title,
@@ -150,20 +177,20 @@ def _build_filter_payload(
}, },
"content": content, "content": content,
} }
# Add openwebui_id if available # Add openwebui_id if available
if openwebui_id: if openwebui_id:
payload["meta"]["manifest"]["openwebui_id"] = openwebui_id payload["meta"]["manifest"]["openwebui_id"] = openwebui_id
return payload return payload
def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
"""Deploy a filter plugin to OpenWebUI. """Deploy a filter plugin to OpenWebUI.
Args: Args:
filter_name: Directory name of the filter to deploy filter_name: Directory name of the filter to deploy
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
@@ -191,7 +218,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
content = file_path.read_text(encoding="utf-8") content = file_path.read_text(encoding="utf-8")
metadata = _extract_metadata(content) metadata = _extract_metadata(content)
if not metadata: if not metadata:
print(f"[ERROR] Could not extract metadata from {file_path}") print(f"[ERROR] Could not extract metadata from {file_path}")
return False return False
@@ -211,12 +238,14 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
} }
# 6. Send update request # 6. Send update request
update_url = "http://localhost:3000/api/v1/functions/id/{}/update".format(filter_id) base_url = _load_openwebui_base_url()
create_url = "http://localhost:3000/api/v1/functions/create" update_url = "{}/api/v1/functions/id/{}/update".format(base_url, filter_id)
create_url = "{}/api/v1/functions/create".format(base_url)
print(f"📦 Deploying filter '{title}' (version {version})...") print(f"📦 Deploying filter '{title}' (version {version})...")
print(f" File: {file_path}") print(f" File: {file_path}")
print(f" Target: {base_url}")
try: try:
# Try update first # Try update first
response = requests.post( response = requests.post(
@@ -225,7 +254,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
data=json.dumps(payload), data=json.dumps(payload),
timeout=10, timeout=10,
) )
if response.status_code == 200: if response.status_code == 200:
print(f"✅ Successfully updated '{title}' filter!") print(f"✅ Successfully updated '{title}' filter!")
return True return True
@@ -234,7 +263,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
f"⚠️ Update failed with status {response.status_code}, " f"⚠️ Update failed with status {response.status_code}, "
"attempting to create instead..." "attempting to create instead..."
) )
# Try create if update fails # Try create if update fails
res_create = requests.post( res_create = requests.post(
create_url, create_url,
@@ -242,23 +271,24 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
data=json.dumps(payload), data=json.dumps(payload),
timeout=10, timeout=10,
) )
if res_create.status_code == 200: if res_create.status_code == 200:
print(f"✅ Successfully created '{title}' filter!") print(f"✅ Successfully created '{title}' filter!")
return True return True
else: else:
print(f"❌ Failed to update or create. Status: {res_create.status_code}") print(
f"❌ Failed to update or create. Status: {res_create.status_code}"
)
try: try:
error_msg = res_create.json() error_msg = res_create.json()
print(f" Error: {error_msg}") print(f" Error: {error_msg}")
except: except:
print(f" Response: {res_create.text[:500]}") print(f" Response: {res_create.text[:500]}")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
print( base_url = _load_openwebui_base_url()
"❌ Connection error: Could not reach OpenWebUI at localhost:3000" print(f"❌ Connection error: Could not reach OpenWebUI at {base_url}")
)
print(" Make sure OpenWebUI is running and accessible.") print(" Make sure OpenWebUI is running and accessible.")
return False return False
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
@@ -272,16 +302,20 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool:
def list_filters() -> None: def list_filters() -> None:
"""List all available filters.""" """List all available filters."""
print("📋 Available filters:") print("📋 Available filters:")
filters = [d.name for d in FILTERS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")] filters = [
d.name
for d in FILTERS_DIR.iterdir()
if d.is_dir() and not d.name.startswith("_")
]
if not filters: if not filters:
print(" (No filters found)") print(" (No filters found)")
return return
for filter_name in sorted(filters): for filter_name in sorted(filters):
filter_dir = FILTERS_DIR / filter_name filter_dir = FILTERS_DIR / filter_name
py_file = _find_filter_file(filter_name) py_file = _find_filter_file(filter_name)
if py_file: if py_file:
content = py_file.read_text(encoding="utf-8") content = py_file.read_text(encoding="utf-8")
metadata = _extract_metadata(content) metadata = _extract_metadata(content)

View File

@@ -76,52 +76,51 @@ def _get_base_url() -> str:
if not base_url: if not base_url:
raise ValueError( raise ValueError(
f"Missing url. Please create {ENV_FILE} with: " f"Missing url. Please create {ENV_FILE} with: " "url=http://localhost:3000"
"url=http://localhost:3000"
) )
return base_url.rstrip("/") return base_url.rstrip("/")
def _find_tool_file(tool_name: str) -> Optional[Path]: def _find_tool_file(tool_name: str) -> Optional[Path]:
"""Find the main Python file for a tool. """Find the main Python file for a tool.
Args: Args:
tool_name: Directory name of the tool (e.g., 'openwebui-skills-manager') tool_name: Directory name of the tool (e.g., 'openwebui-skills-manager')
Returns: Returns:
Path to the main Python file, or None if not found. Path to the main Python file, or None if not found.
""" """
tool_dir = TOOLS_DIR / tool_name tool_dir = TOOLS_DIR / tool_name
if not tool_dir.exists(): if not tool_dir.exists():
return None return None
# Try to find a .py file matching the tool name # Try to find a .py file matching the tool name
py_files = list(tool_dir.glob("*.py")) py_files = list(tool_dir.glob("*.py"))
# Prefer a file with the tool name (with hyphens converted to underscores) # Prefer a file with the tool name (with hyphens converted to underscores)
preferred_name = tool_name.replace("-", "_") + ".py" preferred_name = tool_name.replace("-", "_") + ".py"
for py_file in py_files: for py_file in py_files:
if py_file.name == preferred_name: if py_file.name == preferred_name:
return py_file return py_file
# Otherwise, return the first .py file (usually the only one) # Otherwise, return the first .py file (usually the only one)
if py_files: if py_files:
return py_files[0] return py_files[0]
return None return None
def _extract_metadata(content: str) -> Dict[str, Any]: def _extract_metadata(content: str) -> Dict[str, Any]:
"""Extract metadata from the plugin docstring.""" """Extract metadata from the plugin docstring."""
metadata = {} metadata = {}
# Extract docstring # Extract docstring
match = re.search(r'"""(.*?)"""', content, re.DOTALL) match = re.search(r'"""(.*?)"""', content, re.DOTALL)
if not match: if not match:
return metadata return metadata
docstring = match.group(1) docstring = match.group(1)
# Extract key-value pairs # Extract key-value pairs
for line in docstring.split("\n"): for line in docstring.split("\n"):
line = line.strip() line = line.strip()
@@ -130,7 +129,7 @@ def _extract_metadata(content: str) -> Dict[str, Any]:
key = parts[0].strip().lower() key = parts[0].strip().lower()
value = parts[1].strip() value = parts[1].strip()
metadata[key] = value metadata[key] = value
return metadata return metadata
@@ -141,12 +140,14 @@ def _build_tool_payload(
tool_id = metadata.get("id", tool_name).replace("-", "_") tool_id = metadata.get("id", tool_name).replace("-", "_")
title = metadata.get("title", tool_name) title = metadata.get("title", tool_name)
author = metadata.get("author", "Fu-Jie") author = metadata.get("author", "Fu-Jie")
author_url = metadata.get("author_url", "https://github.com/Fu-Jie/openwebui-extensions") author_url = metadata.get(
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
funding_url = metadata.get("funding_url", "https://github.com/open-webui") funding_url = metadata.get("funding_url", "https://github.com/open-webui")
description = metadata.get("description", f"Tool plugin: {title}") description = metadata.get("description", f"Tool plugin: {title}")
version = metadata.get("version", "1.0.0") version = metadata.get("version", "1.0.0")
openwebui_id = metadata.get("openwebui_id", "") openwebui_id = metadata.get("openwebui_id", "")
payload = { payload = {
"id": tool_id, "id": tool_id,
"name": title, "name": title,
@@ -165,20 +166,20 @@ def _build_tool_payload(
}, },
"content": content, "content": content,
} }
# Add openwebui_id if available # Add openwebui_id if available
if openwebui_id: if openwebui_id:
payload["meta"]["manifest"]["openwebui_id"] = openwebui_id payload["meta"]["manifest"]["openwebui_id"] = openwebui_id
return payload return payload
def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
"""Deploy a tool plugin to OpenWebUI. """Deploy a tool plugin to OpenWebUI.
Args: Args:
tool_name: Directory name of the tool to deploy tool_name: Directory name of the tool to deploy
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
@@ -207,7 +208,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
content = file_path.read_text(encoding="utf-8") content = file_path.read_text(encoding="utf-8")
metadata = _extract_metadata(content) metadata = _extract_metadata(content)
if not metadata: if not metadata:
print(f"[ERROR] Could not extract metadata from {file_path}") print(f"[ERROR] Could not extract metadata from {file_path}")
return False return False
@@ -229,10 +230,10 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
# 6. Send update request through the native tool endpoints # 6. Send update request through the native tool endpoints
update_url = f"{base_url}/api/v1/tools/id/{tool_id}/update" update_url = f"{base_url}/api/v1/tools/id/{tool_id}/update"
create_url = f"{base_url}/api/v1/tools/create" create_url = f"{base_url}/api/v1/tools/create"
print(f"📦 Deploying tool '{title}' (version {version})...") print(f"📦 Deploying tool '{title}' (version {version})...")
print(f" File: {file_path}") print(f" File: {file_path}")
try: try:
# Try update first # Try update first
response = requests.post( response = requests.post(
@@ -241,7 +242,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
data=json.dumps(payload), data=json.dumps(payload),
timeout=10, timeout=10,
) )
if response.status_code == 200: if response.status_code == 200:
print(f"✅ Successfully updated '{title}' tool!") print(f"✅ Successfully updated '{title}' tool!")
return True return True
@@ -250,7 +251,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
f"⚠️ Update failed with status {response.status_code}, " f"⚠️ Update failed with status {response.status_code}, "
"attempting to create instead..." "attempting to create instead..."
) )
# Try create if update fails # Try create if update fails
res_create = requests.post( res_create = requests.post(
create_url, create_url,
@@ -258,23 +259,23 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
data=json.dumps(payload), data=json.dumps(payload),
timeout=10, timeout=10,
) )
if res_create.status_code == 200: if res_create.status_code == 200:
print(f"✅ Successfully created '{title}' tool!") print(f"✅ Successfully created '{title}' tool!")
return True return True
else: else:
print(f"❌ Failed to update or create. Status: {res_create.status_code}") print(
f"❌ Failed to update or create. Status: {res_create.status_code}"
)
try: try:
error_msg = res_create.json() error_msg = res_create.json()
print(f" Error: {error_msg}") print(f" Error: {error_msg}")
except: except:
print(f" Response: {res_create.text[:500]}") print(f" Response: {res_create.text[:500]}")
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
print( print("❌ Connection error: Could not reach OpenWebUI at {base_url}")
"❌ Connection error: Could not reach OpenWebUI at {base_url}"
)
print(" Make sure OpenWebUI is running and accessible.") print(" Make sure OpenWebUI is running and accessible.")
return False return False
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
@@ -288,16 +289,18 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool:
def list_tools() -> None: def list_tools() -> None:
"""List all available tools.""" """List all available tools."""
print("📋 Available tools:") print("📋 Available tools:")
tools = [d.name for d in TOOLS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")] tools = [
d.name for d in TOOLS_DIR.iterdir() if d.is_dir() and not d.name.startswith("_")
]
if not tools: if not tools:
print(" (No tools found)") print(" (No tools found)")
return return
for tool_name in sorted(tools): for tool_name in sorted(tools):
tool_dir = TOOLS_DIR / tool_name tool_dir = TOOLS_DIR / tool_name
py_file = _find_tool_file(tool_name) py_file = _find_tool_file(tool_name)
if py_file: if py_file:
content = py_file.read_text(encoding="utf-8") content = py_file.read_text(encoding="utf-8")
metadata = _extract_metadata(content) metadata = _extract_metadata(content)

View File

@@ -187,9 +187,7 @@ def build_payload(candidate: PluginCandidate) -> Dict[str, object]:
manifest = dict(candidate.metadata) manifest = dict(candidate.metadata)
manifest.setdefault("title", candidate.title) manifest.setdefault("title", candidate.title)
manifest.setdefault("author", "Fu-Jie") manifest.setdefault("author", "Fu-Jie")
manifest.setdefault( manifest.setdefault("author_url", "https://github.com/Fu-Jie/openwebui-extensions")
"author_url", "https://github.com/Fu-Jie/openwebui-extensions"
)
manifest.setdefault("funding_url", "https://github.com/open-webui") manifest.setdefault("funding_url", "https://github.com/open-webui")
manifest.setdefault( manifest.setdefault(
"description", f"{candidate.plugin_type.title()} plugin: {candidate.title}" "description", f"{candidate.plugin_type.title()} plugin: {candidate.title}"
@@ -233,7 +231,9 @@ def build_api_urls(base_url: str, candidate: PluginCandidate) -> Tuple[str, str]
) )
def discover_plugins(plugin_types: Sequence[str]) -> Tuple[List[PluginCandidate], List[Tuple[Path, str]]]: def discover_plugins(
plugin_types: Sequence[str],
) -> Tuple[List[PluginCandidate], List[Tuple[Path, str]]]:
candidates: List[PluginCandidate] = [] candidates: List[PluginCandidate] = []
skipped: List[Tuple[Path, str]] = [] skipped: List[Tuple[Path, str]] = []
@@ -344,7 +344,9 @@ def print_skipped_summary(skipped: Sequence[Tuple[Path, str]]) -> None:
for _, reason in skipped: for _, reason in skipped:
counts[reason] = counts.get(reason, 0) + 1 counts[reason] = counts.get(reason, 0) + 1
summary = ", ".join(f"{reason}: {count}" for reason, count in sorted(counts.items())) summary = ", ".join(
f"{reason}: {count}" for reason, count in sorted(counts.items())
)
print(f"Skipped {len(skipped)} files ({summary}).") print(f"Skipped {len(skipped)} files ({summary}).")
@@ -421,19 +423,19 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
failed_candidates.append(candidate) failed_candidates.append(candidate)
print(f" [FAILED] {message}") print(f" [FAILED] {message}")
print(f"\n" + "="*80) print(f"\n" + "=" * 80)
print( print(
f"Finished: {success_count}/{len(candidates)} plugins installed successfully." f"Finished: {success_count}/{len(candidates)} plugins installed successfully."
) )
if failed_candidates: if failed_candidates:
print(f"\n{len(failed_candidates)} plugin(s) failed to install:") print(f"\n{len(failed_candidates)} plugin(s) failed to install:")
for candidate in failed_candidates: for candidate in failed_candidates:
print(f"{candidate.title} ({candidate.plugin_type})") print(f"{candidate.title} ({candidate.plugin_type})")
print(f" → Check the error message above") print(f" → Check the error message above")
print() print()
print("="*80) print("=" * 80)
return 0 if success_count == len(candidates) else 1 return 0 if success_count == len(candidates) else 1

View File

@@ -9,14 +9,15 @@ local deployment are present and functional.
import sys import sys
from pathlib import Path from pathlib import Path
def main(): def main():
"""Check all deployment tools are ready.""" """Check all deployment tools are ready."""
base_dir = Path(__file__).parent.parent base_dir = Path(__file__).parent.parent
print("\n" + "="*80) print("\n" + "=" * 80)
print("✨ Async Context Compression Local Deployment Tools — Verification Status") print("✨ Async Context Compression Local Deployment Tools — Verification Status")
print("="*80 + "\n") print("=" * 80 + "\n")
files_to_check = { files_to_check = {
"🐍 Python Scripts": [ "🐍 Python Scripts": [
"scripts/deploy_async_context_compression.py", "scripts/deploy_async_context_compression.py",
@@ -34,56 +35,56 @@ def main():
"tests/scripts/test_deploy_filter.py", "tests/scripts/test_deploy_filter.py",
], ],
} }
all_exist = True all_exist = True
for category, files in files_to_check.items(): for category, files in files_to_check.items():
print(f"\n{category}:") print(f"\n{category}:")
print("-" * 80) print("-" * 80)
for file_path in files: for file_path in files:
full_path = base_dir / file_path full_path = base_dir / file_path
exists = full_path.exists() exists = full_path.exists()
status = "" if exists else "" status = "" if exists else ""
print(f" {status} {file_path}") print(f" {status} {file_path}")
if exists and file_path.endswith(".py"): if exists and file_path.endswith(".py"):
size = full_path.stat().st_size size = full_path.stat().st_size
lines = len(full_path.read_text().split('\n')) lines = len(full_path.read_text().split("\n"))
print(f" └─ [{size} bytes, ~{lines} lines]") print(f" └─ [{size} bytes, ~{lines} lines]")
if not exists: if not exists:
all_exist = False all_exist = False
print("\n" + "="*80) print("\n" + "=" * 80)
if all_exist: if all_exist:
print("✅ All deployment tool files are ready!") print("✅ All deployment tool files are ready!")
print("="*80 + "\n") print("=" * 80 + "\n")
print("🚀 Quick Start (3 ways):\n") print("🚀 Quick Start (3 ways):\n")
print(" Method 1: Easiest (Recommended)") print(" Method 1: Easiest (Recommended)")
print(" ─────────────────────────────────────────────────────────") print(" ─────────────────────────────────────────────────────────")
print(" cd scripts") print(" cd scripts")
print(" python deploy_async_context_compression.py") print(" python deploy_async_context_compression.py")
print() print()
print(" Method 2: Generic Tool") print(" Method 2: Generic Tool")
print(" ─────────────────────────────────────────────────────────") print(" ─────────────────────────────────────────────────────────")
print(" cd scripts") print(" cd scripts")
print(" python deploy_filter.py") print(" python deploy_filter.py")
print() print()
print(" Method 3: Deploy Other Filters") print(" Method 3: Deploy Other Filters")
print(" ─────────────────────────────────────────────────────────") print(" ─────────────────────────────────────────────────────────")
print(" cd scripts") print(" cd scripts")
print(" python deploy_filter.py --list") print(" python deploy_filter.py --list")
print(" python deploy_filter.py folder-memory") print(" python deploy_filter.py folder-memory")
print() print()
print("="*80 + "\n") print("=" * 80 + "\n")
print("📚 Documentation References:\n") print("📚 Documentation References:\n")
print(" • Quick Start: scripts/QUICK_START.md") print(" • Quick Start: scripts/QUICK_START.md")
print(" • Complete Guide: scripts/DEPLOYMENT_GUIDE.md") print(" • Complete Guide: scripts/DEPLOYMENT_GUIDE.md")
@@ -91,12 +92,12 @@ def main():
print(" • Script Info: scripts/README.md") print(" • Script Info: scripts/README.md")
print(" • Test Coverage: pytest tests/scripts/test_deploy_filter.py -v") print(" • Test Coverage: pytest tests/scripts/test_deploy_filter.py -v")
print() print()
print("="*80 + "\n") print("=" * 80 + "\n")
return 0 return 0
else: else:
print("❌ Some files are missing!") print("❌ Some files are missing!")
print("="*80 + "\n") print("=" * 80 + "\n")
return 1 return 1

View File

@@ -66,7 +66,7 @@ def test_build_payload_uses_native_tool_shape_for_tools():
"description": "Demo tool description", "description": "Demo tool description",
"openwebui_id": "12345678-1234-1234-1234-123456789abc", "openwebui_id": "12345678-1234-1234-1234-123456789abc",
}, },
content='class Tools:\n pass\n', content="class Tools:\n pass\n",
function_id="demo_tool", function_id="demo_tool",
) )
@@ -79,7 +79,7 @@ def test_build_payload_uses_native_tool_shape_for_tools():
"description": "Demo tool description", "description": "Demo tool description",
"manifest": {}, "manifest": {},
}, },
"content": 'class Tools:\n pass\n', "content": "class Tools:\n pass\n",
"access_grants": [], "access_grants": [],
} }
@@ -89,7 +89,7 @@ def test_build_api_urls_uses_tool_endpoints_for_tools():
plugin_type="tool", plugin_type="tool",
file_path=Path("plugins/tools/demo/demo_tool.py"), file_path=Path("plugins/tools/demo/demo_tool.py"),
metadata={"title": "Demo Tool"}, metadata={"title": "Demo Tool"},
content='class Tools:\n pass\n', content="class Tools:\n pass\n",
function_id="demo_tool", function_id="demo_tool",
) )
@@ -101,7 +101,9 @@ def test_build_api_urls_uses_tool_endpoints_for_tools():
assert create_url == "http://localhost:3000/api/v1/tools/create" assert create_url == "http://localhost:3000/api/v1/tools/create"
def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, monkeypatch): def test_discover_plugins_only_returns_supported_openwebui_plugins(
tmp_path, monkeypatch
):
actions_dir = tmp_path / "plugins" / "actions" actions_dir = tmp_path / "plugins" / "actions"
filters_dir = tmp_path / "plugins" / "filters" filters_dir = tmp_path / "plugins" / "filters"
pipes_dir = tmp_path / "plugins" / "pipes" pipes_dir = tmp_path / "plugins" / "pipes"
@@ -110,7 +112,9 @@ def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, mon
write_plugin(actions_dir / "flash-card" / "flash_card.py", PLUGIN_HEADER) write_plugin(actions_dir / "flash-card" / "flash_card.py", PLUGIN_HEADER)
write_plugin(actions_dir / "flash-card" / "flash_card_cn.py", PLUGIN_HEADER) write_plugin(actions_dir / "flash-card" / "flash_card_cn.py", PLUGIN_HEADER)
write_plugin(actions_dir / "infographic" / "verify_generation.py", PLUGIN_HEADER) write_plugin(actions_dir / "infographic" / "verify_generation.py", PLUGIN_HEADER)
write_plugin(filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n') write_plugin(
filters_dir / "missing-id" / "missing_id.py", '"""\ntitle: Missing ID\n"""\n'
)
write_plugin(pipes_dir / "sdk" / "github_copilot_sdk.py", PLUGIN_HEADER) write_plugin(pipes_dir / "sdk" / "github_copilot_sdk.py", PLUGIN_HEADER)
write_plugin(tools_dir / "skills" / "openwebui_skills_manager.py", PLUGIN_HEADER) write_plugin(tools_dir / "skills" / "openwebui_skills_manager.py", PLUGIN_HEADER)
@@ -150,7 +154,9 @@ def test_discover_plugins_only_returns_supported_openwebui_plugins(tmp_path, mon
("class Action:\n pass\n", "missing plugin header"), ("class Action:\n pass\n", "missing plugin header"),
], ],
) )
def test_discover_plugins_reports_missing_metadata(tmp_path, monkeypatch, header, expected_reason): def test_discover_plugins_reports_missing_metadata(
tmp_path, monkeypatch, header, expected_reason
):
action_dir = tmp_path / "plugins" / "actions" action_dir = tmp_path / "plugins" / "actions"
plugin_file = action_dir / "demo" / "demo.py" plugin_file = action_dir / "demo" / "demo.py"
write_plugin(plugin_file, header) write_plugin(plugin_file, header)