Azure OpenAI の Assistants API によるエージェント開発メモ①

[Assistants API](https://learn.microsoft.com/azure/ai-services/openai/concepts/assistants) を使うと、データを分析し、ソリューションを提案し、タスクを自動化できる、高度な Copilot のようなエクスペリエンスを備えたアプリケーションを簡単に作成できます。2024/02/06 に Azure OpenAI Service でもパブリックプレビューが開始されました。

ステートレスな Chat Completions API であるのに比べて、ステートフルな Assistants API は、自動的に管理される永続的なスレッドがサポートされます。つまり、会話状態管理システムを開発し、モデルのコンテキスト ウィンドウの制約を回避する必要がなくなります。さらにアシスタントは、必要に応じて複数のツールに並列アクセスすることもできます。 以下のツールが利用可能です。

- [Code Interpreter](https://learn.microsoft.com/azure/ai-services/openai/how-to/code-interpreter?tabs=python)
- [Function calling](https://learn.microsoft.com/azure/ai-services/openai/how-to/assistant-functions?tabs=python)（関数呼び出し）

### 動画でも解説しています
https://youtu.be/tVnj-Weas5E?feature=shared&t=1299

# 🎉12/14 Azure AI Agent Service のパブリックプレビューが開始
https://qiita.com/nohanaga/items/82335753e75445755d70

# 記事構成
第1回 [Assistants API の開発](https://qiita.com/nohanaga/items/fa9f5260cf85a7fddc52)（本記事）
第2回 [AutoGen によるマルチエージェントの検証](https://qiita.com/nohanaga/items/e059336a54f87ff55df9)
第3回 [マルチエージェントシステムの実装](https://qiita.com/nohanaga/items/517d01f7c0f987d82548)

# あったらいいなシナリオ
[Azure OpenAI Developers セミナー第2回](https://youtu.be/cEynsEWpXdA?si=OPyHTnTj-uItoIYS&t=1824)でこんなのあったらいいなぁ的なイメージを共有しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/3df2ad11-688e-6f90-84e2-70a2e00c80e8.png)

当時は ChatGPT Plugin を用いて外部の様々なアプリケーションと接続するような構成となっていましたが、Assistants API ではプラグインは Tool という概念となり、その中で `"type": "function"` のように指定できるようになりました。また、`"type": "code_interpreter"` のように指定して Code Interpreter を有効化することもできます。

# アシスタントの役割
上の図では一つのエージェントがすべての Tools をオーケストレートしていますが、Assistants API を使うと簡単にマルチエージェントを開発できます。マルチエージェントアシスタントでは、ユーザーからの質問を複数のエージェントに割り振るプロキシエージェントを前段に配置します。それぞれのアシスタントの専門性ごとにシステムプロンプトを用意してキャラクターを分けます。今回はトラベルアシスタントから開発していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/38539883-422a-d005-ad36-72e238104bdc.png)

それぞれのアシスタントに計算機能をつけるために、Code Interpreter は全部で有効化してもいいですね。

# リアリティのある Tools を作ろう
まずは RAG 機能から実装していきます。今回は無料で使える Web API を使ってテストしてみたいと思います。ホテル検索は楽天トラベルさんの API を、レストラン検索はホットペッパーグルメさんの API を使わせていただきたいと思います。交通機関の検索については無料の API がありませんでした…　注意：今後 LLM 経由で API を使った場合の利用規約が追加されるかもしれません。

```json
tools_list = [
    {
        "type": "code_interpreter"
    },
    {
        "type": "function",
        "function": {
            "name": "search_hotpepper_shops",
            "description": "ホットペッパーグルメAPIを利用し、キーワードや個室の有無などのオプションフィルターで飲食店を検索できます。",
            "parameters": {
                "type": "object",
                "properties": {
                    "keyword": {
                        "type": "string",
                        "description": "飲食店を検索するためのキーワード。店名、住所、駅名、お店ジャンルなどを指定できる。ユーザーメッセージから検索キーワードとなる文字を抽出して検索クエリーにしてください。例: ###大阪駅 和食###"
                    },
                    "private_room": {
                        "type": "integer",
                        "description": "個室ありの店舗のみを検索, 0:絞り込まない, 1:絞り込む。オプション",
                        "enum": [0, 1]
                    }
                },
                "required": ["keyword"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_vacant_hotels",
            "description": "楽天トラベルのAPIを使って、場所、チェックイン日、チェックアウト日、予算、大人の人数など、さまざまなフィルターで空室ホテルを検索できます。",
            "parameters": {
                "type": "object",
                "properties": {
                    "latitude": {
                        "type": "number",
                        "description": "ホテル検索場所の緯度(WGS84), ex:35.6065914"
                    },
                    "longitude": {
                        "type": "number",
                        "description": "ホテル検索場所の経度(WGS84), ex:139.7513225"
                    },
                    "searchRadius": {
                        "type": "number",
                        "description": "緯度経度検索時の検索半径(単位km), 0.1 to 3.0"
                    },
                    "checkinDate": {
                        "type": "string",
                        "description": "yyyy-MM-dd 形式のチェックイン日。年の指定がない場合は2024年を指定する。"
                    },
                    "checkoutDate": {
                        "type": "string",
                        "description": "yyyy-MM-dd 形式のチェックアウト日。年の指定がない場合は2024年を指定する。"
                    },
                    "maxCharge": {
                        "type": "integer",
                        "description": "上限金額, int 0 to 999999999"
                    },
                    "adultNum": {
                        "type": "integer",
                        "description": "宿泊者数, int 1 to 99"
                    }
                },
                "required": ["latitude", "longitude", "searchRadius", "checkinDate", "checkoutDate"]
            }
        }
    }
]
```

Function calling の作成に必要なパラメータは `name`、`description`、`parameters` の 3 つです。もし ChatGPT Plugins で OpenAPI Spec. に従って開発しているのであればそのまま持ってくることができます！開発したプラグインが無駄になることはありません。

```json
"function": {
    "name": "search_vacant_hotels",
    "description": "この説明を読んで使うべきツールを選定します",
    "parameters": {
        "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "検索クエリーを指定します。"
                },
            }
    }
}
 ...
```

# 1. アシスタントの作成
Assistants API の開発で便利な Python コードをここにメモっておきます。ベースのコードは [OpenAI Cookbook](https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb) を参考にさせてもらいます。Azure で Assistants API を使うには、`client` を `AzureOpenAI` に切り替えるだけです。とっても便利！

```python
from openai import AzureOpenAI
client = AzureOpenAI(
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    api_version="2024-02-15-preview",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
)

# Create an assistant
assistant = client.beta.assistants.create(
    name="Travel Assistant",
    instructions="""
あなたは Contoso 社の社員の出張を支援するためのアシスタントです。あなたは以下の業務を遂行します。
 - 旅程を作成します
 - ホテルを検索したり予約します
 - 交通機関を検索します
 - 出張で行くべきレストランや居酒屋を提案します
 - 出張にかかる概算費用を計算します

#制約事項
 - ユーザーからのメッセージは日本語で入力されます
 - ユーザーからのメッセージから忠実に情報を抽出し、それに基づいて応答を生成します。
 - ユーザーからのメッセージに勝手に情報を追加したり、不要な改行文字を追加してはいけません

""",
    tools=tools_list,
    model="gpt-4-1106-preview" #You must replace this value with the deployment name for your model.
)
```

`instructions` にシステムプロンプトを、`model` に使用する GPT モデルを指定します。Azure OpenAI ではまだ `gpt-4-0125-preview` モデルが Assistants API で使用できませんが、検証したところ、`gpt-4-1106-preview` よりも `gpt-4-0125-preview` のほうが無駄な文字列の生成が無くなるなどの改善がありました。[v0125 はよ](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#assistants-preview)。並列 Function calling を使用したい場合は v1106 以降が必要です。

:::note info
2024年4月時点で v0125 が eastus で利用可能になりました
:::

## 1.1. アシスタントを一覧表示
既存のアシスタントを利用したい場合、存在するアシスタントを一覧表示できます。

```python
assistants = client.beta.assistants.list()
print(assistants.model_dump_json(indent=2))
```
## 1.2. アシスタントの取得
特定のアシスタントにアクセスするには `assistant_id` を指定することで Assistant オブジェクトを取得できます。

```python
assistant = client.beta.assistants.retrieve(assistant_id="Your Assistant ID") 
print(assistant.model_dump_json(indent=2))
```

# 2. スレッドを作成する
スレッドとメッセージは、アシスタントとユーザーの会話セッションを表します。スレッドに保存できるメッセージの数に制限はありません。メッセージのサイズがモデルのコンテキストウィンドウを超えると、スレッドはコンテキストウィンドウに収まるできるだけ多くのメッセージを含めようとし、最も古いメッセージを削除します。

このようにメッセージの初期リストを持つスレッドを作成することができます。

```python
# Create a thread
thread = client.beta.threads.create()
print(thread.model_dump_json(indent=2))
```

## 2.1. スレッドの取得
すでにスレッドが存在していて、そのスレッドにアクセスしたい場合、`thread_id` を指定することでスレッドオブジェクトを取得できます。

```python
thread = client.beta.threads.retrieve(thread_id="Your Thread ID")
print(thread.model_dump_json(indent=2))
```

## 2.2. スレッドメッセージの一覧表示

```python
messages = client.beta.threads.messages.list(
  thread_id=thread.id
)
print(messages.model_dump_json(indent=2))
```
この状態ではまだメッセージは存在していません。

# 3. Run を作成する

**スレッドはアシスタントから独立して存在**します。特定のアシスタント ID からスレッドを辿ることはできません。

指定されたスレッドに対してアシスタントから生成結果を得るには、Run を作成する必要があります。Run を作成すると、アシスタントはスレッドのメッセージを見て、1つの応答を追加するか、ツールを使用してアクションを実行する必要があるか判断します。

Run を作成しましょう。`threads.runs.create` は `submit_message` ヘルパー関数の中で記述しています。アシスタントとスレッドの両方を指定する必要があります。

```python
#指定されたスレッドにユーザーメッセージを送信し、その後に同じスレッドで Run を開始
def submit_message(assistant_id, thread_id, user_message):
    client.beta.threads.messages.create(
        thread_id=thread_id, role="user", content=user_message
    )
    return client.beta.threads.runs.create(
        thread_id=thread_id,
        assistant_id=assistant_id,
        
# すでに作成されたスレッドにメッセージを送信する
def create_run(user_input):
    run = submit_message(assistant.id, thread.id, user_input)
    return run

```

```python
run = create_run("有楽町駅近辺でイタリアンのレストランを探しています")
run = wait_on_run(run, thread.id)
# スレッドのメッセージ一覧を出力 client.beta.threads.messages.list
pretty_print(get_response(thread.id))
```

>Initial Run status: queued
Run status: queued
Run status: in_progress
Run status: in_progress
Run status: in_progress
Run status: in_progress
Run status: requires_action
Final Run status: **requires_action**
\# Messages
user: 有楽町駅近辺でイタリアンのレストランを探しています


Chat Completions API で完了を作成するのとは異なり、**Run の作成は非同期操作** です。Runのメタデータはすぐに返され、そのメタデータには `status` が含まれます。アシスタントが操作（ツールの使用やメッセージの追加など）を実行すると、`status`は更新されます。

Run のステータス遷移に注目してください。
`queued -> in_progress -> requires_action`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/50dee5ba-065a-892f-56d8-2858b5f5c520.png)
(画像は [OpenAI 社より引用](https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps))

### Run のステータス遷移をすべて出力する
アシスタントの処理がいつ完了したかを知るには、ループ内で Run をポーリングします。

```python
#Run の非同期処理が完了するまでそのステータスを監視し続けるためのループを実装
import time
def wait_on_run(run, thread_id):
    # 最初のステータス出力
    print(f"Initial Run status: {run.status}")
    while run.status == "queued" or run.status == "in_progress":
        run = client.beta.threads.runs.retrieve(
            thread_id=thread_id,
            run_id=run.id,
        )
        print(f"Run status: {run.status}")
        time.sleep(1)
    # ループ終了後の最終ステータス出力
    print(f"Final Run status: {run.status}\n")
    return run
```

# 4. Tools （Function の呼び出し）
Assistants API の重要な機能は、Code Interpreter、検索、カスタム関数などの Tool をアシスタントに装備できることです。Tool は最大 128 個まで登録できます。

アシスタントが正確に何をしているのかもっと詳しく知りたい場合は、Run の**ステップ**を見る必要があります。

## 4.1. ステップ
Run の `status` が `requires_action` になると **Run の実行が一時停止**され、Tool の実行結果の待ち状態となります。この時、当該 Run の内部ではどうなっているのでしょうか。Run の下位の実行単位である Run step を参照してみましょう。

```python
run_steps = client.beta.threads.runs.steps.list(
    thread_id=thread.id, run_id=run.id, order="asc"
)

print(run_steps.model_dump_json(indent=2))
```

いっぱい出てきちゃいましたね。呼ぶべき Tool は `step_details` の中にあります。

```python
for step in run_steps.data:
    step_details = step.step_details
    print(step_details.model_dump_json(indent=2))
```

`tool_calls` の中に呼ぶべき Function `name` と引数 `arguments` が抽出されました！

```json
{
  "tool_calls": [
    {
      "id": "call_Je8w5sIPkdaB8WzEjC2PrDK5",
      "function": {
        "arguments": "{\"keyword\":\"有楽町駅 イタリアン\"}",
        "name": "search_hotpepper_shops",
        "output": null
      },
      "type": "function"
    }
  ],
  "type": "tool_calls"
}
```

## 4.2. tool_call オブジェクトの取得
Assistant API の Function calling はあくまで呼ぶべき関数の選択とパラメータの抽出のみしか行いません。**実際の関数実行はアプリケーション側で行います**。関数の実行に必要な `name` と `arguments` を解析して必要な情報を抽出しましょう。

```python
tool_call = run.required_action.submit_tool_outputs.tool_calls[0]
name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
arguments
```

## 4.3. ホットペッパー API の実行
ホットペッパーさんの[グルメ API](https://webservice.recruit.co.jp/doc/hotpepper/reference.html) はめちゃめちゃパラメータがありますので、今回サンプルで使用する分だけ選別しています。

`tool_call` オブジェクトから得られた検索パラメータを `search_hotpepper_shops` 関数のパラメータとして流し込みます。

```python
import requests

def search_hotpepper_shops(api_key, keyword=None, private_room=0, start=1, count=3, response_format='json'):
    base_url = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/"
    params = {
        "key": api_key,
        "format": response_format,
        "start": start,
        "count": count,
        "private_room": private_room,  # 個室ありの店舗のみを検索, 0:絞り込まない, 1:絞り込む
        #"budget": "B005"
    }
    
    # キーワードが指定されている場合、パラメータに追加
    if keyword:
        params["keyword"] = keyword

    response = requests.get(base_url, params=params)
    
    # レスポンス形式に応じて結果を処理
    if response_format == 'json':
        return response.json()  # JSON形式のレスポンスを返す
    else:
        return response.text  # XML形式の場合は、レスポンスのテキストをそのまま返す

# 使用例
api_key = "Your API Key"  # APIキーを設定
keyword = arguments.get("keyword")  # 検索キーワードを設定
private_room = arguments.get("private_room", 0) # 個室ありの店舗のみを検索, 0:絞り込まない, 1:絞り込む
response = search_hotpepper_shops(api_key, keyword=keyword, private_room=private_room, response_format='json')

print(response)  # 検索結果を表示
```

### 出力を構造化
GPT-4 に渡すデータだけ構造化しています。

```python
shops = []
for s in response["results"]["shop"]:
    shop = {"name": s["name"],
            "address": s["address"],
            "station_name": s["station_name"],
            "access": s["access"],
            "genre": s["genre"],
            "budgetAverage": s["budget"]["average"],
            "open": s["open"],
            "close": s["close"]
    }
    shops.append(shop)

shops
```

```python
[{'name': 'ウメ子の家 銀座 有楽町駅前店',
  'address': '東京都千代田区有楽町２－３－５\u3000aune有楽町７Ｆ',
  'station_name': '有楽町',
  'access': '地下鉄日比谷線、千代田線日比谷駅Ａ1出口直結／地下鉄銀座駅C1出口すぐです！！絶品のイタリアン料理とビールで乾杯♪',
  'genre': {'name': '居酒屋',
   'catch': '居酒屋 イタリアン ステーキ 個室\u3000宴会',
   'code': 'G001'},
  'budgetAverage': '3700円',
  'open': '月～日、祝日、祝前日: 17:00～23:30 （料理L.O. 22:30 ドリンクL.O. 23:00）',
  'close': '無'},
 {'name': 'マイアミヴィッラ 有楽町イトシアプラザ店',
  'address': '東京都千代田区有楽町２－７－１－S11\u3000有楽町駅前ビルディング１F',
  'station_name': '有楽町',
  'access': '有楽町駅前広場前のイトシアプラザ１Ｆにございます！！',
  'genre': {'name': 'イタリアン・フレンチ', 'catch': '有楽町駅前広場前のイタリアン☆', 'code': 'G006'},
  'budgetAverage': '2000円',
  'open': '月～日、祝日、祝前日: 10:00～23:00 （料理L.O. 22:00 ドリンクL.O. 22:30）',
  'close': 'なし'},
 {'name': 'イルバロッコ 有楽町店',
  'address': '東京都千代田区有楽町２‐１‐１６',
  'station_name': '有楽町',
  'access': 'JR有楽町駅徒歩3分／地下鉄千代田線日比谷駅徒歩3分／地下鉄丸ノ内線銀座駅徒歩3分',
  'genre': {'name': 'イタリアン・フレンチ', 'catch': 'イタリアン・バール', 'code': 'G006'},
  'budgetAverage': '2000円（通常平均）／850円（ランチ平均）',
  'open': '月～日、祝日、祝前日: 11:00～22:00 （料理L.O. 21:30）',
  'close': '不定休'}]
  ```

 <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>
 
## 4.4. Tool 出力の送信
それでは、ホットペッパー API のレスポンスをアシスタントに送信してみましょう。先ほど解析した `tool_call` で見つかった `tool_call` ID が必要です。

```python
input={"answer": shops, "success": True}

run = client.beta.threads.runs.submit_tool_outputs(
    thread_id=thread.id,
    run_id=run.id,
    tool_outputs=[
        {
            "tool_call_id": tool_call.id,
            "output": json.dumps(input),
        }
    ],
)

run = wait_on_run(run, thread.id)
# スレッドのメッセージ一覧を出力 client.beta.threads.messages.list
pretty_print(get_response(thread.id))
```

Initial Run status: queued
Run status: queued
Run status: in_progress
Run status: completed
Final Run status: **completed**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/f1b99c72-798d-8e9f-d911-63d1c503ca0b.png)

```
user: 有楽町駅近辺でイタリアンのレストランを探しています
assistant: 有楽町駅近辺でイタリアンのレストランをいくつか見つけました。以下のオプションをご検討ください。

1. ウメ子の家 銀座 有楽町駅前店
   - 住所: 東京都千代田区有楽町2-3-15 aune有楽町7F
   - アクセス: 地下鉄日比谷線、千代田線日比谷駅！1出口直結／地下鉄銀座線銀座駅C1出口すぐです！！
   - ジャンル: 居酒屋、イタリアン、ステーキ、個室　宴会
   - 平均予算: 3,700円
   - 営業時間: 月～日、祝日、祝前日: 17:00～23:30（料理L.O. 22:30 ドリンクL.O. 23:00）

2. マイアミヴィラ 有楽町イトシアプラザ店
   - 住所: 東京都千代田区有楽町2-7-1-S11 有楽町駅前ビルディング1F
   - アクセス: 有楽町駅前広場前のイトシアプラザ1Fにございます！！
   - ジャンル: イタリアン・フレンチ
   - 平均予算: 2,000円
   - 営業時間: 月～日、祝日、祝前日: 10:00～23:00（料理L.O. 22:00 ドリンクL.O. 22:30）

3. イルバロッコ 有楽町店
   - 住所: 東京都千代田区有楽町2-1-16
   - アクセス: JR有楽町駅徒歩3分／地下鉄千代田線日比谷駅徒歩3分／地下鉄丸ノ内線銀座駅徒歩3分
   - ジャンル: イタリアン・バール
   - 平均予算: 2,000円（通常平均）／850円（ランチ平均）
   - 営業時間: 月～日、祝日、祝前日: 11:00～22:00（料理L.O. 21:30）

これらの情報を元にお好みのレストランを選んでください。
```

:::note warn
注意
`requires_action` ステータスの継続時間は約 10 分です。この時間を過ぎると `expired` となり Run が終了されます。
:::

## 4.5. Run ステップの確認
これでもう一度 Run が完了するのを待ち、Run ステップをチェックします。

```python
run_steps = client.beta.threads.runs.steps.list(
    thread_id=thread.id, run_id=run.id, order="asc"
)

for step in run_steps.data:
    step_details = step.step_details
    print(step_details.model_dump_json(indent=2))
```

2 つのステップの `step_details` が存在しているかと思います。

1. `tool_calls` (複数形。1つのステップで複数になる可能性があるため)
2. `message_creation` メッセージの作成

最初のステップは `tool_calls` で、今回の `function` や `code_interpreter` の呼び出しが格納されます。

- `input` は Tool が呼ばれる前に生成された Python コード
- Tool を実行した結果の `output` が格納

2 つ目のステップは `message_creation` で、結果をユーザーに伝えるためにスレッドに追加された `message` が含まれます。

## 4.6. メッセージの確認
```python
messages = client.beta.threads.messages.list(
  thread_id=thread.id
)
print(messages.model_dump_json(indent=2))
```

```json
{
  "data": [
    {
      "id": "msg_xxx",
      "assistant_id": "asst_xxx",
      "content": [
        {
          "text": {
            "annotations": [],
            "value": "有楽町駅近辺でイタリアンのレストランをいくつか見つけました。以下のオプションをご検討ください。\n\n1. ウメ子の家 銀座 有楽町駅前店\n   - 住所: 東京都千代田区有楽町2-3-15 aune有楽町7F\n   - アクセス: 地下鉄日比谷線、千代田線日比谷駅！1出口直結／地下鉄銀座線銀座駅C1出口すぐです！！\n   - ジャンル: 居酒屋、イタリアン、ステーキ、個室　宴会\n   - 平均予算: 3,700円\n   - 営業時間: 月～日、祝日、祝前日: 17:00～23:30（料理L.O. 22:30 ドリンクL.O. 23:00）\n\n2. マイアミヴィラ 有楽町イトシアプラザ店\n   - 住所: 東京都千代田区有楽町2-7-1-S11 有楽町駅前ビルディング1F\n   - アクセス: 有楽町駅前広場前のイトシアプラザ1Fにございます！！\n   - ジャンル: イタリアン・フレンチ\n   - 平均予算: 2,000円\n   - 営業時間: 月～日、祝日、祝前日: 10:00～23:00（料理L.O. 22:00 ドリンクL.O. 22:30）\n\n3. イルバロッコ 有楽町店\n   - 住所: 東京都千代田区有楽町2-1-16\n   - アクセス: JR有楽町駅徒歩3分／地下鉄千代田線日比谷駅徒歩3分／地下鉄丸ノ内線銀座駅徒歩3分\n   - ジャンル: イタリアン・バール\n   - 平均予算: 2,000円（通常平均）／850円（ランチ平均）\n   - 営業時間: 月～日、祝日、祝前日: 11:00～22:00（料理L.O. 21:30）\n\nこれらの情報を元にお好みのレストランを選んでください。"
          },
          "type": "text"
        }
      ],
      "created_at": 1707668144,
      "file_ids": [],
      "metadata": {},
      "object": "thread.message",
      "role": "assistant",
      "run_id": "run_xxx",
      "thread_id": "thread_xxxxx"
    },
    {
      "id": "msg_xxx",
      "assistant_id": null,
      "content": [
        {
          "text": {
            "annotations": [],
            "value": "有楽町駅近辺でイタリアンのレストランを探しています"
          },
          "type": "text"
        }
      ],
      "created_at": 1707667772,
      "file_ids": [],
      "metadata": {},
      "object": "thread.message",
      "role": "user",
      "run_id": null,
      "thread_id": "thread_xxxxx"
    },
```

最終的に 2 つのメッセージが作成されて対話が完成しました。

# 5. Code Interpreter
計算が必要な質問をすると、自動的に [Code Interpreter](https://learn.microsoft.com/azure/ai-services/openai/how-to/code-interpreter?tabs=python) を起動して質問文から計算式を立式して計算結果を返却します。

```python
run = create_run("2/14 なにわ亭で10,000円、2/15 朝食3,000円、宿泊代16,000円、新幹線代は片道で14,920円でした。合計金額を計算してください。あ、新幹線代は往復でね。")
run = wait_on_run(run, thread.id)
# スレッドのメッセージ一覧を出力
pretty_print(get_response(thread.id))
```

>Initial Run status: queued
Run status: in_progress
Run status: in_progress
Run status: completed
Final Run status: **completed**
\# Messages
user: 2/14 なにわ亭で10,000円、2/15 朝食3,000円、宿泊代16,000円、新幹線代は片道で14,920円でした。合計金額を計算してください。あ、新幹線代は往復でね。
assistant: 新幹線代を往復で計算すると、出張全体での合計金額は**58,840**円となります。

```python
run_steps = client.beta.threads.runs.steps.list(
    thread_id=thread.id, run_id=run.id, order="asc"
)
for step in run_steps.data:
    step_details = step.step_details
    print(step_details.model_dump_json(indent=2))
```

```json
"tool_calls": [
    {
      "id": "call_uHpmvNT5qFLHVrIgAvB9fsjR",
      "code_interpreter": {
        "input": "# 各項目の金額
            restaurant_cost = 10000
            breakfast_cost = 3000
            accommodation_cost = 16000
            shinkansen_one_way_cost = 14920
            
            # 往復なので新幹線の費用は片道の2倍
            shinkansen_round_trip_cost = shinkansen_one_way_cost * 2
            
            # 合計金額の計算
            total_cost = restaurant_cost + breakfast_cost + accommodation_cost + shinkansen_round_trip_cost
            
            total_cost",
        "outputs": [
          {
            "logs": "58840",
            "type": "logs"
          }
        ]
      },
      "type": "code_interpreter"
    }
  ],
```
Code Interpreter のおかげでモデルが苦手な計算問題も任せられますね。

# 6. 並列 Function calling
複数の Tool の結果が必要となるような複雑な指示を投入します。

```python
run = create_run("2/14に大阪に1人で出張します。大阪駅近辺の当日の居酒屋と20000円以内で泊まれる大阪駅のホテルを提案してください。")
run = wait_on_run(run, thread.id)
# スレッドのメッセージ一覧を出力
pretty_print(get_response(thread.id))
```

>Initial Run status: queued
Run status: in_progress
Run status: in_progress
Run status: requires_action
Final Run status: **requires_action**
\# Messages
user: 2/14に大阪に1人で出張します。大阪駅近辺の当日の居酒屋と20000円以内で泊まれる大阪駅のホテルを提案してください。

```python
run_steps = client.beta.threads.runs.steps.list(
    thread_id=thread.id, run_id=run.id, order="asc"
)

for step in run_steps.data:
    step_details = step.step_details
    print(step_details.model_dump_json(indent=2))
```

```json
"tool_calls": [
  {
    "id": "call_mLV4SqqoZszCjt0utqaaQtcw",
    "function": {
      "arguments": "{\"keyword\": \"大阪駅 居酒屋\"}",
      "name": "search_hotpepper_shops",
      "output": null
    },
    "type": "function"
  },
  {
    "id": "call_IkYzUhdJs6dc0doMkwePmFr6",
    "function": {
      "arguments": "{\"latitude\": 34.702485, \"longitude\": 135.495951, \"searchRadius\": 1.0, \"checkinDate\": \"2024-02-14\", \"checkoutDate\": \"2024-02-15\", \"maxCharge\": 20000, \"adultNum\": 1}",
      "name": "search_vacant_hotels",
      "output": null
    },
    "type": "function"
  }
],

```

COOL!😎

# 7. 付録 Assistants API 操作でよく使う API
### Run のキャンセル
`in_progress` 状態の Run をキャンセルします。

```python
client.beta.threads.runs.cancel(thread_id=thread.id, run_id="run_xxx")
```
### スレッドの削除
スレッドの削除はできますが、メッセージの削除は…　ありません。
```python
client.beta.threads.delete(thread_id="thread_xxx")
```
### アシスタントのアップデート
途中でアシスタントの設定を修正したい場合、`assistant_id` を指定してアップデートできます。

```python
client.beta.assistants.update(assistant_id="Your Assistant ID",
    name="Travel Assistant",
    instructions="""xxx"""
    tools=tools_list,
    model="gpt-4-0125-preview"
    )
```

### アシスタントの削除
```python
client.beta.assistants.delete("asst_xxx")
```

# Github
もっと細かく一連の動作を確認するにはこちらの Notebook を参照ください。

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

# 第２回
AutoGen を使用してマルチエージェントアーキテクチャの技術検証を行います。

https://qiita.com/nohanaga/items/e059336a54f87ff55df9

# 参考
以下を参考に作成しました。

https://platform.openai.com/docs/assistants/how-it-works

https://github.com/openai/openai-cookbook/blob/main/examples/Assistants_API_overview_python.ipynb