＃28　function callingを試してみた

# 概要
[2023年6月13日にOpenAIのGPTがアップデート](https://openai.com/blog/function-calling-and-other-api-updates)され、「function calling」という機能が追加されました。これによって外部のAPIなどを呼び出せるようになり高機能なチャットボットの作成が可能になります。

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

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

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

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

```typescript
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に教えてあげるために、[公式ドキュメント](https://platform.openai.com/docs/guides/gpt/function-calling)を参考にJSON形式で関数の役割や引数などを記述します

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

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

```typescript
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()を実装します([公式ドキュメントを参考にしました](https://platform.openai.com/docs/guides/gpt/function-calling))

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

```typescript
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からのメッセージ(メッセージの配列の最後の項目)をコンソールに表示します

```typescript
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人分のプロフィールの登録が完了しました。
```
```json
{
    "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と連携できれば最新の情報を入手することができるようになります

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