みなさんこんにちは。ワンダーソフト開発部です。
今日は、openai の Function Calling を scala-cli から呼び出してみた備忘となります。
(弊社のコーヒーを飲みつつのブログ投稿となります☕️)
モチベーション
普段は、タスクランナーは、Just や mise を利用していますが、scala-cli を試したい気持ちもあります。
さらに、定型作業であり、でも、ちょっとバリエーションもあって、というタスクを Function Calling とかでいい感じにできるのかどうか、は気になっていた部分でした。
ユースケースなど
「アプリケーションのログを確認してエラーを探して。特定のエラーコードがあって、ログに注文番号があったらSQLを実行して該当の注文を見て。該当注文を実行する場合の値付けや割引が適正かを調べて。」
「天気の様子を見て、どの種類のコーヒー豆を用意すべきかを提案してください、その種類のコーヒー豆を約500名に配る場合にどのくらいの量のコーヒー豆をそれぞれ準備すべきかを計算して教えて。カップ(紙かプラスティック)によって原価も変わるので計算して」
などなどがあるのではないか、と考えてみました。ChatGPTやClaudeにそのまま入力しても大丈夫そうな気もしつつ、マクロ的に登録して呼び出したいのと、途中で単純計算がある場合、こちらが用意した計算式や既存関数のロジックを当てた方がいいのかな、というのもあるように思いました。(全部プロンプトで頑張れなくもないとも思っています)
最初の一歩のサンプル
とはいえ、初手からゴリゴリのユースケースに行くのではなく、小さい実例でそもそもどういう挙動になるのかをみに行くことにしました。
「今日の天気を調べて、気温が〇〇度以上であればアイスコーヒーを注文、そうでなければ、ホットコーヒーを注文して」にトライしました。
ライブラリ
以下を利用しました。(これを機に内部実装なども眺め始めています)
https://github.com/softwaremill/sttp-openai
実装のサンプル
天気の取得とコーヒーの注文をする関数を用意しました。
def getWeather(query: WeatherQuery): WeatherInfo = {
val logWeatherRequest = (city: String) => println(s"天気情報を取得中: $city")
val temperature = generateMockTemperature
logWeatherRequest(query.city)
WeatherInfo(
city = query.city,
temperature = temperature,
description = createWeatherDescription(temperature)
)
}
def orderCoffee(order: CoffeeOrder): OrderResult = {
logCoffeeOrder(order.coffeeType, order.reason)
OrderResult(
orderId = generateOrderId,
coffeeType = order.coffeeType,
status = "注文完了"
)
}
それぞれの関数を処理するために、以下のように ChatBody を作ります。
val initialMessage = UserMessage(content = TextContent("東京の天気を調べて、30度以上ならアイスコーヒー、それ以下ならホットコーヒーを注文してください"))
val chatRequest = ChatBody(
model = GPT4oMini,
messages = Vector(initialMessage),
tools = Some(Seq(
FunctionTool.withSchema[WeatherQuery](
name = "get_weather",
description = Some("指定した都市の現在の天気情報を取得します")),
FunctionTool.withSchema[CoffeeOrder](
name = "order_coffee",
description = Some("コーヒーを注文します。coffeeTypeには'アイスコーヒー'または'ホットコーヒー'を指定してください"))
)),
toolChoice = None
)
作成した chatRequest を 処理させます。この時、state が再起的に処理されることになります。
def processChat(
state: ChatState,
request: ChatBody
): Try[String] = Try {
def loop(currentState: ChatState): String = {
val result = openAI.createChatCompletion(request.copy(messages = currentState.messages))
val toolCalls = result.choices.head.message.toolCalls
if (toolCalls.nonEmpty) {
val toolMessages = processToolCalls(toolCalls, getWeather, orderCoffee)
val newState = updateChatState(currentState, toolCalls, toolMessages)
loop(newState)
} else {
result.choices.head.message.content
}
}
loop(state)
}
val initialState = ChatState.initial(initialMessage)
processChat(initialState, chatRequest)
想定する usecase とか、処理群が増えると、tools がわちゃわちゃしそうですが、何かしらの条件があって、調査してもらったり、計算してもらう、というのを一つ一つのタスクは定義しやすいけど、組み合わせて全体で定型業務に落とし込むにはちょっとな、、という場合にはこれはありかも、、と思いました。
さらに試してみたいこと
Structured Outputs と型のある言語は相性がいいのではないのかな、という期待もあるので、こういうので何かしらいい感じに日々のちょっとした業務改善ができそうな場合、組み合わせて使ってみたいな、とも思っています。
ログの調査とかだと最後に報告メッセージやレポートも必要なので、定型的な情報群を出力できると嬉しいとも思っています。
そして気づけば
Function Calling を使って他にも何かできないか、夢膨らんできました。
まだまだ残暑が厳しいですが、「バタック・ブルー」で淹れたアイスコーヒー楽しんでいます。
今後も、ソフトウェア開発やデザインのあれこれについて、コーヒーとともにゆるく投稿していきますので、どうぞお楽しみに ☕✨
🔗 Wonder Soft Coffeeのオンラインストアはこちら:
👉 https://coffee.wonder-soft.com/collections/all