【Function Calling】OpenAI AssistantsとChatCompletionでの活用法まとめ

# 背景
- 以前OpenAI AssistantsのKnowledge Retrievalを解説しました

https://qiita.com/munaita_/items/b34c28936f973c491e64

- 今回はOpenAI AssisntantsのFunction Callingについて解説します
- ChatCompletionにもFunction Calling機能があり似ているので、ここでまとめて解説していきます

# Function Callingとは
OpenAIのモデルに関数定義を複数渡してFunction Callingリクエストをすると、プロンプト指示に従いどの関数をどういった引数で呼び出すべきか判断し、実行をサポートする情報を提供してくれます. **関数を実行するわけではなく**、どの関数を実行すべきか、どの引数で呼び出すべきかを教えてくれるものです.

# ChatCompletionとAssistantsのFunction Callingの違い

機能としてはほぼ一緒です.ChatCompletionではMessageに直接関数を定義できるのに対し、Assistantsの場合、Assistants作成時に定義して紐づける必要があります. 

# ChatCompletionのおけるFunction Calling使い方

## サンプルコード

まずはサンプルとなる関数を用意します.

```py
def get_weather(location: str) -> str:
    if location == 'tokyo':
        return 'sunny'
    elif location == 'NY':
        return 'cloudy'
    elif location == 'london':
        return 'rainy'
    elif location == 'paris':
        return 'windy'
    else:
        return 'snow'

def infer_temprature_from_weather(weather: str) -> int:
    if weather == 'sunny':
        return 20
    elif weather == 'cloudy':
        return 10
    elif weather == 'rainy':
        return 5
    elif weather == 'windy':
        return 1
    else:
        return 0
```

toolsを定義してChatCompletionのFunction Callingで `東京とニューヨークとロンドンの天気教えて`とプロンプトを書いてみます. 

