Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Künstliche Intelligenz 17.06.2026 · 11 min Lesezeit

Open WebUI erweitern: Tools, Functions und Pipelines selbst bauen und einbinden

Von der Chat-Oberfläche zur KI-Plattform: Eigene Python-Tools für Kalenderabfragen, GLPI-Tickets und SearXNG-Suche in Open WebUI einbinden – Schritt für Schritt, ohne Cloud-Abhängigkeit.

Moderne IT Grafik zur Erweiterung von Open WebUI mit eigenen Tools, Functions und Pipelines. Darstellung einer technischen Oberfläche mit Code Beispiel, Integrationen, Workflow Symbolen und sicherer lokaler KI Umgebung.

Open WebUI ist weit mehr als ein Chat-Frontend für Ollama: Mit eigenen Python-Erweiterungen verwandelst du die Oberfläche in eine vollständige KI-Plattform, die Kalender abruft, Helpdesk-Tickets anlegt und im lokalen SearXNG sucht – alles ohne dass eine einzige Anfrage das eigene Netzwerk verlässt. Diese Anleitung richtet sich an IT-Admins und ambitionierte Selfhoster, die über das bloße Modell-Testen hinausgehen wollen und bereit sind, ein paar Zeilen Python zu schreiben.

Voraussetzungen

  1. Open WebUI ab Version 0.4.x, laufend als Docker-Container
  2. Zugang zum Admin-Panel (Administratorrolle)
  3. Ein Tool-Use-fähiges LLM lokal via Ollama (empfohlen: llama3.1:8b, qwen2.5:14b, mistral-nemo) oder remote per API
  4. Python-Grundkenntnisse: Klassen, async/await, Type Hints
  5. API-Zugangsdaten für einzubindende Dienste (z. B. GLPI App-Token + User-Token, SearXNG-URL)
  6. Docker-Umgebung (für Pipelines zusätzlich: freier Port 9099)

Schritt 1: Die drei Erweiterungsebenen verstehen

Bevor du Code schreibst, lohnt sich ein Blick auf die Architektur. Open WebUI unterscheidet drei klar voneinander getrennte Erweiterungstypen, die sich in Ausführungsort, Berechtigung und Anwendungsfall unterscheiden:

MerkmalToolsFunctions (Pipe/Filter/Action)Pipelines (Legacy)
AusführungsortOpen-WebUI-ProzessOpen-WebUI-ProzessSeparater Container (Port 9099)
Wer ruft aufLLM (Function Calling)Automatisch / Event-basiertAls Modell-Endpunkt
pip-PaketeJa (via requirements-Header)Nein (nur vorinstallierte Libs)Ja (volle Kontrolle)
Klassenname (Pflicht)ToolsPipe / Filter / ActionPipeline
ZugriffWorkspace (Nutzer)Admin-Panel onlyAdmin-Panel (URL-Verbindung)
EmpfohlenJaJaNein (Legacy)
Typischer UsecaseKalender, Tickets, APIs, SuchePII-Filter, Custom-Backend, Chat-ButtonsRAG, Multi-Modell-Orchestrierung (alt)

Der entscheidende Unterschied zwischen Tool und Filter: Ein Tool ruft das LLM selbst auf – es entscheidet anhand deiner Docstrings, ob und wann es die Methode braucht. Ein Filter läuft immer, unsichtbar für das Modell, bei jeder Nachricht. Wähle Tools für optionale Aktionen, Filter für obligatorische Transformationen.

Schritt 2: Ein erstes Tool schreiben – GLPI-Ticket-Ersteller

Tools sind Python-Dateien mit einer Klasse namens Tools – der Klassenname ist nicht optional, sondern Pflicht. Open WebUI erkennt den Typ automatisch daran. Jede Methode, die das LLM aufrufen soll, braucht vollständige Type Hints und einen aussagekräftigen Sphinx-Docstring. Schlechte Docstrings führen direkt zu schlechtem Tool-Calling, weil das LLM sie als JSON-Schema liest.

"""
title: GLPI Ticket Creator
author: Marcel Schoenfelder
description: Erstellt GLPI-Tickets aus dem Chat heraus
requirements: requests>=2.31.0
version: 1.0.0
license: MIT
"""

import requests
from pydantic import BaseModel, Field
from typing import Callable, Any

