Function Callingを使って盛岡の天気を教えてくれるAIを作る


ChatGPTは最近の話題は教えてくれないので、API連携して外部APIを叩くと最新話題も取ってこれます。

> ![スクリーンショット 2023-09-01 19.23.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/e83b6351-53ea-bdd5-b2d8-6925a5afbb14.png "スクリーンショット 2023-09-01 19.23.16.png")

ただし、通常言葉の揺らぎや別の言い回しになると認識してくれず、機械に人間が合わせる感じになりますね。

> 通常if文などで判定するのこんな感じになる
> ![スクリーンショット 2023-09-01 19.24.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35387/cf9e73c2-d218-bd57-2c0b-24b82229a50a.png "スクリーンショット 2023-09-01 19.24.16.png")

## Function Callingで文脈の特定

まずは前回、Function Callingのお試しをしてみました。

https://qiita.com/n0bisuke/items/b31fafe2239abf01b13a 

## 文脈を特定して実際に処理を実行

前回のコードの続きみたいな処理です。前回は`getMoriokaWeather関数`を作っていて盛岡限定でしたが今回は`getWeather関数`として都市名を入れ込めるようにしてみます。

```js
const OpenAI = require('openai');
const openai = new OpenAI({
    apiKey: 'APIキー',
});

const prompt = {
    role: 'user',
    content: process.env.USERTEXT, //ターミナルから実行時に命令を入力して受ける
};

//天気を調べる関数
const getWeather = async (city) => {
    console.log(city);
    let cityId = '';
    //都市名とIDの対応はここを参照 https://weather.tsukumijima.net/primary_area.xml 
    if(city === '盛岡') cityId = '030010';     //ここを連携する処理を書けば自動判定できそう

    const url = `https://weather.tsukumijima.net/api/forecast/city/${cityId}`;
    try {
        const response = await fetch(url); 
        const data = await response.json();
        // console.log(data.description.bodyText);
        return data;
    } catch (error) {
        console.error("There was a problem with the fetch operation:", error.message);
    }
}

const functions = {
    getWeather,
};

const main = async () => {

    const gptOptions = {
        model: "gpt-4-0613",
        messages: [prompt],
        function_call: "auto",
        functions: [
            {
                name: "getWeather",
                description: "天気を取得",
                parameters: {
                    type: "object",
                    properties: {
                        city: {
                            type: "string",
                            description: "天気を調べる地名, e.g. 盛岡, 花巻, 平泉"
                        },
                    }
                },
            },
        ],
    }
    
    const completion = await openai.chat.completions.create(gptOptions);
    
    const message = completion.choices[0].message;
    console.log(completion.choices[0]);
    let responseMessage = '';

    //Function Callなのかそうではないのか
    if(completion.choices[0].finish_reason === "function_call"){
        console.log("function_callします...");
        
        const functionCall = message?.function_call;
        const args = JSON.parse(functionCall.arguments || "{}"); //引数の取得
        // console.log(args)
        const functionRes = await functions[functionCall.name](args?.city); //関数の実行
        // console.log(functionRes); //実行結果
        responseMessage = functionRes.description.bodyText

    }else{
        //Function Callではない場合
        responseMessage = message?.content;
        console.log(`通常GPT返信`);
    }

    console.log(responseMessage);
}

main();

```


## 実行: 盛岡の天気を教えて

**盛岡の天気を教えて** と言ってみます。

```bash
$ USERTEXT=盛岡の天気を教えて node fc.js
```

`console.log(completion.choices[0]);`の箇所は以下のようなJSONになっています。
arguments（引数）にプロパティで設定したcityと入力した

```json
{
  index: 0,
  message: {
    role: 'assistant',
    content: null,
    function_call: { name: 'getWeather', arguments: '{\n  "city": "盛岡"\n}' }
  },
  finish_reason: 'function_call'
}
```

結果、以下のようなAPIからの返信を取得できました。

```
高気圧が日本のはるか東にあって、本州付近に張り出しています。一方、低気圧がオホーツク海にあって北東へ進んでおり、前線が北海道付近を通って日本海へのびています。

岩手県は、晴れや曇りで、雨の降っている所があります。

１日夜は、高気圧に覆われるため、晴れや曇りでしょう。

２日は、前線の影響により、曇りや雨で、雷を伴い激しく降る所がある見込みです。
```

## よもやま

仙台の天気を教えてなどでもちゃんとcityの引数に"仙台"という引数が入ります。このAPIが地名から検索できる仕様だったらこのまま使えてそうでした。

https://weather.tsukumijima.net/primary_area.xml

こちらから取得してIDとの紐付けが必要なのでこの辺別の処理を書けば完璧っぽかったですね。