WORKSPACE TOOLS
Custom models give you control over how a model behaves. Tools give it new abilities. A base model can only work with what's in its training data. Add a tool and suddenly it can search the web, run calculations, fetch live data, or interact with external services.
What Are Tools?
Tools are Python scripts that extend what a model can do. When you attach a tool to a model (or enable it globally), the model gains access to functions it can call during a conversation.
Think of it this way. A custom model with a great system prompt is like a smart employee who knows their job description. A tool is like giving that employee a phone, a calculator, or access to the internet. Same person, new capabilities.
OpenWebUI has a built-in tool system. You can install community tools from the OpenWebUI community site, or write your own in Python. Tonight we're going to add one that makes a huge difference: web search powered by Perplexity.
Why Web Search Matters
Local models have a knowledge cutoff. They know what was in their training data and nothing after that. Ask about something that happened last week and the model will either make something up or tell you it doesn't know.
Web search fixes this. When you attach a search tool, the model can look things up in real time. It sends a search query, gets current results back, and uses those results to answer your question with real, up-to-date information.
The difference is dramatic. Without search, ask "What's the weather in Toronto?" and the model guesses or refuses. With search, it actually looks it up and gives you a real answer.
Adding the Perplexity Search Tool
We're going to use a community tool that connects to Perplexity AI's search API. Perplexity is a search engine built for AI. It returns clean, sourced answers rather than a list of links.
You'll need a Perplexity API key for this. You can get one at perplexity.ai. They offer a free tier that's enough for testing.
Step 1: Open the Tools Section
- Click Workspace in the sidebar
- Click the Tools tab
- Click the "+" button or "Create a Tool"
Step 2: Paste the Tool Code
You'll see a code editor. Paste the following Python code. This is the complete Perplexity web search tool:
"""
title: Perplexity Web Search
author: OpenWebUI Community
author_url: https://github.com/open-webui
description: Production-ready Perplexity API integration
required_open_webui_version: 0.6.3
requirements: httpx, pydantic
version: 1.0.3
license: MIT
"""
import httpx
import asyncio
import re
import logging
from datetime import datetime
from pydantic import BaseModel, Field
from typing import Callable, Any, Optional
logger = logging.getLogger(__name__)
class EventEmitter:
def __init__(self, emitter: Optional[Callable[[dict], Any]] = None):
self.emitter = emitter
async def emit(self, event_type: str, data: dict) -> None:
if self.emitter:
await self.emitter({"type": event_type, "data": data})
async def status(self, description: str, done: bool = False) -> None:
await self.emit("status", {"description": description, "done": done})
async def citation(
self, title: str, url: str, content: str,
date: Optional[str] = None
) -> None:
await self.emit(
"citation",
{
"document": [content[:1000] if content else ""],
"metadata": [
{
"date_accessed": datetime.now().isoformat(),
"source": title,
"url": url,
"date": date or "",
}
],
"source": {"name": title, "url": url},
},
)
class Tools:
class Valves(BaseModel):
PERPLEXITY_API_KEY: str = Field(
default="", description="Your Perplexity API key"
)
PERPLEXITY_MODEL: str = Field(
default="sonar-pro", description="Perplexity model to use"
)
SEARCH_CONTEXT_SIZE: str = Field(
default="medium",
description="Search depth: low, medium, high"
)
TIMEOUT_SECONDS: int = Field(
default=120, description="Request timeout in seconds"
)
MAX_TOKENS: int = Field(
default=4096, description="Maximum tokens in response"
)
TEMPERATURE: float = Field(
default=0.2,
description="Response temperature (0.0-2.0)"
)
class UserValves(BaseModel):
include_citations: bool = Field(
default=True, description="Include source citations"
)
search_recency: str = Field(
default="",
description="Filter: hour, day, week, month, or empty"
)
def __init__(self):
self.valves = self.Valves()
self.citation = False
self.tools = [
{
"type": "function",
"function": {
"name": "search_web",
"description": (
"Search the web using Perplexity AI to find "
"current information, recent news, real-time "
"data, or any information that requires "
"up-to-date web search."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": (
"The search query to look up "
"on the web"
),
}
},
"required": ["query"],
},
},
}
]
def _validate_api_key(self, api_key: str) -> tuple:
if not api_key:
return (
False,
"Perplexity API key not configured. "
"Set it in Tool Settings."
)
api_key = api_key.strip()
if len(api_key) < 20:
return False, "API key appears invalid (too short)"
if " " in api_key:
return False, "API key contains invalid characters"
return True, ""
def _validate_query(self, query: str) -> tuple:
if not query:
return False, "Search query cannot be empty", ""
sanitized = query.strip()
sanitized = re.sub(
r"[\x00-\x1f\x7f-\x9f]", "", sanitized
)
if not sanitized:
return (
False,
"Search query cannot be empty after sanitization",
""
)
if len(sanitized) > 4000:
return (
False,
"Search query too long (max 4000 characters)",
""
)
return True, "", sanitized
async def _make_request_with_retry(
self, client, payload: dict, max_retries: int = 3
):
last_exception = None
for attempt in range(max_retries):
try:
response = await client.post(
"https://api.perplexity.ai/chat/completions",
json=payload
)
if (
400 <= response.status_code < 500
and response.status_code != 429
):
response.raise_for_status()
if (
response.status_code == 429
or response.status_code >= 500
):
if attempt < max_retries - 1:
retry_after = response.headers.get(
"retry-after", "2"
)
try:
delay = min(float(retry_after), 30.0)
except ValueError:
delay = (2**attempt) + 0.5
await asyncio.sleep(delay)
continue
response.raise_for_status()
return response
except httpx.TimeoutException as e:
last_exception = e
if attempt < max_retries - 1:
await asyncio.sleep(2**attempt)
continue
raise
except httpx.NetworkError as e:
last_exception = e
if attempt < max_retries - 1:
await asyncio.sleep(2**attempt)
continue
raise
raise last_exception or Exception("Max retries exceeded")
async def search_web(
self,
query: str,
__event_emitter__: Callable[[dict], Any] = None,
__user__: dict = None,
) -> str:
emitter = EventEmitter(__event_emitter__)
is_valid, error_msg = self._validate_api_key(
self.valves.PERPLEXITY_API_KEY
)
if not is_valid:
await emitter.status(
"Configuration error: " + error_msg, done=True
)
return "Error: " + error_msg
is_valid, error_msg, sanitized_query = (
self._validate_query(query)
)
if not is_valid:
await emitter.status(
"Invalid query: " + error_msg, done=True
)
return "Error: " + error_msg
include_citations = True
search_recency = ""
if __user__ and "valves" in __user__:
user_valves = __user__["valves"]
include_citations = getattr(
user_valves, "include_citations", True
)
search_recency = getattr(
user_valves, "search_recency", ""
)
await emitter.status(
"Searching: " + sanitized_query[:50] + "..."
)
payload = {
"model": self.valves.PERPLEXITY_MODEL,
"messages": [
{
"role": "system",
"content": (
"You are a helpful search assistant. "
"Provide accurate, well-sourced answers "
"based on current web information."
),
},
{"role": "user", "content": sanitized_query},
],
"max_tokens": self.valves.MAX_TOKENS,
"temperature": self.valves.TEMPERATURE,
"web_search_options": {
"search_context_size": (
self.valves.SEARCH_CONTEXT_SIZE
)
},
}
if search_recency:
payload["search_recency_filter"] = search_recency
timeout_seconds = self.valves.TIMEOUT_SECONDS
if (
"reasoning" in self.valves.PERPLEXITY_MODEL
or "deep-research" in self.valves.PERPLEXITY_MODEL
):
timeout_seconds = max(timeout_seconds, 180)
try:
async with httpx.AsyncClient(
timeout=httpx.Timeout(
connect=10.0,
read=float(timeout_seconds),
write=30.0,
pool=10.0
),
headers={
"Authorization": (
"Bearer "
+ self.valves.PERPLEXITY_API_KEY
),
"Content-Type": "application/json",
"Accept": "application/json",
},
) as client:
await emitter.status(
"Querying Perplexity AI..."
)
response = (
await self._make_request_with_retry(
client, payload
)
)
response.raise_for_status()
result = response.json()
except httpx.TimeoutException:
error_msg = (
"Search timed out. Try a simpler query "
"or increase timeout."
)
await emitter.status(error_msg, done=True)
return "Error: " + error_msg
except httpx.HTTPStatusError as e:
status_code = e.response.status_code
if status_code == 401:
error_msg = (
"Invalid API key. Please check your "
"Perplexity API key."
)
elif status_code == 429:
error_msg = (
"Rate limit exceeded. Please wait "
"and try again."
)
elif status_code == 400:
error_msg = (
"Invalid request. The query may be "
"malformed."
)
elif status_code >= 500:
error_msg = (
"Perplexity service error. "
"Please try again later."
)
else:
error_msg = (
"Request failed (HTTP "
+ str(status_code) + ")"
)
await emitter.status(error_msg, done=True)
return "Error: " + error_msg
except httpx.NetworkError:
error_msg = (
"Network connection failed. "
"Please check your connection."
)
await emitter.status(error_msg, done=True)
return "Error: " + error_msg
except Exception:
error_msg = (
"An unexpected error occurred during search."
)
await emitter.status(error_msg, done=True)
return "Error: " + error_msg
content = ""
if "choices" in result and len(result["choices"]) > 0:
content = (
result["choices"][0]
.get("message", {})
.get("content", "")
)
if not content:
await emitter.status("No results found", done=True)
return "No results found for your query."
citations_emitted = 0
if include_citations:
await emitter.status("Processing citations...")
search_results = result.get("search_results", [])
citation_urls = result.get("citations", [])
if search_results:
for sr in search_results[:10]:
title = sr.get("title", "Source")
url = sr.get("url", "")
snippet = sr.get("snippet", "")
date = sr.get(
"date", sr.get("last_updated", "")
)
if url:
await emitter.citation(
title=title, url=url,
content=snippet, date=date
)
citations_emitted += 1
elif citation_urls:
for i, url in enumerate(citation_urls[:10]):
if url:
await emitter.citation(
title="Source " + str(i + 1),
url=url, content=""
)
citations_emitted += 1
if citations_emitted > 0:
status_msg = (
"Found " + str(citations_emitted) + " sources"
)
else:
status_msg = "Search complete"
await emitter.status(status_msg, done=True)
return content
Step 3: Configure the API Key
After saving the tool, click the gear icon next to it (or click into the tool settings). You'll see a Valves section with configurable fields:
PERPLEXITY_API_KEY: Your API key from perplexity.ai
PERPLEXITY_MODEL: sonar-pro (default, good for most searches)
SEARCH_CONTEXT_SIZE: medium (how deep to search)
TIMEOUT_SECONDS: 120 (increase for complex searches)
The only required field is the API key. Enter it and save.
Step 4: Assign the Tool to a Model
You can enable the tool two ways:
Globally: Admin Panel > Settings > Tools. Toggle it on for all
models.
Per Model: When creating or editing a custom model in Workspace >
Models, scroll to the Tool Bindings section and select the
Perplexity search tool.
Per-model is usually better. You probably don't need web search on every model. Attach it to the ones where current information matters.
Trying It Out
Once the tool is attached, start a conversation and ask something that requires current information:
What happened in the news today?
What's the current price of Bitcoin?
What are the latest updates to OpenWebUI?
Watch the status indicator below the chat. You'll see "Searching..." appear, then "Querying Perplexity AI...", then "Found X sources." The model's response will include real, current information with source citations.
Compare this to asking the same question without the search tool. The difference is night and day.
How It Works Under the Hood
When the model decides it needs current information, it calls the search_web function from the tool. Here's what happens:
- The model generates a search query based on your question
- The tool sends that query to Perplexity's API
- Perplexity searches the web and returns a summarized answer with source citations
- The tool passes those results back to the model
- The model incorporates the search results into its response
The model decides when to search. If you ask a general knowledge question it can answer from training data, it might skip the search entirely. If you ask about something current, it triggers the tool automatically.
Other Tools Worth Knowing About
The OpenWebUI community has built dozens of tools. A few popular ones:
Code Interpreter: Runs Python code directly in the conversation
URL Fetcher: Reads the contents of a web page
Calculator: Does precise math (models are bad at math)
YouTube Transcript: Pulls transcripts from YouTube videos
You can browse community tools at openwebui.com or search the GitHub repository. Installing them is the same process: create a new tool, paste the code, configure the settings.
Next Up
Let's try something visual. We'll use a vision model to analyze images directly in the chat.