Files
Fu-Jie_openwebui-extensions/docs/en/plugin_development_guide.md

7.9 KiB

OpenWebUI Plugin Development Guide

This guide consolidates official documentation, SDK details, and best practices to provide a systematic tutorial for developers, from beginner to expert.

📚 Table of Contents

  1. Quick Start
  2. Core Concepts & SDK Details
  3. Deep Dive into Plugin Types
  4. Advanced Development Patterns
  5. Best Practices & Design Principles
  6. Troubleshooting

1. Quick Start

1.1 What are OpenWebUI Plugins?

OpenWebUI Plugins (officially called "Functions") are the primary way to extend the platform's capabilities. Running in a backend Python environment, they allow you to:

  • 🔌 Integrate New Models: Connect to Claude, Gemini, or custom RAGs via Pipes.
  • 🎨 Enhance Interaction: Add buttons (e.g., "Export", "Generate Chart") next to messages via Actions.
  • 🔧 Intervene in Processes: Modify data before requests or after responses (e.g., inject context, filter sensitive words) via Filters.

1.2 Your First Plugin (Hello World)

Save the following code as hello.py and upload it to the Functions panel in OpenWebUI:

"""
title: Hello World Action
author: Demo
version: 1.0.0
"""

from pydantic import BaseModel, Field
from typing import Optional

class Action:
    class Valves(BaseModel):
        greeting: str = Field(default="Hello", description="Greeting message")

    def __init__(self):
        self.valves = self.Valves()

    async def action(
        self,
        body: dict,
        __event_emitter__=None,
        __user__=None
    ) -> Optional[dict]:
        user_name = __user__.get("name", "Friend") if __user__ else "Friend"
        
        if __event_emitter__:
            await __event_emitter__({
                "type": "notification",
                "data": {"type": "success", "content": f"{self.valves.greeting}, {user_name}!"}
            })
        return body

2. Core Concepts & SDK Details

2.1 ⚠️ Important: Sync vs Async

OpenWebUI plugins run within an asyncio event loop.

  • Principle: All I/O operations (database, file, network) must be non-blocking.
  • Pitfall: Calling synchronous methods directly (e.g., time.sleep, requests.get) will freeze the entire server.
  • Solution: Wrap synchronous calls using await asyncio.to_thread(sync_func, ...).

2.2 Core Parameters

All plugin methods (inlet, outlet, pipe, action) support injecting the following special parameters:

Parameter Type Description
body dict Core Data. Contains request info like messages, model, stream.
__user__ dict Current User. Contains id, name, role, valves (user config), etc.
__metadata__ dict Metadata. Contains chat_id, message_id. The variables field holds preset variables like {{USER_NAME}}, {{CURRENT_TIME}}.
__request__ Request FastAPI Request Object. Access app.state for cross-plugin communication.
__event_emitter__ func One-way Notification. Used to send Toast notifications or status bar updates.
__event_call__ func Two-way Interaction. Used to execute JS code, show confirmation dialogs, or input boxes on the frontend.

2.3 Configuration System (Valves)

  • Valves: Global admin configuration.
  • UserValves: User-level configuration (higher priority, overrides global).
class Filter:
    class Valves(BaseModel):
        API_KEY: str = Field(default="", description="Global API Key")
        
    class UserValves(BaseModel):
        API_KEY: str = Field(default="", description="User Private API Key")
        
    def inlet(self, body, __user__):
        # Prioritize user's Key
        user_valves = __user__.get("valves", self.UserValves())
        api_key = user_valves.API_KEY or self.valves.API_KEY

3. Deep Dive into Plugin Types

3.1 Action

Role: Adds buttons below messages that trigger upon user click.

Advanced Usage: Execute JavaScript on Frontend (File Download Example)

import base64

async def action(self, body, __event_call__):
    # 1. Generate content on backend
    content = "Hello OpenWebUI".encode()
    b64 = base64.b64encode(content).decode()
    
    # 2. Send JS to frontend for execution
    js = f"""
    const blob = new Blob([atob('{b64}')], {{type: 'text/plain'}});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'hello.txt';
    a.click();
    """
    await __event_call__({"type": "execute", "data": {"code": js}})

3.2 Filter

Role: Middleware that intercepts and modifies requests/responses.

  • inlet: Before request. Used for injecting context, modifying model parameters.
  • outlet: After response. Used for formatting output, logging.
  • stream: During streaming. Used for real-time sensitive word filtering.

Example: Injecting Environment Variables

async def inlet(self, body, __metadata__):
    vars = __metadata__.get("variables", {})
    context = f"Current Time: {vars.get('{{CURRENT_DATETIME}}')}"
    
    # Inject into System Prompt or first message
    if body.get("messages"):
        body["messages"][0]["content"] += f"\n\n{context}"
    return body

3.3 Pipe

Role: Custom Model/Agent.

Example: Simple OpenAI Wrapper

import requests

class Pipe:
    def pipes(self):
        return [{"id": "my-gpt", "name": "My GPT Wrapper"}]

    def pipe(self, body):
        # Modify body here, e.g., force add prompt
        headers = {"Authorization": f"Bearer {self.valves.API_KEY}"}
        r = requests.post("https://api.openai.com/v1/chat/completions", json=body, headers=headers, stream=True)
        return r.iter_lines()

4. Advanced Development Patterns

4.1 Pipe & Filter Collaboration

Use __request__.app.state to share data between plugins.

  • Pipe: __request__.app.state.search_results = [...]
  • Filter (Outlet): Read search_results and format them as citation links appended to the response.

4.2 Async Background Tasks

Execute time-consuming operations (e.g., summarization, database storage) in the background without blocking the user response.

import asyncio

async def outlet(self, body, __metadata__):
    asyncio.create_task(self.background_job(__metadata__["chat_id"]))
    return body

async def background_job(self, chat_id):
    # Execute time-consuming operation...
    pass

5. Best Practices & Design Principles

5.1 Naming & Positioning

  • Short & Punchy: e.g., "FlashCard", "DeepRead". Avoid generic terms like "Text Analysis Assistant".
  • Complementary: Don't reinvent the wheel; clarify what specific problem your plugin solves.

5.2 User Experience (UX)

  • Timely Feedback: Send a notification ("Generating...") before time-consuming operations.
  • Visual Appeal: When Action outputs HTML, use modern CSS (rounded corners, shadows, gradients).
  • Smart Guidance: If text is too short, prompt the user: "Suggest entering more content for better results".

5.3 Error Handling

Never let a plugin fail silently. Catch exceptions and inform the user via __event_emitter__.

try:
    # Business logic
except Exception as e:
    await __event_emitter__({
        "type": "notification",
        "data": {"type": "error", "content": f"Processing failed: {str(e)}"}
    })

6. Troubleshooting

  • HTML not showing? Ensure it's wrapped in a ```html ... ``` code block.
  • Database error? Check if you called synchronous DB methods directly in an async function; use asyncio.to_thread.
  • Parameters not working? Check if Valves are defined correctly and if they are being overridden by UserValves.