class Tools:
    class Valves(BaseModel):
        GLPI_URL: str = Field(
            default="https://glpi.example.com",
            description="GLPI-Server-URL"
        )
        GLPI_API_TOKEN: str = Field(
            default="",
            description="GLPI App-Token"
        )
        GLPI_USER_TOKEN: str = Field(
            default="",
            description="GLPI User-Token"
        )

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

    async def create_ticket(
        self,
        title: str,
        description: str,
        priority: int = 3,
        __event_emitter__: Callable[[dict], Any] = None,
        __user__: dict = {}
    ) -> str:
        """
        Erstellt ein neues Ticket im GLPI-Helpdesk-System.
        :param title: Kurztitel des Tickets (Pflicht).
        :param description: Ausfuehrliche Fehlerbeschreibung (Pflicht).
        :param priority: Prioritaet 1=sehr hoch bis 6=sehr niedrig, Standard 3=mittel.
        :return: Ticket-ID und Link als Bestaetigung.
        """
        if __event_emitter__:
            await __event_emitter__({
                "type": "status",
                "data": {"description": "Erstelle GLPI-Ticket...", "done": False}
            })

        headers = {
            "Content-Type": "application/json",
            "App-Token": self.valves.GLPI_API_TOKEN,
            "Authorization": f"user_token {self.valves.GLPI_USER_TOKEN}"
        }
        payload = {
            "input": {
                "name": title,
                "content": description,
                "priority": priority
            }
        }
        try:
            resp = requests.post(
                f"{self.valves.GLPI_URL}/apirest.php/Ticket",
                json=payload, headers=headers, timeout=10
            )
            resp.raise_for_status()
            ticket_id = resp.json().get("id", "unbekannt")

            if __event_emitter__:
                await __event_emitter__({
                    "type": "status",
                    "data": {"description": f"Ticket #{ticket_id} erstellt!", "done": True}
                })
            return (
                f"Ticket erstellt: ID #{ticket_id} | "
                f"URL: {self.valves.GLPI_URL}/front/ticket.form.php?id={ticket_id}"
            )
        except requests.RequestException as e:
            return f"Fehler beim Erstellen des Tickets: {str(e)}"

Einige Details, die den Unterschied machen: self.citation = True aktiviert automatische Quellenangaben im Chat, die zeigen, welches Tool verwendet wurde. Der __event_emitter__-Parameter ermöglicht Echtzeit-Statusmeldungen während der Ausführung. Die Valves-Klasse speichert Konfigurationswerte (API-Keys, URLs) sicher in der Open-WebUI-Datenbank – niemals hartcodiert im Skript.

Schritt 3: SearXNG-Such-Tool einbinden

Wer eine lokale SearXNG-Instanz betreibt, kann dem LLM damit echte Websuche geben, ohne externe APIs zu bemühen:

"""
title: SearXNG Web Search
author: s-edv.com
description: Sucht im lokalen SearXNG-Suchserver
requirements: requests>=2.31.0
version: 1.0.0
"""

import requests
from pydantic import BaseModel, Field
from typing import Callable, Any

class Tools:
    class Valves(BaseModel):
        SEARXNG_URL: str = Field(
            default="http://localhost:8080",
            description="URL der lokalen SearXNG-Instanz"
        )
        MAX_RESULTS: int = Field(
            default=5,
            description="Maximale Anzahl Suchergebnisse"
        )

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

    async def search_web(
        self,
        query: str,
        __event_emitter__: Callable[[dict], Any] = None
    ) -> str:
        """
        Sucht im lokalen SearXNG nach aktuellen Informationen.
        :param query: Suchanfrage als Stichwort oder Frage.
        :return: Liste der Suchergebnisse mit Titel, URL und Snippet.
        """
        if __event_emitter__:
            await __event_emitter__({"type": "status",
                "data": {"description": f"Suche nach: {query}", "done": False}})
        try:
            resp = requests.get(
                f"{self.valves.SEARXNG_URL}/search",
                params={"q": query, "format": "json",
                        "engines": "google,bing,duckduckgo"},
                timeout=10
            )
            resp.raise_for_status()
            results = resp.json().get("results", [])[:self.valves.MAX_RESULTS]
            if not results:
                return "Keine Ergebnisse gefunden."
            output = []
            for i, r in enumerate(results, 1):
                output.append(
                    f"{i}. {r.get('title','')}\n"
                    f"   {r.get('url','')}\n"
                    f"   {r.get('content','')[:200]}"
                )
            if __event_emitter__:
                await __event_emitter__({"type": "status",
                    "data": {"description": f"{len(results)} Ergebnisse gefunden", "done": True}})
            return "\n\n".join(output)
        except Exception as e:
            return f"Suchfehler: {str(e)}"