```py
from openai.types.chat import ChatCompletionToolParam
from openai import OpenAI

tools = [
    ChatCompletionToolParam({
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "指定した場所の現在の天気を取得する",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "県名や都市名（例：tokyo、ny、london, paris）",
                    },
                },
                "required": ["location"],
            },
        },
    }),
    ChatCompletionToolParam({
        "type": "function",
        "function": {
            "name": "infer_temperature_from_weather",
            "description": "指定した天気の温度を予測する",
            "parameters": {
                "type": "object",
                "properties": {
                    "weather": {
                        "type": "string",
                        "description": "天気（例：sunny, cloudy, rainy, windy, cloudy）",
                    },
                },
                "required": ["weather"],
            },
        },
    })
]
    

client = OpenAI()
messages = [{"role": "user", "content": "東京とニューヨークとロンドンの天気教えて"}]
response = client.chat.completions.create(
    model="gpt-4-1106-preview",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

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

以下のようなJSONが返ります. 重要なのは tool_callsの値です. 
`location: tokyo` や, `location: ny` や `location: london` があります。この情報を利用して端末で関数を実行すれば、`東京とニューヨークとロンドンの天気教えて`の回答をするために必要な情報が揃うことをOpenAIのモデルが教えてくれています. 

```py
{
  "id": "chatcmpl-8ndXpANK4Cfg2G8gBzQ9ExyDKulQO",
  "choices": [
    {
      "finish_reason": "tool_calls",
      "index": 0,
      "logprobs": null,
      "message": {
        "content": null,
        "role": "assistant",
        "function_call": null,
        "tool_calls": [
          {
            "id": "call_zGQSQ0cv8lL3HKEmrKwpPJSo",
            "function": {
              "arguments": "{\"location\": \"tokyo\"}",
              "name": "get_weather"
            },
            "type": "function"
          },
          {
            "id": "call_WgiAqGgjR65VxDtudcVtk4Fc",
            "function": {
              "arguments": "{\"location\": \"ny\"}",
              "name": "get_weather"
            },
            "type": "function"
          },
          {
            "id": "call_GYhWHsNBcVl1opVs8c1QS4Nm",
            "function": {
              "arguments": "{\"location\": \"london\"}",
              "name": "get_weather"
            },
            "type": "function"
          }
        ]
... 略 ...
```

- 関数を複数渡して、Promptを渡すと、タスクを実行するのに必要な情報を取得するためにどの関数を実行すればいいかを判断し、どの引数で呼び出すべきかを教えてくれました
- FunctionCalling機能では**関数は実行されない**と言うポイントに注意してください
    - ※一回のレスポンスで複数の呼び出しの情報が返ってくるようになったのは大きな修正ポイント
- さて、`東京とニューヨークとロンドンの天気教えて` の回答文書を作るためには、実際に関数を実行してその結果を再度OpenAIのモデルに渡す必要があります. レスポンスのID情報と関数の結果の情報を付与してモデルに与えるとその回答文章を作ってくれるのですが、そのコードはここでは省略します. 

↓ 詳細

https://platform.openai.com/docs/guides/function-calling



# AssistantsにおけるFunction Calling

本題の、AssistantsにおけるFunction Callingです.

![スクリーンショット 2024-02-02 18.12.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20307/88936d9e-9cd9-7343-66ba-4b8555f19a9d.png)


https://platform.openai.com/docs/assistants/tools/function-calling

関数情報付与したAssistantを作成し、Thread作成してRunの実行結果を得る(1 ~ 4)

```py
import json
import time
from openai.types.chat import ChatCompletionToolParam
from openai import OpenAI

client = OpenAI()

# assitantの作成
assistant = client.beta.assistants.create(
  instructions="あなたはお天気Botです. 与えられた関数を使って質問に答えてください.",
  model="gpt-4-turbo-preview",
  tools=tools
)

# threadの作成
thread = client.beta.threads.create(
    messages=[{
        "role": "user",
        "content": "東京とロンドンの天気を教えて"
    }]
)

# 実行
run = client.beta.threads.runs.create(
    thread_id=thread.id,
    assistant_id=assistant.id,
)

# Runが required_action になるまで繰り返す
for i in range(100):
    print(f'={i+1}回目=')
    run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)
    if run.status == 'requires_action':
        break
    else:
        time.sleep(3)

# 呼び出すべき関数情報
tool_calls = run.required_action.submit_tool_outputs.tool_calls

print(tool_calls)
```
`arguments='{"location": "tokyo"}', name='get_weather')`と`arguments='{"location": "london"}', name='get_weather'`が取得できています.

```:結果
[RequiredActionFunctionToolCall(id='call_TUw4REteuGy9DBFr4jel1WOO',
function=Function(arguments='{"location": "tokyo"}', name='get_weather'), type='function'), RequiredActionFunctionToolCall(id='call_1sWe7oM4FbYq4I5oaTJygp39', 
function=Function(arguments='{"location": "london"}', name='get_weather'), type='function')]
```

5の関数の実行結果をOpenAIに渡すところ

```py
run = client.beta.threads.runs.submit_tool_outputs(
  thread_id=thread.id,
  run_id=run.id,
  tool_outputs=[
      {
        "tool_call_id": tool_calls[0].id,
        "output": "sunny",
      },
      {
        "tool_call_id": tool_calls[1].id,
        "output": "rainy",
      },
    ]
)

# Runが required_action になるまで繰り返す
for i in range(100):
    print(f'={i+1}回目=')
    run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)
    if run.status == 'completed':
        break
    else:
        time.sleep(3)

messages = client.beta.threads.messages.list(thread_id=thread.id)
print(messages)
```

`value='東京の天気は晴れです。ロンドンの天気は雨です。')`が取得できました.

```:結果
# SyncCursorPage[ThreadMessage](data=[ThreadMessage(id='msg_xmpGnKJLH3YBlRRf2bvqyHp0', assistant_id='asst_3OrnguxLF4Rk8CThDNxRoqld', content=[MessageContentText(text=Text(annotations=[], 
value='東京の天気は晴れです。ロンドンの天気は雨です。'), type='text')], created_at=1706863407, file_ids=[], metadata={}, object='thread.message', role='assistant', run_id='run_CKiwHaqfDq0PB2iGaoyoQd1N', thread_id='thread_kuxoh1zyUAQHG1U7ZrG4sGH4'), ThreadMessage(id='msg_n1GjsrkDrJnkoWoTCdNxWW0Q', assistant_id=None, content=[MessageContentText(text=Text(annotations=[], value='東京とロンドンの天気を教えて'), type='text')], created_at=1706863399, file_ids=[], metadata={}, object='thread.message', role='user', run_id=None, thread_id='thread_kuxoh1zyUAQHG1U7ZrG4sGH4')], object='list', first_id='msg_xmpGnKJLH3YBlRRf2bvqyHp0', last_id='msg_n1GjsrkDrJnkoWoTCdNxWW0Q', has_more=False)
```

# 参考


https://platform.openai.com/docs/api-reference/chat/create