Blog スタッフブログ

PHP Swift システム開発

[ChatGPT]Function CallingでECサイトとAPI連携させてみた

こんにちは、株式会社MIXシステム開発担当のBloomです。

6月13日にChatGPTのAPIに大規模なアップデートがありました。以前の記事では16kモデルを利用して長いシステムメッセージの中にECサイトの商品情報を含める用法を紹介しましたが、

今回は追加されたFunction Calling機能を使い、システムメッセージにベタ書きするのではなくAPI呼び出しを行わせる方式で商品紹介を行うbotの開発を実現してみましょう。

[ChatGPT]ChatGPT APIを使って会社紹介botを作ってみた

[ChatGPT]GPT3.5の16kモデルを使ってECサイトの商品紹介をさせてみた

これから記載するコードは過去記事からの流用・改変となります。

実際にやってみた

利用するモデルはgpt-3.5-turbo-16kで変わりありません。今回は関数宣言が必要なのでさらに追記が必要です。

function callChatGptApi($messages) {
    $api_key = "APIキー";
    $model = "gpt-3.5-turbo-16k";

// 前回記事から商品情報を取り払っています。
    $system_content = <<< EOF
技術を提供するECサイト「MIX Mart」を運営しています。
お客様の要望に対して最適な商品を選び提案してください。

「MIX Mart」のコンセプトを記載します。
技術&知識を手軽に提供するネットショップ
企業や個人における様々なお悩みや課題。しかしそれを解消しようとしても、面倒なやり取りや手続き、コスト面から開発案件やデザイン作成等がスムーズに進まない…というのは、いつの世も無くならない、頭の痛い問題です。
そこで株式会社MIXでは、より気軽にネットでサービスを依頼できる入口として、技術直販ショップを開設しました。「モノ」ではなく「弊社の技術」を手軽にシェアすることをコンセプトとしており、お客様の心理的・物理的なハードルを一気に下げることで、課題解決と事業成長を加速化させるお手伝いができればと考えております。お気軽にお問い合わせください!

ここまでがECサイトのコンセプトになります。
EOF; 

    $system = ["role" => "system", "content" => $system_content];
    $contents = array();
    $contents[] = $system;
    foreach($messages as $message) {
        $role = $message["role"];
        $content = $message["content"];
        $name = $message["name"];

        if ($role == "function") { // roleがfunctionの場合、name(関数名)が必須パラメータになります。
            $contents[] = ["role" => $message["role"], "content" => $message["content"], "name" => $message["name"]];
        }
        else {
            $contents[] = ["role" => $message["role"], "content" => $message["content"]];
        }
    }

    $header = ["Authorization: Bearer ".$api_key, "Content-type: application/json"];

    $params = json_encode(
        ["messages" => $contents, 
        "model"=> $model,
        "functions" => [ // ここから関数宣言
            ["name" => "get_items",
            "description"=> "ECサイトの商品一覧と詳細を返却します。price01が通常価格(nullable)、price02が販売価格です。",
            "parameters" => [
                "type"=> "object",
                "properties"=> [
                    "reason" => [
                        "type" => "string",
                        "description" => "商品一覧を得る理由"
                    ]
                ],
                "required"=> []
                ]]
            ]
        ]);

    $curl = curl_init("https://api.openai.com/v1/chat/completions");
    $options = [CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $header,
        CURLOPT_POSTFIELDS => $params,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => false];
    curl_setopt_array($curl, $options);
    $response = curl_exec($curl);
    $httpcode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    return $response;
}
?>

ここでfunctionsのパラメータが増えていることに注目してください。

nameに関数名、descriptionに関数の振る舞い、parametersに引数を記載する必要があります。

今回利用するECサイトから商品情報を取得するget_items関数に引数は不要ですが、parametersにnullや空配列を指定するとAPIでエラーが発生するため仮の引数を記載しています。(ドキュメントによるとoptionalのはずです)

requiredには必須パラメータを配列で記載します。reasonがどうしても欲しければ”reason”を指定してください。