Schritt 4: Tool im Admin-Panel registrieren und zuweisen

Tools werden direkt im Browser über den eingebauten Code-Editor eingespielt – kein SSH, kein Datei-Upload nötig:

  1. Im Open WebUI: Admin Panel → Workspace → Tools → "+" (Neu)
  2. Python-Code einfügen, Titel und Beschreibung vergeben, speichern
  3. Pakete aus dem requirements-Header werden beim ersten Aufruf automatisch installiert
  4. Tool dem Modell zuweisen: Workspace → Models → Modell bearbeiten → Tools-Tab → Tool aktivieren
  5. Alternativ pro Chat: Chat-Einstellungen → Tools → gewünschte Tools aktivieren
  6. Valves (API-Keys, URLs) konfigurieren: Workspace → Tools → Zahnrad-Icon neben dem Tool

Verifizieren: Starte einen Chat mit dem Tool-fähigen Modell und schreibe z. B. "Erstelle ein GLPI-Ticket: Drucker in Büro 3 druckt nicht." Das Modell sollte nach einer kurzen Statusmeldung "Erstelle GLPI-Ticket…" eine Bestätigung mit Ticket-ID und Link zurückgeben. Erscheint keine Statusmeldung und das Modell antwortet nur mit Text, ist entweder das Tool nicht zugewiesen oder das Modell unterstützt kein Function Calling.

Schritt 5: Eine Filter-Function für PII-Redaktion schreiben

Functions laufen im Admin-Panel und greifen automatisch in den Nachrichtenstrom ein. Der Typ wird durch den Klassennamen bestimmt: Filter für Nachrichten-Middleware. Wichtig: Functions können keine neuen pip-Pakete installieren – nur Bibliotheken, die bereits im Open-WebUI-Container enthalten sind.

"""
title: PII Redaction Filter
author: s-edv.com
description: Entfernt E-Mail-Adressen aus Nachrichten vor der KI-Verarbeitung
version: 1.0.0
"""

import re
from pydantic import BaseModel

class Filter:
    class Valves(BaseModel):
        priority: int = 10

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

    async def inlet(self, body: dict, __user__: dict = {}) -> dict:
        """Wird vor der Modell-Anfrage ausgefuehrt – bereinigt Nutzereingabe."""
        EMAIL_RE = re.compile(
            r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
        )
        messages = body.get("messages", [])
        for msg in messages:
            if msg.get("role") == "user" and isinstance(msg.get("content"), str):
                msg["content"] = EMAIL_RE.sub("[REDACTED_EMAIL]", msg["content"])
        return body

    async def outlet(self, body: dict, __user__: dict = {}) -> dict:
        """Wird nach der Modell-Antwort ausgefuehrt – unveraendert durchlassen."""
        return body

Filter haben drei Hook-Methoden: inlet() greift vor dem Modell ein, outlet() nach der Antwort, und stream() während des Streamings. Functions werden unter Admin Panel → Functions → "+" (Neu) angelegt und können global oder pro Modell aktiviert werden.

Verifizieren: Schreibe im Chat eine Nachricht mit einer E-Mail-Adresse wie "Kontaktiere bitte max.mustermann@example.com". In den Debug-Logs des Admin-Panels sollte die Adresse durch [REDACTED_EMAIL] ersetzt erscheinen, bevor das Modell sie verarbeitet.

Schritt 6: Pipelines (Legacy) – wann und wie

Pipelines gelten offiziell als Legacy-Technologie. Für neue Projekte solltest du Tools oder Functions bevorzugen. Den einzigen echten Vorteil, den Pipelines noch bieten – beliebige pip-Pakete in einem isolierten Container – kannst du bei Tools ebenfalls nutzen, solange du keinen Multi-Worker-Betrieb fährst.

Falls du dennoch ein bestehendes Pipeline-Setup betreiben oder migrieren willst:

docker run -d \
  -p 9099:9099 \
  --add-host=host.docker.internal:host-gateway \
  -v pipelines:/app/pipelines \
  --name pipelines \
  --restart always \
  ghcr.io/open-webui/pipelines:main

