📝

#28 function callingを試してみた

2024/08/14に公開

概要

2023年6月13日にOpenAIのGPTがアップデートされ、「function calling」という機能が追加されました。これによって外部のAPIなどを呼び出せるようになり高機能なチャットボットの作成が可能になります。

今回はローカルに保存されている情報とGPTを連携してみたいと思います

大まかな流れ

  1. 呼び出したい関数群を実装し、公式ドキュメントを参考にJSON形式で関数の情報を記述します
  2. 送信したいメッセージとともに先ほど作成した関数の情報をGPTに渡します
  3. GPTが関数を呼び出す必要があると判断した場合、関数名と引数を返してくれます。関数を呼び出す必要がないと判断した場合はいつも通りメッセージを返します
  4. 関数が呼び出される必要がある場合、GPTから返ってきた情報を基に関数を実行して戻り値をGPTに返します
  5. 3へ戻ります

関数の実装と定義

関数の実装

プロフィールを操作するProfileクラスと、GPTからの関数呼び出しを解釈して実行するexecuteFunction(functionName: string, args: any)を作成します

日付に関しては人間が解釈できるような日付形式であればGPTも解釈できるので特に制約は設けずに文字列型としています

class Profile {
    private profiles: {
        [key: string]: {
            birthday: string
        }
    };

    private filePath: string;

    constructor(path: string) {
        this.filePath = path;
        this.profiles = {};

        if (fs.existsSync(this.filePath)) {
            try {
                this.profiles = JSON.parse(fs.readFileSync(this.filePath).toString());
            } catch (error) {
                throw error;
            }
        }
    }

    add(name: string, birthday: string) {
        //もしすでに同名のプロフィールが存在した場合は空文字を返す
        if (this.profiles[name]) {
            return "";
        }

        this.profiles[name] = {
            birthday: birthday
        };

        try {
            fs.writeFileSync(this.filePath, JSON.stringify(this.profiles));
        } catch (error) {
            throw error;
        }

        return this.profiles[name];
    }

    get(name: string) {
        return this.profiles[name] ? this.profiles[name] : '';
    }
}

const profiles = new Profile('./profiles.json');

function executeFunction(functionName: string, args: any) {
    try {
        switch (functionName) {
            case 'addProfile':
                return JSON.stringify(profiles.add(args.name, args.birthday));
            case 'getProfile':
                return JSON.stringify(profiles.get(args.name));
            default:
                throw new Error(`${functionName} undefined`);
        }
    } catch (error) {
        throw error;
    }
}

関数の定義

先ほど実装した関数をGPTに教えてあげるために、公式ドキュメントを参考にJSON形式で関数の役割や引数などを記述します

  • プロフィールを登録するaddProfile(name: string, birthday: string)
  • プロフィールを取得するgetProfile(name: string)

について以下の通り記述します

import { CompletionCreateParams } from "openai/resources/chat/completions";

const functions: CompletionCreateParams.Function[] = [
    {
        name: "getProfile",
        description: "プロフィールを取得します。存在しない場合は空文字を返します",
        parameters: {
            type: "object",
            properties: {
                name: {
                    type: "string",
                    description: "プロフィールを取得したい人の名前",
                },
            },
            required: [
                "name"
            ],
        },
    },
    {
        name: "addProfile",
        description: "プロフィールを追加します。すでに同名のプロフィールが存在した場合は空文字を返します",
        parameters: {
            type: "object",
            properties: {
                name: {
                    type: "string",
                    description: "プロフィールを登録したい人の名前",
                },
                birthday: {
                    type: "string",
                    description: "誕生日",
                },
            },
            required: [
                "name",
                "birthday"
            ],
        },
    }
];

GPTと会話をする

GPTと会話をするための関数を実装する

GPTと会話するために以下のようにrunConversation()を実装します(公式ドキュメントを参考にしました)

この関数はfunction callingの連続呼び出しに対応するためasync/awaitで同期をとり再帰的に呼び出せるようにしています

import { CreateChatCompletionRequestMessage } from "openai/resources/chat/completions";

