chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:34 +08:00
commit 4b22cfda96
9037 changed files with 2363717 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
# MLflow Filter Pipeline for Open WebUI
A filter pipeline that integrates [MLflow](https://mlflow.org/) tracing with [Open WebUI](https://github.com/open-webui/open-webui), enabling observability for multi-turn chat sessions.
## What It Does
- **inlet**: Captures the last user message and session context before each request
- **outlet**: Logs a complete trace per turn — user input, assistant response, model name, and token usage — grouped under the same session in the MLflow UI
## Features
- **Multi-turn session grouping** — all turns of a conversation are linked via `mlflow.trace.session`, viewable with "Group by session" in the MLflow UI
- **Per-turn tracing** — each request/response pair is logged as a separate MLflow trace with latency and status
- **Token usage tracking** — input/output token counts are captured when provided by the backend, automatically aggregated at the trace level
- **User attribution** — traces are tagged with the authenticated user's email via `mlflow.trace.user`
## Requirements
- MLflow tracking server running and accessible
- `mlflow>=2.14.0`
## Configuration (Valves)
| Valve | Default | Description |
| ------------------------ | ----------------------- | -------------------------- |
| `mlflow_tracking_uri` | `http://localhost:5000` | MLflow tracking server URI |
| `mlflow_experiment_name` | `open-webui` | Experiment name in MLflow |
| `debug` | `false` | Enable debug logging |
## Setup
### 1. Start the MLflow server
```bash
mlflow server --disable-security-middleware
```
### 2. Start Open WebUI
```bash
open-webui serve
```
### 3. Launch the pipeline service via Docker
Build a custom Docker image with MLflow installed:
```bash
# Create Dockerfile.mlflow
cat > Dockerfile.mlflow <<'EOF'
FROM ghcr.io/open-webui/pipelines:main
RUN pip install --no-cache-dir mlflow
EOF
# Build image
docker build -f Dockerfile.mlflow -t pipelines-mlflow .
# Launch container (replace host.docker.internal:5000 with your MLflow server address)
docker run -p 9099:9099 \
--add-host=host.docker.internal:host-gateway \
-v pipelines:/app/pipelines \
--name pipelines \
--restart always \
-e MLFLOW_TRACKING_URI=http://host.docker.internal:5000/ \
-e DEBUG_MODE=true \
pipelines-mlflow
```
### 4. Connect Open WebUI to the pipeline server
In Open WebUI, go to **Admin Panel → Settings → Connections** and add a new OpenAI API connection pointing to the pipeline server:
- **URL:** `http://localhost:9099/`
- **Password:** `0p3n-w3bu!` (default credential)
![Open WebUI connections settings](images/openwebui_settings.png)
### 5. Upload the pipeline in Open WebUI
Go to **Admin Panel → Settings → Pipelines**. Set the Pipelines listener address to `http://host.docker.internal:9099`, then upload `mlflow_filter_pipeline.py` using the file upload button. Then configure MLflow tracking URI and MLflow experiment name as follows:
![OpenWebUI pipeline configuration](images/pipeline_config.png)
### 6. Chat and observe traces
Start a conversation in Open WebUI:
![OpenWebUI chat session](images/chat_session.png)
Open the MLflow UI and enable **"Group by session"** to view full conversations as grouped traces.
**Single turn traces:**
![MLflow single trace view](images/trace_single_1.png)
![MLflow trace detail](images/trace_single_2.png)
**Full chat session grouped view:**
![MLflow session grouped view](images/trace_session.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

@@ -0,0 +1,161 @@
# ruff: noqa
"""
title: MLflow Filter Pipeline
author: open-webui
date: 2026-04-20
version: 0.0.1
license: MIT
description: A filter pipeline that uses MLflow for tracing multi-turn chat sessions.
requirements: mlflow>=2.14.0
"""
from typing import List, Optional
import os
import re
import uuid
from utils.pipelines.main import get_last_assistant_message, get_last_user_message
from pydantic import BaseModel
import mlflow
from mlflow.entities import SpanType
from mlflow.tracing.constant import SpanAttributeKey, TokenUsageKey
def extract_latest_user_input(text: str) -> str:
"""If text contains a <chat_history> block, return only the last USER: segment inside it."""
match = re.search(r"<chat_history>(.*?)</chat_history>", text, re.DOTALL)
if match:
history = match.group(1)
user_messages = re.findall(r"USER:\s*(.*?)(?=\s*ASSISTANT:|\s*$)", history, re.DOTALL)
if user_messages:
return user_messages[-1].strip()
return text
def get_last_assistant_message_obj(messages: List[dict]) -> dict:
for message in reversed(messages):
if message["role"] == "assistant":
return message
return {}
class Pipeline:
class Valves(BaseModel):
pipelines: List[str] = []
priority: int = 0
mlflow_tracking_uri: str = "http://localhost:5000"
mlflow_experiment_name: str = "open-webui"
debug: bool = False
def __init__(self):
self.type = "filter"
self.name = "MLflow Filter"
self.valves = self.Valves(**{
"pipelines": ["*"],
"mlflow_tracking_uri": os.getenv("MLFLOW_TRACKING_URI", "http://localhost:5000"),
"mlflow_experiment_name": os.getenv("MLFLOW_EXPERIMENT_NAME", "open-webui"),
"debug": os.getenv("DEBUG_MODE", "false").lower() == "true",
})
self.pending_inlets: dict = {}
def log(self, message: str):
if self.valves.debug:
print(f"[DEBUG] {message}", flush=True)
async def on_startup(self):
self.log(f"on_startup triggered for {__name__}")
self._setup_mlflow()
async def on_shutdown(self):
self.log(f"on_shutdown triggered for {__name__}")
async def on_valves_updated(self):
self.log("Valves updated, resetting MLflow config.")
self._setup_mlflow()
def _setup_mlflow(self):
mlflow.set_tracking_uri(self.valves.mlflow_tracking_uri)
mlflow.set_experiment(self.valves.mlflow_experiment_name)
self.log(
f"MLflow configured — uri: {self.valves.mlflow_tracking_uri}, "
f"experiment: {self.valves.mlflow_experiment_name}"
)
async def inlet(self, body: dict, user: Optional[dict] = None) -> dict:
self.log("MLflow Filter INLET called")
metadata = body.get("metadata", {})
chat_id = body.get("chat_id") or metadata.get("chat_id") or str(uuid.uuid4())
if chat_id == "local":
session_id = metadata.get("session_id") or str(uuid.uuid4())
metadata["session_id"] = session_id
chat_id = f"temporary-session-{session_id}"
metadata["chat_id"] = chat_id
body["metadata"] = metadata
self.pending_inlets[chat_id] = {
"chat_id": chat_id,
"input": extract_latest_user_input(get_last_user_message(body["messages"])),
"model": body.get("model"),
"user_email": user.get("email") if user else None,
}
self.log(f"Stored inlet snapshot for chat_id: {chat_id}")
return body
async def outlet(self, body: dict, user: Optional[dict] = None) -> dict:
self.log("MLflow Filter OUTLET called")
chat_id = body.get("chat_id") or body.get("metadata", {}).get("chat_id")
if not chat_id:
self.log("[WARNING] No chat_id in outlet body — skipping trace")
return body
inlet_data = self.pending_inlets.pop(chat_id, None)
if inlet_data is None:
self.log(f"[WARNING] No inlet snapshot found for chat_id: {chat_id} — skipping trace")
return body
user_email = inlet_data["user_email"] or (user.get("email") if user else "unknown")
model = inlet_data["model"] or body.get("model", "unknown")
user_input = inlet_data["input"]
assistant_message = get_last_assistant_message(body["messages"])
assistant_message_obj = get_last_assistant_message_obj(body["messages"])
# Extract token usage if available
token_usage = {}
if assistant_message_obj:
info = assistant_message_obj.get("usage") or {}
input_tokens = info.get("prompt_eval_count") or info.get("prompt_tokens")
output_tokens = info.get("eval_count") or info.get("completion_tokens")
if input_tokens is not None:
token_usage[TokenUsageKey.INPUT_TOKENS] = input_tokens
if output_tokens is not None:
token_usage[TokenUsageKey.OUTPUT_TOKENS] = output_tokens
if input_tokens is not None and output_tokens is not None:
token_usage[TokenUsageKey.TOTAL_TOKENS] = input_tokens + output_tokens
try:
with mlflow.start_span(name="chat_turn", span_type=SpanType.AGENT) as span:
span.set_inputs({"user": user_input})
span.set_outputs({"response": assistant_message})
span.set_attribute(SpanAttributeKey.MODEL, model)
if token_usage:
span.set_attribute(SpanAttributeKey.CHAT_USAGE, token_usage)
# Groups all turns of this chat under one session in the MLflow UI
mlflow.update_current_trace(
session_id=chat_id,
user=user_email,
)
self.log(f"MLflow trace logged for chat_id: {chat_id}")
except Exception as e:
warning = f"[WARNING] Failed to log MLflow trace ({type(e).__name__}) for chat_id: {chat_id}: {e}"
self.log(warning)
return body