AI asistent v Open WebUI
Update: Optimálnější řešení s pomocí Claude Code a Agent Skills popisuji v novém článku.
Množství příspěvků na mém blogu utěšeně roste a začíná dávat smysl je seskupovat pomocí tagů. Zároveň si říkám, že vymýšlení tagů je přesně úkol pro nějaký jazykový model: předhodit mu text článku a nechat si tagy vygenerovat, alespoň jako návrh. Z pohledu konzistentnosti by bylo zároveň dobré modelu poskytnout i seznam již existujících tagů, aby se jich držel a negeneroval nové varianty téhož (např. ai-tutor, ai-tutors, ai-tutoring apod.).
Toto se dá jistě vyřešit pomocí jednoduchého skriptu do příkazové řádky, nicméně mě napadlo vytvořit si takového asistenta v Open WebUI, které slouží jako webové rozhraní pro náš univerzitní AI Sandbox. Asistenti (podobní GPTs v ChatGPT; v Open WebUI nesou označení Models) mohou volat nástroje (Tools) implementované jako třídy v programovacím jazyce Python. Níže je příklad nástroje pro získání aktuálního data a času:
import datetime
class Tools:
def __init__(self):
pass
def get_current_time(self) -> str:
"""Get the current time in a more human-readable format."""
now = datetime.now()
current_time = now.strftime("%I:%M:%S %p") # Using 12-hour format with AM/PM
current_date = now.strftime("%A, %B %d, %Y") # Full weekday, month name, day, and year
return f"Current Date and Time = {current_date}, {current_time}"
Asistent si pak může dostupné nástroje volat sám.
Vytvoření asistenta
V uživatelském rozhraní Open WebUI v levém panelu nahoře klikneme na položku Workspace (uživatel, pokud není admin, musí mít příslušné oprávnění). Jako první je zpravidla vybrána karta Models. V ní klikneme v levém horním rohu na nenápadnou ikonku +. Otevře se dialog pro přidání nového asistenta/modelu. V mém případě jsou parametry následující:
- Model Name:
Tag Generator -
Base Model (From):
GPT-5 ChatPoznámka
Nejspíš by stačil i nějaký rychlejší a levnější model jako např.
GPT-5 mini, ten ale, překvapivě, nemůžeme v tuto chvíli přes API volat. Předpokládám, že to je proto, že náš testovací účet není ověřený. -
Description:
Asistent pro generování tagů k článkům na mém osobním blogu (https://peberanek.github.io/) -
Add Tags:
AssistantPoznámka
Při větším množství modelů se hodí takto vytvořit samostatnou kategorii, která je zároveň viditelná v uživatelském rozhraní výběru modelů.
-
System Prompt:
Generate a list of tags for the article that will follow afterwards. I need them for my blog generated by Material for MkDocs. Example in YAML: ``` tags: - ai - ai-agents - steve-jobs ``` Rules: * Try to get a list of all existing tags from my personal blog. You should have a tool available for this. The point is to keep consistency with the existing tags. * Tags must be in english lower case only. * If a person is mentioned, generate a tag for them. Here is the article: -
Tools: Zde je potřeba zaškrtnout příslušný nástroj, který si vytvoříme v následující kapitole.
Poznámka
Pro rychlé otestování lze nechat prázdné s tím, že si assistent bude pravděpodobně stěžovat, že nemá nástroj k dispozici (viz systémový prompt).
Implementace nástroje
V Open WebUI přidáme nástroj opět v položce Workspace, tentokrát v kartě Tools. Ikona pro přidání nástroje je také schovaná v pravém horním rohu. Po kliknutí na + a New Tool se zobrazí dialog s ukázkovým zdrojovým kódem, což je fajn.
K rychlému vytvoření prototypu jsem se rozhodl vyzkoušet programovacího agenta Claude Code. Pomocí nástroje uv (uv init fetch-tags) jsem mu vytvořil samostatný adresář a dodal pár souborů navíc:
fetch-tags/
├── main.py
├── pyproject.toml
├── README.md
├── tags.html <- zdrojový kód stránky s existujícími tagy (https://peberanek.github.io/tags/)
├── tools.py <- soubor s ukázkovým zdrojovým kódem z Open WebUI
└── uv.lock
Úvodní prompt:
I need to implement a tool for an LLM in Open WebUI. It is just a function
in Python. You can find examples in tools.py. I need a tool, that gives
the LLM a list of existing tags on my personal blog. The list of tags
is available as content of HTML page at https://peberanek.github.io/tags/
so tags must be extracted from it. You can find its source as tags.html
(I've downloaded it for you). Any questions?
Agent se na nic neptal a "na první dobrou" doplnil do tools.py funkční kód:
def get_blog_tags(self) -> str:
"""
Get a list of all tags from my personal blog.
"""
url = "https://peberanek.github.io/tags/"
try:
response = requests.get(url)
response.raise_for_status()
html_content = response.text
# Extract tags using regex pattern for <span class="md-tag">tagname</span>
pattern = r'<span class="md-tag">([^<]+)</span>'
tags = re.findall(pattern, html_content)
if not tags:
return "No tags found on the blog"
# Remove duplicates and sort
unique_tags = sorted(list(set(tags)))
return f"Available blog tags: {', '.join(unique_tags)}"
except requests.RequestException as e:
return f"Error fetching blog tags: {str(e)}"
Jako testovací prototyp je to dobré. Nicméně se hned ukázalo jedno z rizik spoléhání se na generovaný kód. Na nezkušeného zde totiž číhá přinejmenším jedna past, a tou je volání requests.get(url) bez timeoutu. V případě, že je stránka nedostupná, nástroj může zůstat "viset" po neomezeně dlouhou dobu. Když jsem Clauda vyzval ke kontrole, chybu opravil (s tím, že opravil a další, která mě nenapadla):
Can you spot any security issues with the code you just generated?
● Yes, there are a few potential security considerations:
1. No request timeout - The requests.get() call could hang indefinitely if the server doesn't
respond
2. No size limits - Could potentially download very large responses that consume excessive
memory
3. No URL validation - While the URL is hardcoded here, it's good practice to validate URLs
Here's a more secure version:
● Update(tools.py)
⎿ Updated tools.py with 7 additions and 1 removal
114 url = "https://peberanek.github.io/tags/"
115
116 try:
117 - response = requests.get(url)
117 + # Add timeout and size limits for security
118 + response = requests.get(url, timeout=10)
119 response.raise_for_status()
120 +
121 + # Limit response size to prevent memory exhaustion
122 + if len(response.content) > 5 * 1024 * 1024: # 5MB limit
123 + return "Error: Response too large"
124 +
125 html_content = response.text
126
127 # Extract tags using regex pattern for <span
class="md-tag">tagname</span>
● The updated version adds:
- 10-second timeout to prevent hanging requests
- 5MB response size limit to prevent memory exhaustion attacks
- Better error handling for oversized responses
Opravený kód viz níže. (O řadě implementačních detailů by se jistě dalo polemizovat, nicméně z tuto chvíli je to za mě použitelné řešení).
def get_blog_tags(self) -> str:
"""
Get a list of all tags from my personal blog.
"""
url = "https://peberanek.github.io/tags/"
try:
# Add timeout and size limits for security
response = requests.get(url, timeout=10)
response.raise_for_status()
# Limit response size to prevent memory exhaustion
if len(response.content) > 5 * 1024 * 1024: # 5MB limit
return "Error: Response too large"
html_content = response.text
# Extract tags using regex pattern for <span class="md-tag">tagname</span>
pattern = r'<span class="md-tag">([^<]+)</span>'
tags = re.findall(pattern, html_content)
if not tags:
return "No tags found on the blog"
# Remove duplicates and sort
unique_tags = sorted(list(set(tags)))
return f"Available blog tags: {', '.join(unique_tags)}"
except requests.RequestException as e:
return f"Error fetching blog tags: {str(e)}"
(Jak už jsem psal dříve v souvislosti s měnící se rolí vývojáře, na vytváření prototypů jsou podobní agenti zajímavou pomůckou, ale k plně autonomnímu psaní produkčního kódu mají zatím daleko.)
Jakmile máme v kartě Tools nástroj vytvořený, s příslušným zdrojovým kódem, je potřeba ho zaškrtnou v dialogu asistenta/modelu a tím mu ho automaticky zpřístupnit. Je také je nutné otevřít nový chat. Ve starých zůstane, zdá se, nástroj nepřístupný.
Vygenerovaný seznam tagů není zpravidla dokonalý a stále slouží spíš jako návrh než "copy-paste" řešení. I tak je ale vytvořený asistent užitečnou praktickou ukázkou.