From fdf95a2825045a88ee495439c2ecfa5a29a4eedd Mon Sep 17 00:00:00 2001 From: fujie Date: Wed, 11 Mar 2026 23:32:00 +0800 Subject: [PATCH] feat: Implement configurable OpenWebUI base URL for deployment scripts and update documentation. --- .gitignore | 1 + .../async_context_compression.py | 12 +- .../github_copilot_sdk_files_filter.py | 1 + scripts/DEPLOYMENT_GUIDE.md | 12 +- scripts/DEPLOYMENT_SUMMARY.md | 8 ++ scripts/README.md | 12 +- scripts/UPDATE_MECHANISM.md | 11 ++ scripts/deploy_async_context_compression.py | 10 +- scripts/deploy_filter.py | 108 ++++++++++++------ scripts/deploy_tool.py | 69 +++++------ scripts/install_all_plugins.py | 20 ++-- scripts/verify_deployment_tools.py | 49 ++++---- tests/scripts/test_install_all_plugins.py | 18 ++- 13 files changed, 203 insertions(+), 128 deletions(-) diff --git a/.gitignore b/.gitignore index d77653b..5fffba8 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,4 @@ logs/ # OpenWebUI specific # Add any specific ignores for OpenWebUI plugins if needed .git-worktrees/ +plugins/filters/auth_model_info/ diff --git a/plugins/filters/async-context-compression/async_context_compression.py b/plugins/filters/async-context-compression/async_context_compression.py index fcdf0b3..44c7304 100644 --- a/plugins/filters/async-context-compression/async_context_compression.py +++ b/plugins/filters/async-context-compression/async_context_compression.py @@ -1886,19 +1886,9 @@ class Filter: """ Check if compression should be skipped. Returns True if: - 1. The base model includes 'copilot_sdk' """ - # Check if base model includes copilot_sdk - 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(): + if body.get("is_copilot_model", False): return True - return False async def inlet( diff --git a/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter.py b/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter.py index a163f0e..e989486 100644 --- a/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter.py +++ b/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter.py @@ -114,6 +114,7 @@ class Filter: # Check if it's a Copilot model is_copilot_model = self._is_copilot_model(current_model) + body["is_copilot_model"] = is_copilot_model await self._emit_debug_log( __event_emitter__, diff --git a/scripts/DEPLOYMENT_GUIDE.md b/scripts/DEPLOYMENT_GUIDE.md index e23cc55..8588b29 100644 --- a/scripts/DEPLOYMENT_GUIDE.md +++ b/scripts/DEPLOYMENT_GUIDE.md @@ -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`) 2. **API Key**: You need a valid OpenWebUI API key 3. **Environment File**: Create a `.env` file in this directory containing your API key: + ``` 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.). **Key Features**: + - ✅ Auto-extracts metadata from Python files (version, author, description, etc.) - ✅ Attempts to update existing plugins, creates if not found - ✅ Supports multiple Filter plugin management - ✅ Detailed error messages and connection diagnostics **Usage**: + ```bash # Deploy async_context_compression (default) python deploy_filter.py @@ -62,6 +65,7 @@ python deploy_filter.py -l ``` **Workflow**: + 1. Load API key from `.env` 2. Find target Filter plugin directory 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). **Usage**: + ```bash 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 **Solution**: + - Make sure OpenWebUI is running - Check which port OpenWebUI is actually listening on (usually 3000) - 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 **Solution**: + ```bash 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 **Solution**: + ```bash # List all available Filters python deploy_filter.py --list @@ -129,6 +137,7 @@ python deploy_filter.py --list **Cause**: API key is invalid or expired **Solution**: + 1. Verify your API key is valid 2. Generate a new API key 3. Update the `.env` file @@ -177,7 +186,8 @@ python deploy_filter.py async-context-compression ## Security Considerations -⚠️ **Important**: +⚠️ **Important**: + - ✅ Add `.env` file to `.gitignore` (avoid committing sensitive info) - ✅ Never commit API keys to version control - ✅ Use only on trusted networks diff --git a/scripts/DEPLOYMENT_SUMMARY.md b/scripts/DEPLOYMENT_SUMMARY.md index b4b87d8..ad5c6a8 100644 --- a/scripts/DEPLOYMENT_SUMMARY.md +++ b/scripts/DEPLOYMENT_SUMMARY.md @@ -7,6 +7,7 @@ Added a complete local deployment toolchain for the `async_context_compression` ## 📋 New Files ### 1. **deploy_filter.py** — Filter Plugin Deployment Script + - **Location**: `scripts/deploy_filter.py` - **Function**: Auto-deploy Filter-type plugins to local OpenWebUI instance - **Features**: @@ -19,6 +20,7 @@ Added a complete local deployment toolchain for the `async_context_compression` - **Code Lines**: ~300 ### 2. **DEPLOYMENT_GUIDE.md** — Complete Deployment Guide + - **Location**: `scripts/DEPLOYMENT_GUIDE.md` - **Contents**: - Prerequisites and quick start @@ -28,6 +30,7 @@ Added a complete local deployment toolchain for the `async_context_compression` - Step-by-step workflow examples ### 3. **QUICK_START.md** — Quick Reference Card + - **Location**: `scripts/QUICK_START.md` - **Contents**: - One-line deployment command @@ -37,6 +40,7 @@ Added a complete local deployment toolchain for the `async_context_compression` - CI/CD integration examples ### 4. **test_deploy_filter.py** — Unit Test Suite + - **Location**: `tests/scripts/test_deploy_filter.py` - **Test Coverage**: - ✅ Filter file discovery (3 tests) @@ -138,6 +142,7 @@ openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce ``` **Supported Metadata Fields**: + - `title` — Filter display name ✅ - `id` — Unique identifier ✅ - `author` — Author name ✅ @@ -335,17 +340,20 @@ Metadata Extraction and Delivery ### Debugging Tips 1. **Enable Verbose Logging**: + ```bash python deploy_filter.py 2>&1 | tee deploy.log ``` 2. **Test API Connection**: + ```bash curl -X GET http://localhost:3000/api/v1/functions \ -H "Authorization: Bearer $API_KEY" ``` 3. **Verify .env File**: + ```bash grep "api_key=" scripts/.env ``` diff --git a/scripts/README.md b/scripts/README.md index 5d2cf5f..7bb6882 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -73,12 +73,14 @@ python deploy_async_context_compression.py ``` **Features**: + - ✅ Optimized specifically for async_context_compression - ✅ Clear deployment steps and confirmation - ✅ Friendly error messages - ✅ Shows next steps after successful deployment **Sample Output**: + ``` ====================================================================== 🚀 Deploying Async Context Compression Filter Plugin @@ -117,6 +119,7 @@ python deploy_filter.py --list ``` **Features**: + - ✅ Generic Filter deployment tool - ✅ Supports multiple plugins - ✅ Auto metadata extraction @@ -142,6 +145,7 @@ python deploy_tool.py openwebui-skills-manager ``` **Features**: + - ✅ Supports Tools plugin deployment - ✅ Auto-detects `Tools` class definition - ✅ Smart update/create logic @@ -290,6 +294,7 @@ git status # should not show .env ``` **Solution**: + ```bash # 1. Check if OpenWebUI is running curl http://localhost:3000 @@ -309,6 +314,7 @@ curl http://localhost:3000 ``` **Solution**: + ```bash echo "api_key=sk-your-api-key" > .env cat .env # verify file created @@ -321,6 +327,7 @@ cat .env # verify file created ``` **Solution**: + ```bash # List all available Filters python deploy_filter.py --list @@ -337,6 +344,7 @@ python deploy_filter.py async-context-compression ``` **Solution**: + ```bash # 1. Verify API key is correct grep "api_key=" .env @@ -370,7 +378,7 @@ python deploy_async_context_compression.py ### Method 2: Verify in OpenWebUI -1. Open OpenWebUI: http://localhost:3000 +1. Open OpenWebUI: 2. Go to Settings → Filters 3. Check if 'Async Context Compression' is listed 4. Verify version number is correct (should be latest) @@ -380,6 +388,7 @@ python deploy_async_context_compression.py 1. Open a new conversation 2. Enable 'Async Context Compression' Filter 3. Have multiple-turn conversation and verify compression/summarization works + ## 💡 Advanced Usage ### Automated Deploy & Test @@ -473,4 +482,3 @@ Newly created deployment-related files: **Last Updated**: 2026-03-09 **Script Status**: ✅ Ready for production **Test Coverage**: 10/10 passed ✅ - diff --git a/scripts/UPDATE_MECHANISM.md b/scripts/UPDATE_MECHANISM.md index fdcf98e..ee9ad37 100644 --- a/scripts/UPDATE_MECHANISM.md +++ b/scripts/UPDATE_MECHANISM.md @@ -5,6 +5,7 @@ ✅ **Yes, re-deploying automatically updates the plugin!** The deployment script uses a **smart two-stage strategy**: + 1. 🔄 **Try UPDATE First** (if plugin exists) 2. 📝 **Auto CREATE** (if update fails — plugin doesn't exist) @@ -54,6 +55,7 @@ if response.status_code == 200: ``` **What Happens**: + - Send **POST** to `/api/v1/functions/id/{filter_id}/update` - If returns **HTTP 200**, plugin exists and update succeeded - Includes: @@ -84,6 +86,7 @@ if response.status_code != 200: ``` **What Happens**: + - If update fails (HTTP ≠ 200), auto-attempt create - Send **POST** to `/api/v1/functions/create` - Uses **same payload** (code, metadata identical) @@ -103,6 +106,7 @@ $ python deploy_async_context_compression.py ``` **What Happens**: + 1. Try UPDATE → fails (HTTP 404 — plugin doesn't exist) 2. Auto-try CREATE → succeeds (HTTP 200) 3. Plugin created in OpenWebUI @@ -121,6 +125,7 @@ $ python deploy_async_context_compression.py ``` **What Happens**: + 1. Read modified code 2. Try UPDATE → succeeds (HTTP 200 — plugin exists) 3. Plugin in OpenWebUI updated to latest code @@ -147,6 +152,7 @@ $ python deploy_async_context_compression.py ``` **Characteristics**: + - 🚀 Each update takes only 5 seconds - 📝 Each is an incremental update - ✅ No need to restart OpenWebUI @@ -181,11 +187,13 @@ version: 1.3.0 ``` **Each deployment**: + 1. Script reads version from docstring 2. Sends this version in manifest to OpenWebUI 3. If you change version in code, deployment updates to new version **Best Practice**: + ```bash # 1. Modify code vim async_context_compression.py @@ -300,6 +308,7 @@ Usually **not needed** because: 4. ✅ Failures auto-rollback 但如果真的需要控制,可以: + - 手动修改脚本 (修改 `deploy_filter.py`) - 或分别使用 UPDATE/CREATE 的具体 API 端点 @@ -323,6 +332,7 @@ Usually **not needed** because: ### Q: 可以同时部署多个插件吗? ✅ **可以!** + ```bash python deploy_filter.py async-context-compression python deploy_filter.py folder-memory @@ -337,6 +347,7 @@ python deploy_filter.py context_enhancement_filter --- **总结**: 部署脚本的更新机制完全自动化,开发者只需修改代码,每次运行 `deploy_async_context_compression.py` 就会自动: + 1. ✅ 创建(第一次)或更新(后续)插件 2. ✅ 从代码提取最新的元数据和版本号 3. ✅ 立即生效,无需重启 OpenWebUI diff --git a/scripts/deploy_async_context_compression.py b/scripts/deploy_async_context_compression.py index cd0d094..d00f7b2 100644 --- a/scripts/deploy_async_context_compression.py +++ b/scripts/deploy_async_context_compression.py @@ -11,9 +11,9 @@ Usage: To get started: 1. Create .env file with your OpenWebUI API key: echo "api_key=sk-your-key-here" > .env - + 2. Make sure OpenWebUI is running on localhost:3000 - + 3. Run this script: python deploy_async_context_compression.py """ @@ -34,10 +34,10 @@ def main(): print("🚀 Deploying Async Context Compression Filter Plugin") print("=" * 70) print() - + # Deploy the filter success = deploy_filter("async-context-compression") - + if success: print() print("=" * 70) @@ -63,7 +63,7 @@ def main(): print(" • Check network connectivity") print() return 1 - + return 0 diff --git a/scripts/deploy_filter.py b/scripts/deploy_filter.py index 8202cd7..54791b4 100644 --- a/scripts/deploy_filter.py +++ b/scripts/deploy_filter.py @@ -49,53 +49,78 @@ def _load_api_key() -> str: 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]: """Find the main Python file for a filter. - + Args: filter_name: Directory name of the filter (e.g., 'async-context-compression') - + Returns: Path to the main Python file, or None if not found. """ filter_dir = FILTERS_DIR / filter_name if not filter_dir.exists(): return None - + # Try to find a .py file matching the filter name py_files = list(filter_dir.glob("*.py")) - + # Prefer a file with the filter name (with hyphens converted to underscores) preferred_name = filter_name.replace("-", "_") + ".py" for py_file in py_files: if py_file.name == preferred_name: return py_file - + # Otherwise, return the first .py file (usually the only one) if py_files: return py_files[0] - + return None def _extract_metadata(content: str) -> Dict[str, Any]: """Extract metadata from the plugin docstring. - + Args: content: Python file content - + Returns: Dictionary with extracted metadata (title, author, version, etc.) """ metadata = {} - + # Extract docstring match = re.search(r'"""(.*?)"""', content, re.DOTALL) if not match: return metadata - + docstring = match.group(1) - + # Extract key-value pairs for line in docstring.split("\n"): line = line.strip() @@ -104,7 +129,7 @@ def _extract_metadata(content: str) -> Dict[str, Any]: key = parts[0].strip().lower() value = parts[1].strip() metadata[key] = value - + return metadata @@ -112,13 +137,13 @@ def _build_filter_payload( filter_name: str, file_path: Path, content: str, metadata: Dict[str, Any] ) -> Dict[str, Any]: """Build the payload for the filter update/create API. - + Args: filter_name: Directory name of the filter file_path: Path to the plugin file content: File content metadata: Extracted metadata - + Returns: Payload dictionary ready for API submission """ @@ -126,12 +151,14 @@ def _build_filter_payload( filter_id = metadata.get("id", filter_name).replace("-", "_") title = metadata.get("title", filter_name) 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") description = metadata.get("description", f"Filter plugin: {title}") version = metadata.get("version", "1.0.0") openwebui_id = metadata.get("openwebui_id", "") - + payload = { "id": filter_id, "name": title, @@ -150,20 +177,20 @@ def _build_filter_payload( }, "content": content, } - + # Add openwebui_id if available if openwebui_id: payload["meta"]["manifest"]["openwebui_id"] = openwebui_id - + return payload def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: """Deploy a filter plugin to OpenWebUI. - + Args: filter_name: Directory name of the filter to deploy - + Returns: 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") metadata = _extract_metadata(content) - + if not metadata: print(f"[ERROR] Could not extract metadata from {file_path}") return False @@ -211,12 +238,14 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: } # 6. Send update request - update_url = "http://localhost:3000/api/v1/functions/id/{}/update".format(filter_id) - create_url = "http://localhost:3000/api/v1/functions/create" - + base_url = _load_openwebui_base_url() + 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" File: {file_path}") - + print(f" Target: {base_url}") + try: # Try update first response = requests.post( @@ -225,7 +254,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: data=json.dumps(payload), timeout=10, ) - + if response.status_code == 200: print(f"✅ Successfully updated '{title}' filter!") return True @@ -234,7 +263,7 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: f"⚠️ Update failed with status {response.status_code}, " "attempting to create instead..." ) - + # Try create if update fails res_create = requests.post( create_url, @@ -242,23 +271,24 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: data=json.dumps(payload), timeout=10, ) - + if res_create.status_code == 200: print(f"✅ Successfully created '{title}' filter!") return True 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: error_msg = res_create.json() print(f" Error: {error_msg}") except: print(f" Response: {res_create.text[:500]}") return False - + except requests.exceptions.ConnectionError: - print( - "❌ Connection error: Could not reach OpenWebUI at localhost:3000" - ) + base_url = _load_openwebui_base_url() + print(f"❌ Connection error: Could not reach OpenWebUI at {base_url}") print(" Make sure OpenWebUI is running and accessible.") return False except requests.exceptions.Timeout: @@ -272,16 +302,20 @@ def deploy_filter(filter_name: str = DEFAULT_FILTER) -> bool: def list_filters() -> None: """List all 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: print(" (No filters found)") return - + for filter_name in sorted(filters): filter_dir = FILTERS_DIR / filter_name py_file = _find_filter_file(filter_name) - + if py_file: content = py_file.read_text(encoding="utf-8") metadata = _extract_metadata(content) diff --git a/scripts/deploy_tool.py b/scripts/deploy_tool.py index ddbf7c0..e6de866 100644 --- a/scripts/deploy_tool.py +++ b/scripts/deploy_tool.py @@ -76,52 +76,51 @@ def _get_base_url() -> str: if not base_url: raise ValueError( - f"Missing url. Please create {ENV_FILE} with: " - "url=http://localhost:3000" + f"Missing url. Please create {ENV_FILE} with: " "url=http://localhost:3000" ) return base_url.rstrip("/") def _find_tool_file(tool_name: str) -> Optional[Path]: """Find the main Python file for a tool. - + Args: tool_name: Directory name of the tool (e.g., 'openwebui-skills-manager') - + Returns: Path to the main Python file, or None if not found. """ tool_dir = TOOLS_DIR / tool_name if not tool_dir.exists(): return None - + # Try to find a .py file matching the tool name py_files = list(tool_dir.glob("*.py")) - + # Prefer a file with the tool name (with hyphens converted to underscores) preferred_name = tool_name.replace("-", "_") + ".py" for py_file in py_files: if py_file.name == preferred_name: return py_file - + # Otherwise, return the first .py file (usually the only one) if py_files: return py_files[0] - + return None def _extract_metadata(content: str) -> Dict[str, Any]: """Extract metadata from the plugin docstring.""" metadata = {} - + # Extract docstring match = re.search(r'"""(.*?)"""', content, re.DOTALL) if not match: return metadata - + docstring = match.group(1) - + # Extract key-value pairs for line in docstring.split("\n"): line = line.strip() @@ -130,7 +129,7 @@ def _extract_metadata(content: str) -> Dict[str, Any]: key = parts[0].strip().lower() value = parts[1].strip() metadata[key] = value - + return metadata @@ -141,12 +140,14 @@ def _build_tool_payload( tool_id = metadata.get("id", tool_name).replace("-", "_") title = metadata.get("title", tool_name) 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") description = metadata.get("description", f"Tool plugin: {title}") version = metadata.get("version", "1.0.0") openwebui_id = metadata.get("openwebui_id", "") - + payload = { "id": tool_id, "name": title, @@ -165,20 +166,20 @@ def _build_tool_payload( }, "content": content, } - + # Add openwebui_id if available if openwebui_id: payload["meta"]["manifest"]["openwebui_id"] = openwebui_id - + return payload def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: """Deploy a tool plugin to OpenWebUI. - + Args: tool_name: Directory name of the tool to deploy - + Returns: 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") metadata = _extract_metadata(content) - + if not metadata: print(f"[ERROR] Could not extract metadata from {file_path}") return False @@ -229,10 +230,10 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: # 6. Send update request through the native tool endpoints update_url = f"{base_url}/api/v1/tools/id/{tool_id}/update" create_url = f"{base_url}/api/v1/tools/create" - + print(f"📦 Deploying tool '{title}' (version {version})...") print(f" File: {file_path}") - + try: # Try update first response = requests.post( @@ -241,7 +242,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: data=json.dumps(payload), timeout=10, ) - + if response.status_code == 200: print(f"✅ Successfully updated '{title}' tool!") return True @@ -250,7 +251,7 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: f"⚠️ Update failed with status {response.status_code}, " "attempting to create instead..." ) - + # Try create if update fails res_create = requests.post( create_url, @@ -258,23 +259,23 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: data=json.dumps(payload), timeout=10, ) - + if res_create.status_code == 200: print(f"✅ Successfully created '{title}' tool!") return True 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: error_msg = res_create.json() print(f" Error: {error_msg}") except: print(f" Response: {res_create.text[:500]}") return False - + except requests.exceptions.ConnectionError: - print( - "❌ Connection error: Could not reach OpenWebUI at {base_url}" - ) + print("❌ Connection error: Could not reach OpenWebUI at {base_url}") print(" Make sure OpenWebUI is running and accessible.") return False except requests.exceptions.Timeout: @@ -288,16 +289,18 @@ def deploy_tool(tool_name: str = DEFAULT_TOOL) -> bool: def list_tools() -> None: """List all 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: print(" (No tools found)") return - + for tool_name in sorted(tools): tool_dir = TOOLS_DIR / tool_name py_file = _find_tool_file(tool_name) - + if py_file: content = py_file.read_text(encoding="utf-8") metadata = _extract_metadata(content) diff --git a/scripts/install_all_plugins.py b/scripts/install_all_plugins.py index 16112d7..d3bd3c2 100644 --- a/scripts/install_all_plugins.py +++ b/scripts/install_all_plugins.py @@ -187,9 +187,7 @@ def build_payload(candidate: PluginCandidate) -> Dict[str, object]: manifest = dict(candidate.metadata) manifest.setdefault("title", candidate.title) manifest.setdefault("author", "Fu-Jie") - manifest.setdefault( - "author_url", "https://github.com/Fu-Jie/openwebui-extensions" - ) + manifest.setdefault("author_url", "https://github.com/Fu-Jie/openwebui-extensions") manifest.setdefault("funding_url", "https://github.com/open-webui") manifest.setdefault( "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] = [] skipped: List[Tuple[Path, str]] = [] @@ -344,7 +344,9 @@ def print_skipped_summary(skipped: Sequence[Tuple[Path, str]]) -> None: for _, reason in skipped: 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}).") @@ -421,19 +423,19 @@ def main(argv: Optional[Sequence[str]] = None) -> int: failed_candidates.append(candidate) print(f" [FAILED] {message}") - print(f"\n" + "="*80) + print(f"\n" + "=" * 80) print( f"Finished: {success_count}/{len(candidates)} plugins installed successfully." ) - + if failed_candidates: print(f"\n❌ {len(failed_candidates)} plugin(s) failed to install:") for candidate in failed_candidates: print(f" • {candidate.title} ({candidate.plugin_type})") print(f" → Check the error message above") print() - - print("="*80) + + print("=" * 80) return 0 if success_count == len(candidates) else 1 diff --git a/scripts/verify_deployment_tools.py b/scripts/verify_deployment_tools.py index 877f5d0..631fc05 100644 --- a/scripts/verify_deployment_tools.py +++ b/scripts/verify_deployment_tools.py @@ -9,14 +9,15 @@ local deployment are present and functional. import sys from pathlib import Path + def main(): """Check all deployment tools are ready.""" base_dir = Path(__file__).parent.parent - - print("\n" + "="*80) + + print("\n" + "=" * 80) print("✨ Async Context Compression Local Deployment Tools — Verification Status") - print("="*80 + "\n") - + print("=" * 80 + "\n") + files_to_check = { "🐍 Python Scripts": [ "scripts/deploy_async_context_compression.py", @@ -34,56 +35,56 @@ def main(): "tests/scripts/test_deploy_filter.py", ], } - + all_exist = True - + for category, files in files_to_check.items(): print(f"\n{category}:") print("-" * 80) - + for file_path in files: full_path = base_dir / file_path exists = full_path.exists() status = "✅" if exists else "❌" - + print(f" {status} {file_path}") - + if exists and file_path.endswith(".py"): 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]") - + if not exists: all_exist = False - - print("\n" + "="*80) - + + print("\n" + "=" * 80) + if all_exist: print("✅ All deployment tool files are ready!") - print("="*80 + "\n") - + print("=" * 80 + "\n") + print("🚀 Quick Start (3 ways):\n") - + print(" Method 1: Easiest (Recommended)") print(" ─────────────────────────────────────────────────────────") print(" cd scripts") print(" python deploy_async_context_compression.py") print() - + print(" Method 2: Generic Tool") print(" ─────────────────────────────────────────────────────────") print(" cd scripts") print(" python deploy_filter.py") print() - + print(" Method 3: Deploy Other Filters") print(" ─────────────────────────────────────────────────────────") print(" cd scripts") print(" python deploy_filter.py --list") print(" python deploy_filter.py folder-memory") print() - - print("="*80 + "\n") + + print("=" * 80 + "\n") print("📚 Documentation References:\n") print(" • Quick Start: scripts/QUICK_START.md") print(" • Complete Guide: scripts/DEPLOYMENT_GUIDE.md") @@ -91,12 +92,12 @@ def main(): print(" • Script Info: scripts/README.md") print(" • Test Coverage: pytest tests/scripts/test_deploy_filter.py -v") print() - - print("="*80 + "\n") + + print("=" * 80 + "\n") return 0 else: print("❌ Some files are missing!") - print("="*80 + "\n") + print("=" * 80 + "\n") return 1 diff --git a/tests/scripts/test_install_all_plugins.py b/tests/scripts/test_install_all_plugins.py index 6d4978e..96a6d43 100644 --- a/tests/scripts/test_install_all_plugins.py +++ b/tests/scripts/test_install_all_plugins.py @@ -66,7 +66,7 @@ def test_build_payload_uses_native_tool_shape_for_tools(): "description": "Demo tool description", "openwebui_id": "12345678-1234-1234-1234-123456789abc", }, - content='class Tools:\n pass\n', + content="class Tools:\n pass\n", function_id="demo_tool", ) @@ -79,7 +79,7 @@ def test_build_payload_uses_native_tool_shape_for_tools(): "description": "Demo tool description", "manifest": {}, }, - "content": 'class Tools:\n pass\n', + "content": "class Tools:\n pass\n", "access_grants": [], } @@ -89,7 +89,7 @@ def test_build_api_urls_uses_tool_endpoints_for_tools(): plugin_type="tool", file_path=Path("plugins/tools/demo/demo_tool.py"), metadata={"title": "Demo Tool"}, - content='class Tools:\n pass\n', + content="class Tools:\n pass\n", 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" -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" filters_dir = tmp_path / "plugins" / "filters" 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_cn.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(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"), ], ) -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" plugin_file = action_dir / "demo" / "demo.py" write_plugin(plugin_file, header)