Verbindung in Open WebUI herstellen: Admin Panel → Settings → Connections → OpenAI-API hinzufügen

  1. API URL: http://host.docker.internal:9099
  2. API Key: 0p3n-w3bu!

Eine bestehende Pipeline lässt sich meist als Pipe-Function migrieren: Die Klasse wird von Pipeline zu Pipe umbenannt und direkt in Open WebUI eingespielt. Der größte Vorteil dabei ist der Wegfall des separaten Containers.

Schritt 7: Multi-Worker-Deployments absichern

Wer Open WebUI mit mehreren Gunicorn-Workern betreibt, sollte das automatische pip-Install deaktivieren. Gleichzeitige Installationsversuche können den Dienst destabilisieren:

ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS=False

Pakete dann im eigenen Dockerfile vorinstallieren:

# Eigenes Dockerfile (FROM ghcr.io/open-webui/open-webui:main)
RUN pip install requests>=2.31.0 caldav>=1.3.0

Vergleich: Einbindungsschritte auf einen Blick

SchrittToolFilter-FunctionPipeline (Legacy)
Wo anlegenWorkspace → Tools → NeuAdmin → Functions → NeuEigener Container deployen
Code einfügenPython-Datei im Browser-EditorPython-Datei im Browser-EditorDatei im /pipelines-Ordner
AktivierenModell zuweisen oder im ChatGlobal oder pro Modell/ChatOpenAI-Endpunkt in Connections
KonfigurierenValves im WorkspaceValves im Admin-PanelValves im Pipeline-Container
pip-PaketeJa (requirements-Header)NeinJa (volle Kontrolle)

Troubleshooting / Typische Fehler

Tool wird nie aufgerufen

Ursache 1: Falscher Klassenname. Die Klasse muss exakt Tools heißen – ein Tippfehler führt dazu, dass Open WebUI den Typ nicht erkennt und das Plugin ignoriert. Ursache 2: Fehlende Type Hints. Methoden ohne vollständige Typ-Annotationen erzeugen kein gültiges JSON-Schema, das LLM "sieht" das Tool dann nicht. Ursache 3: Modell unterstützt kein Function Calling. Prüfe mit ollama show <modell>, ob tool_use in den Capabilities aufgeführt ist.

Modell ruft Tool unzuverlässig auf

Schwache Docstrings sind der häufigste Grund. Da das LLM die Docstrings liest, um zu entscheiden, ob und wie ein Tool verwendet wird, führen unklare oder fehlende :param:/:return:-Beschreibungen zu unzuverlässigem Tool-Calling. Auch Modelle unter ~13B Parameter – vor allem viele 7B-Varianten – unterstützen Function Calling nur eingeschränkt. Empfehlung: llama3.1, qwen2.5, mistral-nemo oder command-r verwenden.

Function kann requests nicht importieren

Functions können keine neuen pip-Pakete installieren. Wer requests, caldav oder ähnliches benötigt, muss entweder ein Tool (nicht Function) verwenden oder die Pakete manuell im Open-WebUI-Container vorinstallieren. Das ist kein Bug, sondern eine bewusste Designentscheidung.

Race-Condition bei pip-Install (Multi-Worker)

In Deployments mit mehreren Workern können gleichzeitige Installationsversuche den Dienst destabilisieren. Lösung: ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS=False setzen und Pakete im Dockerfile vorinstallieren.

Status-Events flackern oder verschwinden

Event-Typ message und chat:message:delta funktionieren im aktuellen Native-Function-Calling-Modus nicht stabil – sie werden sofort von Completion-Snapshots überschrieben. Stattdessen immer status-Events verwenden, wie in den Beispielen gezeigt.

API-Keys tauchen in Logs auf

Valve-Felder mit sensiblen Werten sollten als Field(json_schema_extra={'format': 'password'}) deklariert werden, damit sie nicht im Klartext in Logs erscheinen. Hardcodierte Secrets im Quellcode sind grundsätzlich verboten.

Häufige Fragen

Brauche ich ein bestimmtes Ollama-Modell, damit Tools funktionieren?

