Skip to content

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 Chat

    Pozná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: Assistant

    Pozná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.