Function callingを使ってタスク管理を試してみた

## はじめに

先日、OpenAIからアナウンスがあったChatGPTの[Function calling](https://openai.com/blog/function-calling-and-other-api-updates)を調べてみてこれは面白いことができるんじゃないかと思い、今回タスク管理アプリ[TickTick](https://ticktick.com/webapp/#p/inbox/tasks)を自然言語から操作できるようにしました！

簡易的なソースコードは[こちら](https://github.com/kp047i/function-calling-task-management/tree/main)

## Function callingとは

ChatGPTに対して事前に関数の内容や、関数に必要な引数の情報を定義しておくことで、自然言語の命令から、**どの関数を使ったらいいのか判断し、その関数に渡す引数をJSON形式で生成**してくれます。

![Function callingの動作イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/384849/8eb56706-ecdb-b106-2b75-264efb15589e.jpeg)

これができると何が嬉しいかというと、曖昧な自然言語から必要なデータを生成してくれるので、ITに慣れていない人にとっても操作しやすいアプリケーションが開発できます。特に、高齢者の方の見守りをするアプリケーションなどには相性がいいのではないかと思います。

そのほかにも、仕事のやり取りをしているチャットアプリ上のテキストからタスクを作成したり、リマインダーを生成することもできるんじゃないかと考えています。

## 実際に動かしてみた

### Function callingの設定

今回は`gpt-3.5-turbo-0613`で試してみました。

ChatGPTに渡すmessagesオブジェクトは以下のようにしました。ここのプロンプトを工夫するだけでもかなり精度が上がると思います。タイムゾーンや現在時刻を渡してあげるとタスクの時間の設定もかなり精度が上がりました。

```js
const messages = [
    {
      role: "assistant",
      content: `
      あなたは私のタスク管理を手伝ってくれるアシスタントです。ticktickのAPIを使ってタスクの管理を行います。
        以下は命令文です。
        ---
        ${prompt}
        ---
      `,
    },
    {
      role: "user",
      content: `
        以下は制約です。タスクの更新をするときは現状のタスクの情報を利用してtaskIdを指定してください。
        ---
        現在時刻: ${new Date().toLocaleString()}
        タイムゾーン: Asia/Tokyo
        現状のタスク: ${JSON.stringify(tasks)}
        ---
        `,
    },
  ];
```

[TickTickのAPIのスキーマ](https://developer.ticktick.com/docs#/openapi)から以下のような関数を用意しました。

```js
const functions = [
  {
    name: "create_task",
    description: "API経由でticktcikにタスクを生成する関数",
    parameters: {
      type: "object",
      properties: {
        title: {
          type: "string",
          description: "タスクのタイトル",
        },
        projectId: {
          type: "string",
          description: "タスクを追加するプロジェクトのID",
        },
        content: {
          type: "string",
          desctription: "タスクの詳細",
        },
        desc: {
          type: "string",
          description: "タスクの詳細",
        },
        isAllDay: {
          type: "boolean",
          description: "タスクが終日かどうか",
        },
        startDate: {
          type: "string",
          description: `タスクの開始時刻。フォーマットの例 : "2019-11-13T03:00:00+0000"`,
          format: "yyyy-MM-dd'T'HH:mm:ssZ",
        },
        dueDate: {
          type: "string",
          description: `タスクの終了時刻。フォーマットの例: "2019-11-13T03:00:00+0000"`,
          format: "yyyy-MM-dd'T'HH:mm:ssZ",
        },
        timeZone: {
          type: "string",
          description: "タスクのタイムゾーン",
        },
        priority: {
          type: "number",
          description:
            "タスクの優先度。デフォルトの値が0で、値が大きいほど優先度が高くなる。最大は3",
        },
      },
      required: ["title"],
    },
  },
  {
    name: "get_project_tasks",
    description: "API経由でticktcickからタスクを取得する関数",
    parameters: {
      type: "object",
      properties: {
        projectId: {
          type: "string",
        },
      },
    },
  },
  {
    name: "complete_project_tasks",
    description: "API経由でticktcickのタスクを完了する関数",
    parameters: {
      type: "object",
      properties: {
        projectId: {
          type: "string",
          description: "タスクを完了するプロジェクトのID。現状は固定値",
        },
        taskId: {
          type: "string",
          description: "タスクのID",
        },
      },
    },
  },
];
```

### 動かしてみた様子

入力した命令は以下です。

`明日中にqiitaの記事を書くタスクを作成して`

それに対するJSONの出力が以下です。今日が2023年6月19日なので明日中という命令も認識してくれています。

```json
{
	"index": 0,
	"message": {
		"role": "assistant",
		"content": null,
		"function_call": {
			"name": "create_task",
			"arguments": "{\n\"projectId\": \"実際のprojectId\",\n\"title\": \"qiitaの記事を書く\",\n\"dueDate\": \"2023-06-20T23:59:59+0900\"\n}"
		}
	},
	"finish_reason": "function_call"
}
```

次にちょっと複雑な命令を実行してみます。

`来週の日曜日までにPCを買うタスクを優先度高く作成して`

それに対する出力がこちらです。優先度の設定やタスクの終了日の設定がうまくいっています。
```json
{
	"index": 0,
	"message": {
		"role": "assistant",
		"content": null,
		"function_call": {
			"name": "create_task",
			"arguments": "{\n  \"title\": \"PCを買うタスク\",\n  \"priority\": 2,\n  \"dueDate\": \"2023-06-26T23:59:59+0900\"\n}"
		}
	},
	"finish_reason": "function_call"
}
```

次に同じプログラムで、タスクを完了させる命令を出してみます。
タスクを完了させるようなある全体のリソースを取得して、その中から命令に合うようなリソースを操作する命令は一つの関数では難しそうでした。（他のやり方があるかもしれない）
なので、私はプロンプトを生成するときに、現状のタスク一覧の情報を渡してあげることで、タスクのIDをChatGPTに覚えさせています。その結果実際にタスクを完了させるAPIを実行できました。


:::note warn
現状のタスクの情報をプロンプトに含めるとトークンの量が増えるので、気をつけてください。
:::

`qiitaの記事を書くタスクを完了にして`


```json{
	"index": 0,
	"message": {
		"role": "assistant",
		"content": null,
		"function_call": {
			"name": "complete_project_tasks",
			"arguments": "{\n  \"projectId\": \"実際のprojectId\",\n  \"実際のtaskId\": \"taskId\"\n}"
		}
	},
	"finish_reason": "function_call"
}
```

## まとめ

- Function callingを使うことで、自然言語から実際にタスク管理のアプリケーションを操作することができました。
- プロンプトによってまだまだ精度を高めたり、人間との会話を続けることで複雑なタスク操作を行うことも可能だと感じました
- ここもう少し工夫できそうといった点もあったらぜひ教えてください
- Function calling面白い！