OpenAIのアシスタントAIを使って、Function Callingを試してみた

新しく公開された、AssistantAIからもFunction Callができるとのことでしたので、試してみました。

以前、ChatGPTから関数を呼び出す「Function calling」については、以下の記事で記載しています。関数の定義方法などは類似していますので、参照して下さい。

OpenAI ChatGPTのFunction callingを調べてみた

OpenAIからChatGPTの新しいモデル(gpt-3.5-turbo-0613、gpt-3.5-turbo-16k-0613)が発表されました。16kのつくモデルは最大トークン数が従来の4倍になっていて、より多…

Function calling

OpenAIの準備

Open AIのAPIが使えるように準備します。

!pip install openai --upgrade

事前にOpenAIに登録して、organizationを取得し、api_keyを作成する必要があります。

from openai import OpenAI

client = OpenAI(
  api_key=<Your api key>,
  organization=<Your org key>,
)

assistant AI の生成 (Function calling)

場所と単位を与えると天気を返す、getCurrentWeather関数を定義します。この関数はダミー関数で、常に同じweather_infoを返します。

def getCurrentWeather(location, unit="fahrenheit"):
    """Get the current weather in a given location"""
    weather_info = {
        "location": location,
        "temperature": "72",
        "unit": unit,
        "forecast": ["sunny", "windy"],
    }
    return json.dumps(weather_info)

アシスタントを生成します。

assistant = client.beta.assistants.create(
  instructions="You are a weather bot. Use the provided functions to answer questions.",
  model="gpt-3.5-turbo-1106",

  tools=[{
      "type": "function",
    "function": {
      "name": "getCurrentWeather",
      "description": "Get the weather in location",
      "parameters": {
        "type": "object",
        "properties": {
          "location": {"type": "string", "description": "The city and state e.g. San Francisco, CA"},
          "unit": {"type": "string", "enum": ["c", "f"]}
        },
        "required": ["location"]
      }
    }
  
  }]
)

スレッドを生成します。

thread = client.beta.threads.create()

ユーザからのメッセージ「明日の京都の天気について教えて」を生成します。

message = client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="明日の京都の天気について教えて"
)

runオブジェクトを生成します。

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

この時点でのrun.statusは"queued"になっています。

runオブジェクトのライフサイクルは、以下のようになっています。

Run lifecycle

Run objects can have multiple statuses.

Run lifecycle - diagram showing possible status transitions
STATUSDEFINITION
queuedWhen Runs are first created or when you complete the required_action, they are moved to a queued status. They should almost immediately move to in_progress.
in_progressWhile in_progress, the Assistant uses the model and tools to perform steps. You can view progress being made by the Run by examining the Run Steps.
completedThe Run successfully completed! You can now view all Messages the Assistant added to the Thread, and all the steps the Run took. You can also continue the conversation by adding more user Messages to the Thread and creating another Run.
requires_actionWhen using the Function calling tool, the Run will move to a required_action state once the model determines the names and arguments of the functions to be called. You must then run those functions and submit the outputs before the run proceeds. If the outputs are not provided before the expires_at timestamp passes (roughly 10 mins past creation), the run will move to an expired status.
expiredThis happens when the function calling outputs were not submitted before expires_at and the run expires. Additionally, if the runs take too long to execute and go beyond the time stated in expires_at, our systems will expire the run.
cancellingYou can attempt to cancel an in_progress run using the Cancel Run endpoint. Once the attempt to cancel succeeds, status of the Run moves to cancelled. Cancellation is attempted but not guaranteed.
cancelledRun was successfully cancelled.
failedYou can view the reason for the failure by looking at the last_error object in the Run. The timestamp for the failure will be recorded under failed_at.
https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps

retreivalの場合には、client.beta.threads.runs.createを実行すると、直ちにmessageが生成されましたが、Function callingの場合には、一旦queuedで止まるようです。

これは、先の記事内でも説明していますが、FanctionCallingを実行する際には2回GPTモデルにアクセスする必要があるためと思われます。すなわち、1) Functionに渡す引数を解析するためと、2)Functionからの結果に基づいて回答を生成するための2回です。

つまり、1)と2)との間で、Functionを呼び出すことになります。

runオブジェクトは、以下の処理を実行することでrequires_actionに遷移します。(10分すぎるとexpireされるので注意が必要です)

run = client.beta.threads.runs.retrieve(
    thread_id=thread.id,
    run_id=run.id
)

これで、run.status が ”requires_action”となりました。 この状態になると必要なパラメータ(Functionに与える引数)を取得することができます。

import json 

# require_actionのパラメータの取得
tool_id = run.required_action.submit_tool_outputs.tool_calls[0].id
function_arguments = json.loads(run.required_action.submit_tool_outputs.tool_calls[0].function.arguments)

ちなみに、function_argumentsは、以下のようになっています。

{'location': 'Kyoto', 'unit': 'c'}

必須の引数である"location"に、”Kyoto”がセットされています。この引数を使って、Functionを実行します。

result = getCurrentWeather(
    tool_function_arguments["location"], 
    tool_function_arguments["unit"]
)

getCurrentWeathre関数の結果(result)は以下のようになります。

{"location": "Kyoto", "temperature": "72", "unit": "c", "forecast": ["sunny", "windy"]}

最後に関数の結果をassistant aiに送り、メッセージを受け取ります。

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

messages = client.beta.threads.messages.list(
    thread_id=thread.id,
    order="asc"
)
for message in messages:
    print(message.role, ":", message.content[0].text.value)

user : 明日の京都の天気について教えて
assistant : 明日の京都は晴れで、気温は摂氏72度です。風も強いようです。

まとめ

retrievalに比べて少し手順が多かったですが、getCurrentWeatherの結果に基づいて、回答を生成することができました。