Open WebUIにSwarmを組み込む

# はじめに

TRIAL＆RetailAI Advent Calendar 2024の16日目になります．昨日は[@takurUN](https://qiita.com/takurUN)さんの[『errgroupの上位互換？ sourcegraph/concを触ってみた』](https://qiita.com/takurUN/items/d472c066945e252b86f5)でした．かゆいところに手が届くような新しいパッケージによって，便利でより品質の高い開発が行なえるので，新しいパッケージには常にアンテナを張っておくことが大切だと改めて感じました．
本日のテーマは『Open WebUIにSwarmを組み込む』です．今年話題になった生成AI関連のツールの紹介で，Open WebUIとSwarmを組み合わせて触ってみた内容になります．

https://qiita.com/advent-calendar/2024/retail-ai

# ツール

## [Open WebUI]((https://docs.openwebui.com/))

Open WebUIはオープンソースで，ローカル含めさまざまな場所にホスティングすることができます．ですので，ChatGPTのような画面でローカルLLMを動かすことも可能になります．機能も豊富なので，UIを作るのに手間なく生成AIを用いたサービスをリリースできます．[github](https://github.com/open-webui/open-webui)のスター数も2024/12/15時点で51.3kとなっており，人気があるようです(ちなみに2024/12/02時点では49.6kで，2週間で1,500も増えていました)．
また，機能を拡張するために[Pipelines](https://github.com/open-webui/pipelines)というワークフローが提供されています．これを用いることで，簡単に機能を拡張し，独自のロジックを統合し，わずか数行のコードでダイナミックなワークフローを作成できます．

## [Swarm](https://github.com/openai/swarm)

Open AIがリリースした，マルチエージェントオーケストレーションフレームワークです．商用利用はできませんが，複数のAIエージェントを効率よく連携でき，複雑なタスクを実行できるシステムを容易に構築できます．


# お題

今回は上記2つのツールを用いてエージェントシステムを構築します．ざっと探したのですが，この2つを組み合わせた記事を見つけられなかったので自分で作ってみます．題材としてはせっかくなので，弊社のアドベントカレンダーの内容を取得し，英訳することにします．

# 構成

構成のイメージは下図のようなものです．今回はローカルで環境を構築し，Open WebUIのPipelineにSwarmを組み込みます．エージェントは3つ作成します．
- **トリアージエージェント**
    ユーザーの問い合わせを受付，適宜別エージェントに転送
- **一覧表示エージェント**
    TRIAL＆RetailAI Advent Calendarの一覧を表示(ここは今回インチキした)
- **記事取得エージェント**
    QiitaのAPIを叩いて，指定されたQiitaの記事を取得する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/da7f65b6-b900-9551-5eb4-3782d7c1de2d.png)

## Open WebUIの設定
UIとPipelinesそれぞれのコンテナを，以下の`compose.yaml`で立ち上げます．そして
[README](https://github.com/open-webui/pipelines?tab=readme-ov-file#-quick-start-with-docker)を参考に，UI上から**Admin Settings** -> **Connections**で2つのコンテナをつなぎます．

```yaml:compose.yaml
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: always
    ports:
      - "3000:8080"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - open-webui:/app/backend/data
    depends_on:
      - pipelines
  pipelines:
    image: ghcr.io/open-webui/pipelines:main
    container_name: pipelines
    restart: always
    ports:
      - "9099:9099"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - pipelines:/app/pipelines
    extra_hosts:
      - "host.docker.internal:host-gateway"

volumes:
  pipelines:
  open-webui:
```

## エージェント作成

Swarmを用いてマルチエージェントを作成します．作成したコードをPipelinesに取り込むため，仕様に沿ったコードとなっています．ポイントとしては大きく2点です．

- インストールしてほしいライブラリ(今回はswarm)や環境変数をFront Matterに記述

```python
"""
requirements: git+https://github.com/openai/swarm.git
environments: OPENAI_API_KEY
"""
```
- 以下のようにPipelineクラスを定義する
```python
class Pipeline:
    class Valves(BaseModel):

        ...

    def pipe(
        self, user_message: str, model_id: str, messages: list[dict], body: dict
    ):
        ...
```

<details><summary>swarm_article.py (折りたたんでいます↓)</summary>

```python:swarm_article.py
"""
title: Swarm Pipeline
author: mrsd
date: 2024-12-02
version: 1.0
description: A pipeline for generating text using Swarm.
requirements: git+https://github.com/openai/swarm.git
environments: OPENAI_API_KEY
"""

import os
import logging

from typing import Union, Generator, Iterator

import http.client
import json
from pydantic import BaseModel
from swarm import Agent, Swarm

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

UPDATED_DATE_MIN = "2024-11-20"
QIITA_LIST = [...]  # カレンダーのリスト．長いので記述は割愛

class Pipeline:

    class Valves(BaseModel):
        pass

    def __init__(self) -> None:
        self.name = "Qiita Advent Pipeline"
        self.valves = self.Valves(**{"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", "")})

        self.client = Swarm()
        self.triage_agent = Agent(
            name="トリアージエージェント",
            instructions="""ユーザーの要求を最適に処理できるエージェントを判断し、その会話をそのエージェントに転送します。
            質問の中に，2024年以降の日付がある場合は処理をするようにしてください．
            リクエストをエージェントにトリアージするためにもっと情報が必要な場合は、理由を説明せずに直接質問してください。""",
            model="gpt-4o-mini",
        )
        self.article_search_agent = Agent(
            name="記事取得エージェント",
            instructions="""該当の日付とユーザーが書いた記事を取得し、その内容を提供してください。
            ユーザーがどの記事を取得したいかに関わらず，取得を行なうには日付とユーザーの両方を入力する必要があります。
            この2つの情報がなければ取得できません。
            1つのメッセージでdateまたはuserを尋ねてください。
            """,
            functions=[self.get_advent_article],
            model="gpt-4o-mini",
        )
        self.article_list_agent = Agent(
            name="一覧表示エージェント",
            instructions="""RetaiAIのAdvent Calendar記事の一覧を提供してください．
            記事の内容を取得する場合は、トリアージエージェントに会話を転送してください。
            """,
            functions=[self.return_article_list],
            model="gpt-4o-mini",
        )
        self.triage_agent.functions = [
            self.transfer_to_article_search,
            self.transfer_to_list_search,
        ]
        self.article_search_agent.functions.append(self.transfer_back_to_triage)
        self.article_list_agent.functions.append(self.transfer_back_to_triage)
        self.messages: list[dict] = []

    async def on_startup(self) -> None:
        print(f"on_startup:{__name__}")
        self.messages = []
        self.current_agent = self.triage_agent

    async def on_shutdown(self) -> None:
        print(f"on_shutdown:{__name__}")
        self.messages = []
        self.current_agent = self.triage_agent

    def transfer_to_article_search(self) -> Agent:
        return self.article_search_agent

    def transfer_back_to_triage(self) -> Agent:
        return self.triage_agent

    def transfer_to_list_search(self) -> Agent:
        return self.article_list_agent

    def return_article_list(self) -> list:
        # QiitaはスクレイピングNGなので，あらかじめ作っておいたリストを返す
        return QIITA_LIST

    def get_advent_article(self, date: str, user: str) -> str:
        conn = http.client.HTTPSConnection("qiita.com", 443)
        query = f"/api/v2/items?query=updated:>={UPDATED_DATE_MIN}+updated:<={date}+user:{user}"
        conn.request("GET", query)
        res = conn.getresponse()
        data = res.read().decode("utf-8")
        articles = json.loads(data)
        return str(articles[0]["body"])

    def pipe(
        self, user_message: str, model_id: str, messages: list[dict], body: dict
    ) -> Union[str, Generator, Iterator]:
        self.current_agent = self.triage_agent
        if len(user_message) < 100:
            self.messages.append({"role": "user", "content": user_message})

        response = self.client.run(
            agent=self.current_agent,
            messages=self.messages,
            context_variables={},
            stream=False,
            debug=False,
        )
        self.messages.extend(response.messages)
        self.current_agent = response.agent

        for idx, _message in enumerate(self.messages):
            logging.info(f"self.messages{idx}: {_message}")
        swarm_messages = ""
        for message in response.messages:
            if message["role"] != "assistant":
                continue
            if message["content"]:
                swarm_messages += f"{message['sender']}: {message['content']}\n"
                if body.get("stream", False):
                    yield f"{message['sender']}: {message['content']}\n"
                else:
                    return swarm_messages
        return swarm_messages
```

</details>


上記のファイルを，Open WebUIの**Admin Settings** -> **Pipelines** -> **Upload Pipeline**でアップロードすることで，この機能を有したChatを作ることができます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/d64ef423-646b-6da8-e084-8c12c588fdcb.png)



# 結果

下記3つの問い合わせを順に行なったところ，理想通りの回答をエージェントから得ることができました．各問い合わせに対して，適したエージェントが呼ばれそれぞれがfunction callingした形になります．会話の履歴からも，何を扱えばよいのか理解して対応してくれました．これらの質問をChatGPTにしても，


### *「Retail AIのAdvent Calendarの一覧を表示して」* 

→ 一覧表示エージェントが対応

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/48ba8bb8-4c29-2745-c29f-b47cdfd2829b.png)


### *「2024-12-02の記事を取得して」*

→ 記事取得エージェントが対応

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/ac6ef125-dd70-bce0-9030-30ff4852a449.png)

