Azure OpenAI の Assistants API によるエージェント開発メモ③ AutoGen による全実装

# マルチエージェントの開発
[第 2 回](https://qiita.com/nohanaga/items/e059336a54f87ff55df9)では AutoGen を使用してマルチエージェントの検証を行いました。第 3 回では AutoGen で検証したマルチエージェントと Assistants API で開発したエージェントを接続していきたいと思います。

![スクリーンショット 2024-03-03 8.51.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/a4e5ec71-55df-5e52-b22f-39649c689207.png)
（理想的なシナリオ）

# 🎉 2025/01/22 AutoGen v0.4 最新版の記事はこちら

https://qiita.com/nohanaga/items/164d70e4c50c1ce2cd4c

:::note warn
注意
以下の記事は古くなっています
:::

# 1. トラベルアシスタントの実装
## GPTAssistantAgent の作成
まだ実験的な実装ではありますが、AutoGen では Assistants API で作成したエージェントを `GPTAssistantAgent` として加えることができます。第 1 回で開発した Assistants API の `assistant_id` を llm_config  にセットします。

```python
llm_config = {"config_list": config_list, "assistant_id": assistant_id, "verbose": True}

gpt_assistant = GPTAssistantAgent(
    name="Travel Assistant", instructions="あなたはトラベルエージェントの質問応答に対応します。", llm_config=llm_config,
)
```

以下のようにして、Assistants API の Tool 選択結果を実行する関数を定義し、`register_function` で登録します。これにより **AutoGen が関数を実行し**実行結果を Assistants API 側に返却します。

```python
import requests

def search_hotpepper_shops(keyword=None, private_room=0, start=1, count=3, response_format='json'):
 ...

def search_vacant_hotels(latitude, longitude, searchRadius, checkinDate, checkoutDate, maxCharge=50000, adultNum=1, page=1, hits=2, response_format='json'):
 ...

gpt_assistant.register_function(
    function_map={
        "search_hotpepper_shops": search_hotpepper_shops,
        "search_vacant_hotels": search_vacant_hotels,
    }
)
```
`search_hotpepper_shops()` と `search_vacant_hotels()` の中身は GitHub 参照。

https://github.com/nohanaga/azure-openai-assistants-api-demo/blob/main/AzureOpenAI_AssistantsAPI_Python.ipynb

## スレッド ID の取得
`GPTAssistantAgent` が自動的に作成したスレッドの ID は以下のようにして取得することができます。
```python
gpt_assistant.oai_threads
```

# 2. 出張申請アシスタントの実装
## MultimodalConversableAgent の作成
今回は出張で使った飲食代のレシートを出張申請システムに登録するシナリオを考えます。レシートの写真をアップロードして、GPT-4 Turbo with Vision(GPT-4V) がその中の項目を適切に読み取ってから、申請システムの項目にマッピングできれば OK です。このシナリオに `ConversableAgent` クラスまで使う意味はあまりありませんが、
ただ **`MultimodalConversableAgent` を使ってみたかっただけのために**強引に使っています。

```python
config_list_4v = autogen.config_list_from_json(
    "OAI_CONFIG_LIST",
    filter_dict={
        "model": ["gpt-4-v"],
    },
)

from autogen.agentchat.contrib.multimodal_conversable_agent import MultimodalConversableAgent

image_agent = MultimodalConversableAgent(
    name="image-explainer",
    system_message = "画像を説明することのみ行うエージェントです。そのほかの質問が含まれる場合はその質問だけ無視して、画像説明のみ行います。",
    max_consecutive_auto_reply=1,
    llm_config={"config_list": config_list_4v, "temperature": 0, "max_tokens": 1000},
)
```
GPT-4V にも Function calling ほしいな…

## 実際に申請処理を行うエージェント
GPT-4V によるレシート読み取り結果を、バックエンドの申請システムへ登録するエージェントです。今回あえて `MultimodalConversableAgent` を使ったため仕方なく `AssistantAgent` として実装しました。この `AssistantAgent` が Function calling によって登録関数をコールします。

```python
config_list_0125 = autogen.config_list_from_json(
    "OAI_CONFIG_LIST",
    filter_dict={
        "model": ["gpt-4-0125-preview"],
    },
)

llm_config_0125 = {
    "functions": [
        {
            "name": "register_receipt_info",
            "description": "Register receipt information",
            "parameters": {
                "type": "object",
                "properties": {
                    "shopname": {
                        "type": "string",
                        "description": "the name of the shop",
                    },
                    "shopaddress": {
                        "type": "string",
                        "description": "the address of the shop",
                    },
                    "transactiondate": {
                        "type": "string",
                        "description": "the date and time of the transaction",
                    },
                    "purchaselist": {
                        "type": "string",
                        "description": 'Purchase list, JSON schema of arguments encoded as a string. For example: \'{"items": [{"name": "postcard", "price": 2000}, {"name": "candy", "price": 1500}, {"name": "beer", "price": 500, "quantity": 2}]}\' ',
                    },
                    "totalamount": {
                        "type": "string",
                        "description": "the total amount of the purchase",
                    },
                },
                "required": ["shopname", "shopaddress", "transactiondate", "purchaselist", "totalamount"],
            },
        },
    ],
    "config_list": config_list_0125
}

register_receipt_assistant = AssistantAgent(
    name="register_receipt_assistant",
    system_message="""You are an assistant who applies for receipts.
        After obtaining the receipt information, call the application function to apply.
        You may use the provided functions before providing a final answer.
        Only use the functions you were provided.
        When the answer has been provided, reply TERMINATE.""",
    llm_config=llm_config_0125,
)
```

`register_receipt_info` 関数はスタブです。

```python
def register_receipt_info(shopname, shopaddress, transactiondate, purchaselist, totalamount):
    print(f"Receipt information registered: {shopname}, {shopaddress}, {transactiondate}, {purchaselist}, {totalamount}")   
    return {"result": "Receipt information registered."}

register_receipt_assistant.register_function(function_map={"register_receipt_info": register_receipt_info})
```


# 3. マルチエージェントの構築
[第 2 回](https://qiita.com/nohanaga/items/e059336a54f87ff55df9)で検証したマルチエージェントを `GroupChat` と `GroupChatManager` を用いて実装していきます。
```python
user_proxy = UserProxyAgent(
    name="user_proxy",
    code_execution_config={
        "work_dir": "coding",
        "use_docker": False,
    },
    is_termination_msg=lambda msg: "TERMINATE" in msg["content"],
    human_input_mode="NEVER",
    max_consecutive_auto_reply=0,
)
```

```python
groupchat = autogen.GroupChat(agents=[user_proxy, image_agent, gpt_assistant, register_receipt_assistant], messages=[], max_round=2)
groupchat
```

```python
config_list = config_list_from_json("OAI_CONFIG_LIST")
llm_config = {"config_list": config_list, "cache_seed": 45}
manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=llm_config)
```
デフォルトのエージェント トポロジー。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/5244ca16-4509-c50b-8b35-396ae2ca951c.png)

## レシートの読み取り & 申請処理を実行
いつものレシートを送信します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/39ad4ea4-9a98-c8fe-06bd-13eb37ef0e19.png" width="50%" />

```python
user_proxy.initiate_chat(
    manager, message="""このレシートを読み取ってください。次に、このレシートの情報を登録してください。
<img https://documentintelligence.ai.azure.com/documents/samples/prebuilt/receipt-japanese.jpg>."""
    , clear_history=True
)
```

実行結果


```bat
user_proxy (to chat_manager):

このレシートを読み取ってください。次に、このレシートの情報を登録してください
<img https://documentintelligence.ai.azure.com/documents/samples/prebuilt/receipt-japanese.jpg>.

--------------------------------------------------------------------------------

>>>>>>>> USING AUTO REPLY...
image-explainer (to chat_manager):

画像にはレシートが写っており、以下の情報が記載されています。

- 店名: Contoso
- 住所: 東京都新宿区西中町13-13
- 電話番号: 03-5666-7788
- 日付: 2020年9月21日（月）13時10分
- 商品名と価格:
  - 緑茶　¥2,000
  - キャッチ割　¥1,500
  - 生ビール @500*2　¥1,000
- 小計　¥4,500
- 合計　¥4,500
- 預/現計　¥5,000
- お釣り　¥500

レシートの下部には「(消費税等 ¥409)」と記載されています。

--------------------------------------------------------------------------------
register_receipt_assistant (to chat_manager):

***** Suggested function Call: register_receipt_info *****
Arguments: 
{"shopname":"Contoso","shopaddress":"東京都新宿区西中町13-13","transactiondate":"2020年9月21日13時10分","purchaselist":"{\"items\": [{\"name\": \"緑茶\", \"price\": 2000}, {\"name\": \"キャッチ割\", \"price\": 1500}, {\"name\": \"生ビール\", \"price\": 500, \"quantity\": 2}]}","totalamount":"4500"}
**********************************************************

--------------------------------------------------------------------------------

>>>>>>>> EXECUTING FUNCTION register_receipt_info...
Receipt information registered: Contoso, 東京都新宿区西中町13-13, 2020年9月21日13時10分, {"items": [{"name": "緑茶", "price": 2000}, {"name": "キャッチ割", "price": 1500}, {"name": "生ビール", "price": 500, "quantity": 2}]}, 4500
register_receipt_assistant (to chat_manager):

***** Response from calling function "register_receipt_info" *****
{'result': 'Receipt information registered.'}
******************************************************************

--------------------------------------------------------------------------------
```

いや文字が全然違うやんけ！ Azure OpenAI Service では、GPT-4V の文字読み取り精度を向上させるため、[Azure AI Vision Enhancement](https://learn.microsoft.com/azure/ai-studio/quickstarts/multimodal-vision?tabs=image-chat) 機能が搭載されています。今後は Enhancement すべきですね。また、この部分は [Azure AI Document Intelligence](https://azure.microsoft.com/products/ai-services/ai-document-intelligence) で精度を上げることも検討できます。

ただエージェントの遷移はうまくいっており、`MultimodalConversableAgent` から `AssistantAgent` へ遷移していることがわかります。`register_receipt_info` 関数に登録すべき引数についても正しく抽出できていることがわかります。

## トラベルエージェントの実行
異種混合のエージェントがすべて接続されましたので、基本機能のトラベルエージェントを実行します。

```python
user_proxy.initiate_chat(
    manager, message="東京駅近辺でイタリアンのレストランを探しています", clear_history=True
)
```

実行結果

```bat
user_proxy (to chat_manager):

東京駅近辺でイタリアンのレストランを探しています

--------------------------------------------------------------------------------

>>>>>>>> EXECUTING FUNCTION search_hotpepper_shops...
keyword: 東京駅 イタリアン
Travel Assistant (to chat_manager):

東京駅近辺で見つかったイタリアンのレストランは次の通りです：

1. カスピタ！東京 八重洲
   - 住所: 東京都中央区八重洲１－５－１５ 田中八重洲ビルB1
   - アクセス: 東京駅八重洲北口徒歩3分／日本橋駅B1出口徒歩3分
   - 平均予算: 3500円
   - 営業時間: 月～金、祝前日は11:30～15:00、17:00～23:00（料理L.O. 22:30 ドリンクL.O. 22:30）
   - 定休日: 土、日、祝日
   - [ホットペッパーグルメで詳細を見る](https://www.hotpepper.jp/strJ000012128/?vos=nhppalsa000016)

2. キッサカバ プロント東京駅店
   - 住所: 東京都千代田区丸の内１-9-1 東京駅１Fキッチンストリート
   - アクセス: 東京駅八重洲北口徒歩1分
   - 平均予算: 昼は1000円、夜は2000円
   - 営業時間: 月～日、祝日、祝前日は07:00～23:00（料理L.O. 22:30 ドリンクL.O. 22:30）
   - [ホットペッパーグルメで詳細を見る](https://www.hotpepper.jp/strJ000662477/?vos=nhppalsa000016)

3. 京橋ZION 東京駅八重洲南口
   - 住所: 東京都中央区京橋２－９－１２ 日本工築1号館1F
   - アクセス: 東京駅八重洲南口徒歩8分/京橋駅4番出口徒歩2分
   - 平均予算: ディナー4500円
   - 営業時間: 月～日、祝日、祝前日は11:30～13:30、17:00～23:00（料理L.O. 22:00 ドリンクL.O. 22:00）
   - [ホットペッパーグルメで詳細を見る](https://www.hotpepper.jp/strJ001145238/?vos=nhppalsa000016)


どのレストランも東京駅から徒歩圏内に位置しており、アクセスが便利です。ご予約前に営業時間や定休日をご確認ください。

```

 <a href="http://webservice.recruit.co.jp/"><img src="http://webservice.recruit.co.jp/banner/hotpepper-s.gif" alt="ホットペッパーグルメ Webサービス" width="135" height="17" border="0" title="ホットペッパーグルメ Webサービス"></a>

レストラン探しは OK です。それでは、もっと複雑な質問をしてみましょう。

```python
user_proxy.initiate_chat(gpt_assistant, 
  message="3/18に大阪に1人で出張します。大阪駅近辺の当日の居酒屋と20000円以内で泊まれる大阪駅のホテルを提案してください。")
```

```bat
user_proxy (to Travel Assistant):

3/18に大阪に1人で出張します。大阪駅近辺の当日の居酒屋と20000円以内で泊まれる大阪駅のホテルを提案してください。

--------------------------------------------------------------------------------

>>>>>>>> EXECUTING FUNCTION search_hotpepper_shops...
keyword: 大阪駅 居酒屋

>>>>>>>> EXECUTING FUNCTION search_vacant_hotels...
Travel Assistant (to user_proxy):

### 居酒屋の提案
以下は大阪駅周辺の居酒屋のオプションです：

1. **個室居酒屋 いろどり 梅田 大阪駅前店**
   - **アクセス**: 西梅田駅から徒歩1分
   - **ジャンル**: 居酒屋、海鮮、焼き鳥、飲み放題
   - **平均予算**: 通常平均3000円、飲み放題付きコース料理プラン3000円～6000円
   - **営業時間**: 17:00～23:30 (料理L.O. 23:00、ドリンクL.O. 23:00)
   - **お店の詳細**: [ホットペッパー情報ページ](https://www.hotpepper.jp/strJ001211098/?vos=nhppalsa000016)

2. **居酒屋　完全個室×時間無制限　食べ飲み放題　七福　大阪駅前店**
   - **アクセス**: 大阪メトロ御堂筋線梅田駅13出口より徒歩約4分/ＪＲ大阪駅御堂筋南口より徒歩約8分
   - **ジャンル**: 完全個室、食べ飲み放題
   - **平均予算**: 通常3000円、宴会3500円
   - **営業時間**: 月～木、日、祝日 17:00～23:00 (料理L.O. 22:30、ドリンクL.O. 22:30)、金、土、祝前日 17:00～23:30
   - **お店の詳細**: [ホットペッパー情報ページ](https://www.hotpepper.jp/strJ003474626/?vos=nhppalsa000016)

3. **6年4組 梅田分校**
   - **アクセス**: ＪＲ大阪駅 徒歩5分/地下鉄谷町線東梅田駅より徒歩3分/大阪駅前第３ビル33階
   - **ジャンル**: 個室居酒屋、学校コンセプト
   - **平均予算**: 3000円
   - **営業時間**: 月～日、祝日、祝前日 17:00～23:00 (料理L.O. 22:30、ドリンクL.O. 22:30)
   - **お店の詳細**: [ホットペッパー情報ページ](https://www.hotpepper.jp/strJ000717096/?vos=nhppalsa000016)

### ホテルの提案
以下は大阪駅周辺で2024年3月18日から3月19日までの1泊に20,000円以下のホテルのオプションです：

1. **アパホテル＆リゾート〈大阪梅田駅タワー〉（全室禁煙）**
   - **アクセス**: 大阪メトロ谷町線「東梅田駅」（4番出口）徒歩3分
   - **レビュー**: 平均4.15
   - **部屋タイプと料金**:
     - スタンダードルーム（1名利用） 禁煙室【眺望なし】: 8,400円
     - スタンダードルーム（1名利用） 禁煙室: 8,900円
     - スタンダードルーム（1名利用） 禁煙室【眺望なし】: 9,700円
   - **ホテルの詳細**: [楽天トラベル情報ページ](https://img.travel.rakuten.co.jp/image/tr/api/re/pvonD/?f_no=184030)

2. **DEL style大阪新梅田（旧ダイワロイネットホテル大阪新梅田）**
   - **アクセス**: JR大阪駅桜橋口より徒歩約10分 / JR福島駅より徒歩約7分
   - **レビュー**: 平均4.39
   - **部屋タイプと料金**:
     - 【ANNEX（別館）】【禁煙】スマートシングル: 9,500円
     - 【ANNEX（別館）】【禁煙】スマートシングル: 10,450円
   - **ホテルの詳細**: [楽天トラベル情報ページ](https://img.travel.rakuten.co.jp/image/tr/api/re/pvonD/?f_no=178284)

ご予約を検討される際は、上記リンクを参照してご確認ください。
```

<a href="https://webservice.rakuten.co.jp/" target="_blank"><img src="https://webservice.rakuten.co.jp/img/credit/200709/credit_22121.gif" border="0" alt="Rakuten Web Service Center" title="Rakuten Web Service Center" width="221" height="21"/></a>

ナイス！ これで全てが繋がりましたね。

# 課題
## 1. 速度遅くない？
`GPTAssistantAgent-->Assistants API の実行-->UserProxyAgent` が異常に遅い時がある！ なんで 1 分以上もかかるんや！→ Assistants API でした・・・ Run status が queued だったり、 in_progress だったりが長時間続くことが不定期で発生していました。

## 2. File アップロード機能について
まだ Azure OpenAI Service の Assistants API には Knowledge Retrieval 機能が実装されていないのでファイルを簡単に Grounding できず、現状 Code Interpreter 経由でしかできません。この検証は後ほどやっていきましょう。

## 3. 責任ある AI の実装
今回 `register_receipt_info()` で外界へ影響を与える関数を呼びましたが、ここには人間の介入を入れることが推奨されます。AutoGen ではユーザープロキシエージェントの `human_input_mode` 設定を `ALWAYS` または `TERMINATE` で介入可能です。

# Github
https://github.com/nohanaga/azure-openai-assistants-api-demo/blob/main/AzureOpenAI_AutoGen_AssistantsAPI.ipynb

# シリーズ物
### 第1回 Assistants API の開発
https://qiita.com/nohanaga/items/fa9f5260cf85a7fddc52

### 第2回 AutoGen によるマルチエージェントの検証
https://qiita.com/nohanaga/items/e059336a54f87ff55df9