type Message = CreateChatCompletionRequestMessage;

async function runConversation(messages: Message[]): Promise<Message[]> {
    return await openai.chat.completions.create(
        {
            model: 'gpt-3.5-turbo',
            messages: messages,
            functions: functions
        }
    ).then(
        async value => {
            try {
                const message = value.choices[0].message;
                messages.push(message);

                //もし関数呼び出しであれば関数を呼び出して結果をGPTに渡す
                if (message.function_call) {
                    const functionName = message.function_call.name;
                    const arg = JSON.parse(message.function_call.arguments);

                    //関数の実行・結果の格納
                    const functionResponce: Message = {
                        role: 'function',
                        name: functionName,
                        content: executeFunction(functionName, arg)
                    }
                    messages.push(functionResponce);

                    //関数の実行結果をGPTに渡す
                    return await runConversation(messages);
                }

                return messages;
            } catch (error) {
                throw error;
            }
        }
    ).catch(
        reason => {
            throw new Error(reason);
        }
    );
}

UIを作成する

UIに関しては以下のように実装しました

以下の処理をループします

  • readlineでユーザの入力を受け取ってメッセージ配列に追加してrunConversation()に渡します
  • runConversation()から返ってきた配列をメッセージ配列に追加します。
  • GPTからのメッセージ(メッセージの配列の最後の項目)をコンソールに表示します
async function main() {
    const messages: Message[] = new Array();
    while (true) {
        process.stdout.write(`user> `);
        const readLine = (readline.createInterface(process.stdin))[Symbol.asyncIterator]();

        const message: Message = {
            role: "user",
            content: (await readLine.next()).value
        };
        messages.push(message);

        const responce = await runConversation(messages);
        const content = responce[responce.length - 1].content;
        const role = responce[responce.length - 1].role;

        if (content && role) {
            messages.concat(responce);
            
            process.stdout.write(`${role}> `);
            process.stdout.write(`${content}\n`);
        }
    }
};

実際に会話してみる

プロフィールを生成・登録する

まずはGPTに名前と誕生日のペアを3つ生成してもらってプロフィールに登録してもらいます

起動するにはコマンド$ npx ts-node ./src/main.tsを入力します

起動するとuser>と表示されるので送信したいメッセージを送信します。今回は3人分のプロフィールを生成して登録してと入力します

しばらく待つと以下のように返事が返ってきてjsonファイルにプロフィールが記録されます

user> 3人分のプロフィールを生成して登録して
assistant> 3人分のプロフィールの登録が完了しました。
{
    "Alice": {
        "birthday": "1990-05-12"
    },
    "Bob": {
        "birthday": "1985-09-28"
    },
    "Charlie": {
        "birthday": "1995-03-21"
    }
}

Ctrl + Cで会話を終了します

プロフィールを参照させてみる

先ほどと同じように起動し、誕生日を聞いてみると以下のように先ほど記録された誕生日が返ってきます

また、存在しないプロフィールを参照しようとした場合は存在しない旨を返してくれますし、簡単な計算もこなしてくれます

関数の呼び出しが必要ない場面では適切な答えを返してくれます

user> Aliceの誕生日を教えて
assistant> Aliceの誕生日は1990年5月12日です。
user> Daveの誕生日を教えて
assistant> 申し訳ありませんが、Daveのプロフィールが存在しませんので、誕生日を教えることはできません。
user> BobとCharlieの年齢差を教えて
assistant> BobとCharlieの年齢差は10歳です。
user> おふとんはふかふかですか?
assistant> 私はAIですので、ふかふかした感触を感じることはできません。ただし、通常、ふとんは柔らかくてふかふかした感触がありますので、快適な寝心地を提供してくれます。

まとめ

OpenAIのfunction callingという機能を使ってGPTからデータの登録・参照をしてみました

今回の内容を応用すると例えば外部のカレンダーのAPIと連携すれば予定を登録できるようになりますし、検索エンジンのAPIと連携できれば最新の情報を入手することができるようになります

この記事が参考になれば幸いです。最後までご覧いただきありがとうございました

Discussion