### *「英語にして」*

→ (他のエージェントに転送する必要がないと判断したため)トリアージエージェントが対応

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/f9095eea-40a9-c15c-99be-28e7459f8291.png)

# 所感

Open WebUIを今回初めて触ったのですが，非常に扱いやすかったです．コードも読みやすかったため，どのように実装すれば動くのか理解しやすかったです．おかげさまで，フロント部分の実装は一切やっておらず，エージェント作成に集中して取り組むことができました．その点でもOpen WebUIはありがたいツールだと感じました．機能も豊富なのでOpen WebUIの旨味を十分に活かしきれていませんが，RAGの構築など含めもう少し使いこなすことができればと思います．
Swarmも初めて触りましたが，簡単に実装することができ，シンプルなタスクに対するエージェント作成は難なくできました．しかし，役割やエラー処理などをしっかり与えておかないとうまく動かないという印象です．上で示した結果はうまくいった例で，別の文言で要求したらうまくいかない時もあり，チューニングが必要なのだと感じました．


# まとめ

今回は，Open WebUIとSwarmを組み合わせたシステムを作りました．参考までに，ChatGPT(無料版)で先程の問い合わせをしてみたところ，下記のように，程よい回答は得られました．このくらいの回答まで得られれば十分だと自分は感じました．今回のお題に対して，上記のようなマルチエージェントシステムはオーバースペックで，わざわざ作る必要がなかったようです．しかし，ローカルLLMを使いたい場合などでは，活路が見えてくると思います．利用シーンや仕様を整理したうえで，今回構築した際のノウハウが役立てばよいと思います．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618319/106170c6-3401-a84f-4474-97caaa55d992.png)

# おわりに

明日は[@urakawa_jinsei](https://qiita.com/urakawa_jinsei)さんの『非公式のHTTPステータスコードまとめ』になります．お楽しみに．
Retail AIとTRIALはエンジニアを募集しています．

https://www.recruit-retail-ai.jp/

https://hrmos.co/pages/trialgp/jobs?category=2020003768196907008

# 参考

- [Open WebUI docs](https://docs.openwebui.com/)
- [Open WebUI github](https://github.com/open-webui/open-webui)
- [Open WebUI Pipelines](https://github.com/open-webui/pipelines)
- [Swarm github](https://github.com/openai/swarm)
- [Swarm Basic フォルダの解説：多機能エージェントシステムの基礎](https://zenn.dev/sunwood_ai_labs/articles/swarm-basic-folder)
- [【MOA】社内向けAI Web UI構築ガイド：open webui + pipeline + langfuse](https://zenn.dev/sunwood_ai_labs/articles/moa-internal-ai-web-ui-guide-open-webui-pipeline)
- [OpenAIの「Swarm」でAIエージェントを開発してみた](https://zenn.dev/dalab/articles/1d4f649c8005a0)
- [Qiita API](https://qiita.com/api/v2/docs)