これでAPIを利用する準備が整いました。クライアント側のコードも修正しましょう。

struct Message: Codable {
    struct FunctionCall: Codable {
        let name: String
        let arguments: String?
    }
    let role: String
    let content: String
    let function_call: FunctionCall? // function_callのために追加しています。
    let name: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.role = try container.decode(String.self, forKey: .role)
        // 関数呼び出しを行う場合assistantのroleから返却されるcontentがnullですが、
        // nullのまままた送信するとcontent is a required propertyとエラーが発生するので空文字を入れています。
        self.content = (try? container.decode(String?.self, forKey: .content)) ?? ""
        self.function_call = try? container.decode(FunctionCall?.self, forKey: .function_call)
        self.name = try? container.decode(String?.self, forKey: .name)
    }
}
// ECサイト商品一覧APIの戻り値です。
struct ItemsResponse: Codable {
    struct Item: Codable {
        let name: String
        let about: String
        let price01: Int?
        let price02: Int
    }
    let lists: [Item]
}
// コードは前回の記事と異なる部分を抜粋しています。
class ViewController: UIViewController, UINavigationControllerDelegate {
    // 前回記事のsendTouchUpInsideから実行されるOpenAI APIの戻り値を処理する関数です。
    func chatSucceed(response: ChatResponse) {
        if let choice = response.choices.first {
            messages.append(choice.message)
            table.reloadData()

            // finish_reasonがfunction_callの場合にAPIの実行を行います。
            if choice.finish_reason == "function_call",
               let function = choice.message.function_call {
                // argumentsには引数のJSON文字列が返却されます。必要なら適宜デコードして利用しましょう。
                callFunction(name: function.name, param: function.arguments)
            }
        }
    }
    func callFunction(name: String, param: Any?) {
        if name == "get_items" { 
            // ECサイトから商品を取得するAPIを実行しています。
            APIConnection.shared.getItems { data in
                if let data = data {
                    do {
                        let json = try JSONDecoder().decode(ItemsResponse.self, from: data)
                        self.getItemSucceed(response: json)
                    }
                    catch let e {
                        print(e)
                        print(String(data: data, encoding: .utf8))
                    }
                }
            }
        }
    }
    func getItemSucceed(response: ItemsResponse) {
        let encoder = JSONEncoder()
        let content = try! String(data: encoder.encode(response), encoding: .utf8)
        // contentには戻り値JSONを文字列化した内容をそのまま指定します。
        let functionMessage = Message(role: "function", content: content ?? "", function_call: nil, name: "get_items")
        messages.append(functionMessage)
        let messages = self.messages
        APIConnection.shared.postApi(param: messages) { data in
            if let data = data {
                do {
                    let json = try JSONDecoder().decode(ChatResponse.self, from: data)
                    self.chatSucceed(response: json)
                }
                catch let e {
                    print(e)
                    print(String(data: data, encoding: .utf8))
                }
            }
        }
    }
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell
        let message = messages[indexPath.row + 1]
        cell = table.dequeueReusableCell(withIdentifier: message.role)!
        let label = cell.viewWithTag(1) as! UILabel
        if message.role == "function" {
            // 関数が実行された場合のUI表示
            label.text = "function_call: " + (message.name ?? "")
        }
        else {
            if message.role == "assistant", let function = message.function_call {
                // 関数の実行をリクエストされた場合のUI表示
                label.text = "関数をコールしてください:\(function.name) args:\(function.arguments ?? "null")"
            }
            else {
                label.text = message.content
            }
        }
        return cell
    }
}

実行結果

これでAPI実行をリクエストさせ、ECサイトの商品を読み込ませることができました。

今回はoptionalにしていますが、Function Calling機能では引数を生成可能です。ChatGPTから安定してJSONでパラメータを取得できるのはかなり強力な機能なので上手く使いこなしたいところです。例えばユーザの自由な入力から商品検索を行うような機能も簡単に(プロンプトの試行錯誤を抑えて)実装できるでしょう。良かったですね。

参考文献

API Reference – OpenAI API