Ja – das Modell muss Function Calling / Tool Use unterstützen. Für lokale Nutzung empfehlen sich llama3.1 (8B/70B), qwen2.5 (7B/14B/72B), mistral-nemo oder command-r. In Ollama prüfbar per ollama show <modell>: Steht tool_use in den Capabilities, ist das Modell geeignet. Die Modellauswahl hat dabei erheblichen Einfluss auf die Zuverlässigkeit – mehr dazu in der Anleitung Ollama-Modelle richtig auswählen.

Was ist der Unterschied zwischen Tool und Filter?

Tool = das LLM entscheidet selbst, das Tool aufzurufen (z. B. "hole mir den Kalender für morgen"). Filter = läuft immer automatisch für jede Nachricht, ohne dass das LLM es weiß oder entscheiden kann (z. B. jede ausgehende Nachricht auf personenbezogene Daten prüfen). Tools für optionale Aktionen, Filter für obligatorische Transformationen.

Kann ich externe Python-Bibliotheken in Functions nutzen?

In Tools ja – über das requirements-Feld im Datei-Header werden Pakete bei Bedarf per pip installiert. In Functions nein – Functions können nur Bibliotheken verwenden, die bereits im Open-WebUI-Container enthalten sind. Lösung: Entweder als Tool implementieren oder Pakete via Dockerfile manuell vorinstallieren.

Muss ich Open WebUI neu starten, wenn ich ein Tool hinzufüge?

Nein – Tools und Functions werden dynamisch geladen. Speichern im UI genügt. Nur wenn Requirements-Pakete neu installiert werden müssen, kann es beim ersten Aufruf kurz dauern.

Kann ein Tool auch in die Datenbank schreiben?

Technisch ja – ein Tool kann beliebigen Python-Code ausführen, also auch Datenbankverbindungen aufbauen, Dateien anlegen oder REST-APIs mit Schreibzugriff aufrufen. Es gibt keine technische Einschränkung auf Lesezugriff. Das unterstreicht die Notwendigkeit eines sorgfältigen Code-Reviews und des Prinzips minimaler Berechtigungen.

Wie sichere ich API-Keys in Tools und Functions ab?

Über die Valves-Klasse: Pydantic-BaseModel-Felder mit Field(default='') anlegen. Die Werte werden in der Open-WebUI-Datenbank gespeichert und im Admin-Panel konfiguriert – niemals im Quellcode hardcodieren. Für nutzerspezifische Keys gibt es UserValves, die jeder Nutzer selbst pro Chat anpassen kann.

Fazit

Mit Tools und Filter-Functions bekommst du eine leistungsfähige Erweiterungsebene, die eng in Open WebUI integriert ist und ohne separaten Infrastrukturaufwand auskommt. Der empfohlene Einstieg ist ein einfaches Tool – etwa die SearXNG-Suche – um das Zusammenspiel zwischen LLM, Docstrings und Valves zu verstehen. Wer danach mehr will, kann mit Filter-Functions die gesamte Nachrichtenverarbeitung kontrollieren. Pipelines bleiben für bestehende Installationen relevant, sollten aber für neue Projekte nicht mehr gewählt werden. Das wichtigste Sicherheitsprinzip bleibt unverändert: Code-Review vor jeder Installation ist Pflicht – der Code läuft mit vollen Server-Rechten.

Für den nächsten Schritt lohnt sich ein Blick auf die Absicherung der Plattform selbst: Open WebUI absichern: Benutzergruppen, RBAC und Modell-Zugriffssteuerung zeigt, wie du den Zugriff auf Tools und Modelle granular steuerst. Wer Tools mit komplexen Workflows koppeln will, findet in der n8n-Anleitung einen sinnvollen Einstieg in die KI-Automatisierung.

Weiterführende Anleitungen und Quellen

  1. Ollama und Open WebUI mit Docker: eigenes lokales KI-Sprachmodell ohne Cloud betreiben
  2. Open WebUI absichern: Benutzergruppen, RBAC und Modell-Zugriffssteuerung
  3. Ollama-Modelle 2026 richtig auswählen: VRAM, Quantisierung und Modellvergleich
  4. n8n mit Docker: KI-Workflows und Automatisierung self-hosted
  5. SearXNG auf dem Synology NAS: private Metasuchmaschine ohne Tracking
  6. Open WebUI Docs: Tools & Functions Overview
  7. Open WebUI Docs: Tool Development Guide
  8. Open WebUI Docs: Events / EventEmitter Reference