OpenAIのFunction callingとBacklog APIを使って、自然言語で課題を作成,更新する

# はじめに

OpenAPIのFunction Callingとタスク管理ツール [Backlog のAPI](https://developer.nulab.com/ja/docs/backlog/)を連携し、「優先度 高 で課題を登録して」などの自然言語で課題の追加や課題情報の更新が行えるか検証してみました。

Function Callingと外部APIとの連携では、天気情報やHotpepperからの情報取得の例があります。これらの例では、「東京の天気」や「博多のおすすめのレストラン」などの自然言語からLLMが利用可能な関数やAPIに渡すパラメータを判断し、それをもとに関数を実行しレスポンスを得たり再度LLMに問い合わせて回答を組み立てています。同様に、BacklogのAPIと連携して自然言語で課題の登録や更新が行えるか検証してみました。

:::note info
2023年11月6日にOpenAI Python Library v1がリリースされました｡これまでとは大きく仕様が異なっており､新しいライブラリに移行するための[v1.0.0 Migration Guide](https://github.com/openai/openai-python/discussions/742)をもとにコードを移行する必要があります｡本記事のコードをv1系に移行したものに更新しました｡また､v1系以前のコードとの差分も載せているので､移行の参考になれば幸いです｡
:::

# 参考情報

https://gihyo.jp/book/2023/978-4-297-13839-4

https://qiita.com/hedgehog051/items/e9724af2786c5e262e62

https://qiita.com/kp047i/items/43f2d99a8b7fa4770342

https://note.com/npaka/n/n917463f55b8a

https://note.com/it_navi/n/ncc9f000967f2

https://dev.classmethod.jp/articles/backlog-api-many-issues/

https://developer.nulab.com/ja/docs/backlog/

# 前提条件

- OpenAIのAPIキーを環境変数`OPENAI_API_KEY`に設定済み
- BacklogのAPIキーや後述のコード内で使用するissueTypeId(課題種別ID)などの準備ができている(Backlog APIの利用に必要なAPIキーやパラメータの確認方法は､以下の記事が参考になります｡)

https://dev.classmethod.jp/articles/backlog-api-many-issues/


# 環境構築

## Python環境

```console
$ python3 --version
Python 3.11.4
```

```console
$ python3 -m venv .venv
$ . .venv/bin/activate
```

```console
$ pip3 install --upgrade pip
$ pip3 --version
pip 23.3.1 from /home/xxx/.venv/lib/python3.11/site-packages/pip (python 3.11)
```

```console
$ pip install openai 
```

## コードの実装

### 課題の登録

`課題の追加`と`課題情報の更新`のうち､課題の追加を行ってみます｡Backlog APIの[課題の追加](https://developer.nulab.com/ja/docs/backlog/api/2/add-issue/)のリクエストパラメーターからcreate_task関数を定義しました｡LLMが自然言語をもとに使用するパラメータと値を判断できるよう､descriptionに日本語に対応する値を記述しました｡issueTypeIdはBacklogのスペースによって異なります｡前述の前提条件のとおり､確認が必要です｡

#### create_task関数

```python
functions = [
    {
        "name": "create_task",
        "description": "Create task using a task management system called Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題種別が'タスク'の場合､  IssueTypeId は 624224
                    課題種別が'バグ'の場合､ IssueTypeId は 624223
                    課題種別が'要望'の場合､ IssueTypeId は 624225
                    課題種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
            "required": [
                "summary",
                "projectId",
                "issueTypeId",
                "priorityId",
            ],
        },
    },
]
```

#### プロンプト

プロンプトは以下のとおりです｡プロジェクトIDは課題を登録するプロジェクトのID(数値)を指定します｡日付は､西暦を省略すると過去の日付と誤認識される場合がありました｡[Function callingを使ってタスク管理を試してみた](https://qiita.com/kp047i/items/43f2d99a8b7fa4770342#:~:text=%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%82%84%E7%8F%BE%E5%9C%A8%E6%99%82%E5%88%BB%E3%82%92%E6%B8%A1%E3%81%97%E3%81%A6%E3%81%82%E3%81%92%E3%82%8B%E3%81%A8%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AE%E6%99%82%E9%96%93%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%82%E3%81%8B%E3%81%AA%E3%82%8A%E7%B2%BE%E5%BA%A6%E3%81%8C%E4%B8%8A%E3%81%8C%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82) の記事にあるように､プロンプトに現在日時を加えると良いようです｡

```python
messages = [
    {
        "role": "system",
        "content": "あなたはタスクマネージャーである。あなたには関数のリストと、関数で使用するパラメータが与えられています。また、作成すべきタスクのリストも与えられています。与えられた関数とパラメータを使ってタスクを作成する必要があります。作成したタスクのリストを返してください。",
    },
    {
        "role": "user",
        "content": """
            Backlogに新規課題を登録してください｡
            プロジェクトIDはxxxxxxです｡
            課題のタイトルは｢アプリケーションに新規機能を追加する｣です｡
            課題の本文には、アプリケーション開発に必要な検討項目を箇条書きで記述してください｡それぞれの項目には､上位の検討項目を細分化した小項目を記述してください｡
            課題種別は要望です｡
            優先度は高です｡
            開始日は2023年11月6日､期限日は11月23日です｡
            """,
    },
]
```

#### LLMからのレスポンス

上記のプロンプトをもとに､LLMから以下のレスポンスが得られました｡LLMによってcreate_task関数が選択され､プロンプトに記述した内容がそれぞれパラメータに当てはめられているのが分かります｡`アプリケーション開発に必要な検討項目を箇条書きで記述してください｡`と指示した内容も反映されています｡

```json
{
	"id": "chatcmpl-8GuN0TEEaK6wPUZdqKkqWBtY4rClR",
	"object": "chat.completion",
	"created": 1699040626,
	"model": "gpt-3.5-turbo-0613",
	"choices": [
		{
			"index": 0,
			"message": {
				"role": "assistant",
				"content": null,
				"function_call": {
					"name": "create_task",
					"arguments": "{\n  \"summary\": \"アプリケーションに新規機能を追加する\",\n  \"projectId\": xxxxxx,\n  \"description\": \"アプリケーション開発に必要な検討項目:\\n- UIデザインの検討\\n  - カラーパレットの選定\\n  - レイアウトの構築\\n\\n- 機能の実装\\n  - データベーステーブルの設計\\n  - APIの作成\\n  - ユーザー認証の実装\\n\\n- テストとデバッグ\\n  - ユニットテストの作成\\n  - エラーハンドリングの実装\\n\\n- ドキュメントの作成\\n  - ユーザーマニュアルの作成\\n  - 開発ドキュメントの作成\",\n  \"issueTypeId\": \"624225\",\n  \"startDate\": \"2023-11-06\",\n  \"dueDate\": \"2023-11-23\",\n  \"priorityId\": \"2\"\n}"
				}
			},
			"finish_reason": "function_call"
		}
	],
	"usage": {
		"prompt_tokens": 729,
		"completion_tokens": 290,
		"total_tokens": 1019
	}
}
```

descriptionの内容は以下のとおりです｡この部分は実行のたびに内容が大きく変わるので､一定の回答を得たい場合は､プロンプトに期待する回答のフォーマットを与えるなど工夫が必要です｡
```
アプリケーション開発に必要な検討項目:
- UIデザインの検討
  - カラーパレットの選定
  - レイアウトの構築

- 機能の実装
  - データベーステーブルの設計
  - APIの作成
  - ユーザー認証の実装

- テストとデバッグ
  - ユニットテストの作成
  - エラーハンドリングの実装

- ドキュメントの作成
  - ユーザーマニュアルの作成
  - 開発ドキュメントの作成
```

#### Backlog APIからのレスポンス

create_taskを実行しBacklog API ( /api/v2/issues )にPOSTすると､Backlog APIから以下のレスポンスが得られました｡issueType, summarydescription, priority, startDate, dueDateの値で登録内容を確認できます｡

```json
{
	"id": 37638919,
	"projectId": xxxxxx,
	"issueKey": "APP-49",
	"keyId": 49,
	"issueType": {
		"id": 624225,
		"projectId": xxxxxx,
		"name": "要望",
		"color": "#ff9200",
		"displayOrder": 2
	},
	"summary": "アプリケーションに新規機能を追加する",
	"description": "アプリケーション開発に必要な検討項目:\n- UIデザインの検討\n  - カラーパレットの選定\n  - レイアウトの構築\n\n- 機能の実装\n  - データベーステーブルの設計\n  - APIの作成\n  - ユーザー認証の実装\n\n- テストとデバッグ\n  - ユニットテストの作成\n  - エラーハンドリングの実装\n\n- ドキュメントの作成\n  - ユーザーマニュアルの作成\n  - 開発ドキュメントの作成",
	"resolution": null,
	"priority": {
		"id": 2,
		"name": "高"
	},
	"status": {
		"id": 1,
		"projectId": xxxxxx,
		"name": "未対応",
		"color": "#ed8077",
		"displayOrder": 1000
	},
	"assignee": null,
	"category": [],
	"versions": [],
	"milestone": [],
	"startDate": "2023-11-06T00:00:00Z",
	"dueDate": "2023-11-23T00:00:00Z",
	"estimatedHours": null,
	"actualHours": null,
	"parentIssueId": null,
	"createdUser": {
		"id": xxxxx,
		"userId": "xxxxxxxx",
		"name": "Rev",
		"roleType": 1,
		"lang": "ja",
		"mailAddress": "foobar@example.com",
		"nulabAccount": {
			"nulabId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
			"name": "Rev",
			"uniqueId": "xxxxxx"
		},
		"keyword": "Rev",
		"lastLoginTime": "2023-11-02T16:53:27Z"
	},
	"created": "2023-11-03T19:43:52Z",
	"updatedUser": {
		"id": 58407,
		"userId": "xxxxxx",
		"name": "Rev",
		"roleType": 1,
		"lang": "ja",
		"mailAddress": "foobar@example.com",
		"nulabAccount": {
			"nulabId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
			"name": "Rev",
			"uniqueId": "xxxxxx"
		},
		"keyword": "Rev",
		"lastLoginTime": "2023-11-02T16:53:27Z"
	},
	"updated": "2023-11-03T19:43:52Z",
	"customFields": [],
	"attachments": [],
	"sharedFiles": [],
	"stars": [],
	"externalFileLinks": []
}
```

#### LLMからのレスポンス

プロンプト､LLMからのレスポンスそしてBacklog APIからのレスポンスをつなげてLLMに渡します｡その結果､以下のレスポンスを得ました｡チャットアプリなどに課題を追加するプロンプトを投稿し､その結果をこのような形式で得るといったことが可能です｡

```console
新しいタスクが正常に作成されました。タスクの詳細は以下の通りです：

- タイトル: アプリケーションに新規機能を追加する
- 説明: アプリケーション開発に必要な検討項目:
  - UIデザインの検討
    - カラーパレットの選定
    - レイアウトの構築
  - 機能の実装
    - データベーステーブルの設計
    - APIの作成
    - ユーザー認証の実装
  - テストとデバッグ
    - ユニットテストの作成
    - エラーハンドリングの実装
  - ドキュメントの作成
    - ユーザーマニュアルの作成
    - 開発ドキュメントの作成
- 課題ID: APP-49
- 課題種別: 要望
- 優先度: 高
- 開始日: 2023年11月6日
- 期限日: 2023年11月23日

これにより、Backlogに新しい課題が登録されました。
```

#### Backlog画面

実際のBacklogの画面はこのようになりました｡課題種別､課題の件名､優先度､開始日､期限日のそれぞれが自然言語で指示したとおりのものとなりました｡課題の詳細では､リストを表現するMarkdown記法が正しくないためすべての項目が同じ階層となっています｡この点は､プロンプトでMarkdown記法に従うように指示することで解決できそうです｡
![スクリーンショット 2023-11-04 135645.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/a2d46104-3dfe-2339-4831-41af2549cfa1.png)


#### コード全体
コード全体は以下のとおりです｡
OpenAI Python Library v1に対応しています｡


```python
import json
import logging
import os
import sys

import requests
from openai import OpenAI

# logging.disable()
logging.basicConfig(stream=sys.stdout, level=logging.INFO, force=True)

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

BACKLOG_SPACE_KEY = "xxxx"
BACKLOG_BASE_URL = f"https://{BACKLOG_SPACE_KEY}.backlog.com"
BACKLOG_API_KEY = os.environ.get("BACKLOG_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")


def create_task(arguments):
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {
        "projectId": arguments.get("projectId"),
        "summary": arguments.get("summary"),
        "description": arguments.get("description"),
        "issueTypeId": arguments.get("issueTypeId"),
        "startDate": arguments.get("startDate"),
        "dueDate": arguments.get("dueDate"),
        "priorityId": arguments.get("priorityId"),
    }

    response_body = requests.post(
        f"{BACKLOG_BASE_URL}/api/v2/issues?apiKey={BACKLOG_API_KEY}",
        data=data,
        headers=headers,
        timeout=10,
    )

    return json.dumps(response_body.json())


functions = [
    {
        "name": "create_task",
        "description": "Create a task in a task management system named Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題種別が'タスク'の場合､  IssueTypeId は 624224
                    課題種別が'バグ'の場合､ IssueTypeId は 624223
                    課題種別が'要望'の場合､ IssueTypeId は 624225
                    課題種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
            "required": [
                "summary",
                "projectId",
                "issueTypeId",
                "priorityId",
            ],
        },
    },
]

messages = [
    {
        "role": "system",
        "content": "あなたはタスクマネージャーである。あなたには関数のリストと、関数で使用するパラメータが与えられています。また、作成すべきタスクのリストも与えられています。与えられた関数とパラメータを使ってタスクを作成する必要があります。作成したタスクのリストを返してください。",
    },
    {
        "role": "user",
        "content": """
            Backlogに新規課題を登録してください｡
            プロジェクトIDはxxxxxxです｡
            課題のタイトルは｢アプリケーションに新規機能を追加する｣です｡
            課題の本文には、アプリケーション開発に必要な検討項目を箇条書きで記述してください｡それぞれの項目には､上位の検討項目を細分化した小項目を記述してください｡
            課題種別は要望です｡
            優先度は高です｡
            開始日は2023年11月6日､期限日は11月23日です｡
            """,
    },
]

# 使用する関数と､与える引数
response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=messages,
    function_call="auto",
    functions=functions,
)
logging.info("response %s", response)

llm_response = response.choices[0].message


if llm_response.function_call:
    # 選択した関数を実行
    available_functions = {
        "create_task": create_task,
    }

    function_name = llm_response.function_call.name
    function_to_call = available_functions[function_name]
    function_args = json.loads(llm_response.function_call.arguments)

    # LLMが選択した引数を関数に渡す
    function_response = function_to_call(function_args)
    logging.info("function_response %s", function_response)

    # LLMのレスポンスと関数の実行結果をmessagesに追加
    messages.append(llm_response)
    messages.append(
        {
            "role": "function",
            "name": function_name,
            "content": function_response,
        }
    )

    logging.info("messages %s", messages)

    # messagesを再度LLMに渡す
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
    )

    message = response.choices[0].message
    print(message.content)
else:
    print("No further action.")
```

#### v1以前のコードとの差分

```diff
6d5
< import openai
7a7
> from openai import OpenAI
11a12
> client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
124c125
< response = openai.ChatCompletion.create(
---
> response = client.chat.completions.create(
132c133
< llm_response = response["choices"][0]["message"]
---
> llm_response = response.choices[0].message
135c136
< if llm_response.get("function_call"):
---
> if llm_response.function_call:
141c142
<     function_name = llm_response["function_call"]["name"]
---
>     function_name = llm_response.function_call.name
143c144
<     function_args = json.loads(llm_response["function_call"]["arguments"])
---
>     function_args = json.loads(llm_response.function_call.arguments)
162c163
<     response = openai.ChatCompletion.create(
---
>     response = client.chat.completions.create(
167,169c168,169
<     message = response["choices"][0]["message"]
<     print(f"{message['content']}")
<
---
>     message = response.choices[0].message
>     print(message.content)
```

[v1.0.0 Migration Guide](https://github.com/openai/openai-python/discussions/742)にあるように`openai migrate`コマンドを実行すると､ある程度自動的にコードを移行してくれます｡

```console
pip install --upgrade openai
openai migrate
```

`openai migrate`を実行した際のログは以下のとおりです｡

```diff
     import os
     import sys

    -import openai
    +from openai import OpenAI
    +
    +client = OpenAI()
     import requests

     # logging.disable()
     ]

     # 使用する関数と､与える引数
    -response = openai.ChatCompletion.create(
    -    model="gpt-3.5-turbo",
    -    messages=messages,
    -    function_call="auto",
    -    functions=functions,
    -)
    +response = client.chat.completions.create(model="gpt-3.5-turbo",
    +messages=messages,
    +function_call="auto",
    +functions=functions)
     logging.info("response %s", response)

     llm_response = response["choices"][0]["message"]
         logging.info("messages %s", messages)

         # messagesを再度LLMに渡す
    -    response = openai.ChatCompletion.create(
    -        model="gpt-3.5-turbo",
    -        messages=messages,
    -    )
    +    response = client.chat.completions.create(model="gpt-3.5-turbo",
    +    messages=messages)

         message = response["choices"][0]["message"]
         print(f"{message['content']}")
```





### 課題情報の更新

つづいて､課題の追加を行ってみます｡Backlog APIの[課題情報の更新](https://developer.nulab.com/ja/docs/backlog/api/2/update-issue/)のリクエストパラメーターからupdate_task関数を追加しました｡課題の追加とほぼ同じですが､更新対象を特定するためにissueKeyパラメータを定義しています｡プロンプトの内容をもとにLLMが適切な関数を選択できるかを検証します｡

#### update_task関数を追加

```python
functions = [
    {
        "name": "create_task",
        "description": "Create task using a task management system called Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題種別が'タスク'の場合､  IssueTypeId は 624224
                    課題種別が'バグ'の場合､ IssueTypeId は 624223
                    課題種別が'要望'の場合､ IssueTypeId は 624225
                    課題種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
            "required": [
                "summary",
                "projectId",
                "issueTypeId",
                "priorityId",
            ],
        },
    },
    {
        "name": "update_task",
        "description": "Update task using a task management system called Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "issueKey": {
                    "type": "string",
                    "description": "課題のキー",
                    "example": "ABC-1",
                },
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題の種別が'タスク'の場合､ IssueTypeId は 624224
                    課題の種別が'バグ'の場合､ IssueTypeId は 624223
                    課題の種別が'要望'の場合､ IssueTypeId は 624225
                    課題の種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2023-11-01",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2023-11-11",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
        },
    },
]
```

#### プロンプト

プロンプトは以下のとおりです｡課題情報の更新の場合は､更新対象の課題キーを指定します｡

```console
messages = [
    {
        "role": "system",
        "content": "あなたはタスクマネージャーである。あなたには関数のリストと、関数で使用するパラメータが与えられています。また、作成すべきタスクのリストも与えられています。与えられた関数とパラメータを使ってタスクを作成する必要があります。作成したタスクのリストを返してください。",
    },
    {
        "role": "user",
        "content": """
            Backlogの課題を更新してください｡
            対象の課題は APP-49 です。
            課題の件名を｢アプリケーションに新規機能とヘルプページを追加する｣とします｡
            課題種別をタスクに変更します｡
            優先度を中にします。
            期限日は2023年11月30日です｡
            """,
    },
]
```

#### LLMからのレスポンス

上記のプロンプトをもとに､LLMから以下のレスポンスが得られました｡プロンプトの内容をもとにupdate_task関数が選択され､プロンプトに記述した内容がそれぞれパラメータに当てはめられているのが分かります｡依頼内容の文末を｢~です｡｣｢~とします｡｣｢~に変更します｡｣などばらばらにしてみましたが､いずれも正しく処理されました｡

```json
{
	"id": "chatcmpl-8H3gvJfnZvWegUrNstfuiY6EdFeuo",
	"object": "chat.completion",
	"created": 1699076457,
	"model": "gpt-3.5-turbo-0613",
	"choices": [
		{
			"index": 0,
			"message": {
				"role": "assistant",
				"content": null,
				"function_call": {
					"name": "update_task",
					"arguments": "{\n  \"issueKey\": \"APP-49\",\n  \"summary\": \"アプリケーションに新規機能とヘルプページを追加する\",\n  \"issueTypeId\": \"624224\",\n  \"priorityId\": \"3\",\n  \"dueDate\": \"2023-11-30\"\n}"
				}
			},
			"finish_reason": "function_call"
		}
	],
	"usage": {
		"prompt_tokens": 1016,
		"completion_tokens": 82,
		"total_tokens": 1098
	}
}
```

課題の追加と同様にプロンプト､LLMからのレスポンスそしてBacklog APIからのレスポンスをつなげてLLMに渡します｡その結果､以下のレスポンスを得ました｡突然｢どういたしまして！｣と返信がある点や､課題情報の更新にも関わらず､｢タスクが作成されました｡｣となっている点が不自然ですが､情報の更新は正しく行われました｡

```console
タスクが作成されました。以下が作成されたタスクの情報です：

- 課題キー: APP-49
- 件名: アプリケーションに新規機能とヘルプページを追加する
- 課題種別: タスク
- 優先度: 中
- 期限日: 2023年11月30日

このタスクは、2023年11月6日に開始されました。

どういたしまして！もしご質問がございましたら、お気軽にお聞きください。
```

#### Backlog画面

実際のBacklogの画面はこのようになりました｡課題種別､課題の件名､優先度､期限日のそれぞれが自然言語で指示したとおりのものとなりました｡
![スクリーンショット 2023-11-04 151359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/9857d667-d1c8-aa96-3508-7718bea540c0.png)


#### コード全体

```python
import json
import logging
import os
import sys

import requests
from openai import OpenAI

# logging.disable()
logging.basicConfig(stream=sys.stdout, level=logging.INFO, force=True)

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

BACKLOG_SPACE_KEY = "xxxx"
BACKLOG_BASE_URL = f"https://{BACKLOG_SPACE_KEY}.backlog.com"
BACKLOG_API_KEY = os.environ.get("BACKLOG_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")


def create_task(arguments):
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {
        "projectId": arguments.get("projectId"),
        "summary": arguments.get("summary"),
        "description": arguments.get("description"),
        "issueTypeId": arguments.get("issueTypeId"),
        "startDate": arguments.get("startDate"),
        "dueDate": arguments.get("dueDate"),
        "priorityId": arguments.get("priorityId"),
    }

    response_body = requests.post(
        f"{BACKLOG_BASE_URL}/api/v2/issues?apiKey={BACKLOG_API_KEY}",
        data=data,
        headers=headers,
        timeout=10,
    )

    return json.dumps(response_body.json())


def update_task(arguments):
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    data = {
        "summary": arguments.get("summary"),
        "description": arguments.get("description"),
        "issueTypeId": arguments.get("issueTypeId"),
        "startDate": arguments.get("startDate"),
        "dueDate": arguments.get("dueDate"),
        "priorityId": arguments.get("priorityId"),
    }

    response_body = requests.patch(
        f"{BACKLOG_BASE_URL}/api/v2/issues/{arguments.get('issueKey')}?apiKey={BACKLOG_API_KEY}",
        data=data,
        headers=headers,
        timeout=10,
    )

    return json.dumps(response_body.json())


functions = [
    {
        "name": "create_task",
        "description": "Create a task in a task management system named Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題種別が'タスク'の場合､  IssueTypeId は 624224
                    課題種別が'バグ'の場合､ IssueTypeId は 624223
                    課題種別が'要望'の場合､ IssueTypeId は 624225
                    課題種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2019-11-13",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
            "required": [
                "summary",
                "projectId",
                "issueTypeId",
                "priorityId",
            ],
        },
    },
    {
        "name": "update_task",
        "description": "Update a task in a task management system named Backlog.",
        "parameters": {
            "type": "object",
            "properties": {
                "issueKey": {
                    "type": "string",
                    "description": "課題のキー",
                    "example": "ABC-1",
                },
                "summary": {
                    "type": "string",
                    "description": "課題の件名",
                },
                "projectId": {
                    "type": "number",
                    "description": "課題を登録するプロジェクトのID",
                },
                "description": {
                    "type": "string",
                    "description": "課題の詳細",
                },
                "issueTypeId": {
                    "type": "number",
                    "enum": [624223, 624224, 624225, 624226],
                    "description": """
                    課題の種別のID
                    課題の種別が'タスク'の場合､ IssueTypeId は 624224
                    課題の種別が'バグ'の場合､ IssueTypeId は 624223
                    課題の種別が'要望'の場合､ IssueTypeId は 624225
                    課題の種別が'その他'の場合､ IssueTypeId は 624226
                    """,
                },
                "startDate": {
                    "type": "string",
                    "description": "課題の開始日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2023-11-01",
                },
                "dueDate": {
                    "type": "string",
                    "description": "課題の期日(yyyy-MM-dd)",
                    "format": "yyyy-MM-dd",
                    "example": "2023-11-11",
                },
                "priorityId": {
                    "type": "number",
                    "enum": [2, 3, 4],
                    "description": """
                    課題の優先度のID
                    優先度が'低'の場合､  PriorityId は 4
                    優先度が'中'の場合､  PriorityId は 3
                    優先度が'高'の場合､  PriorityId は 2
                    """,
                },
            },
        },
    },
]

messages = [
    {
        "role": "system",
        "content": "あなたはタスクマネージャーである。あなたには関数のリストと、関数で使用するパラメータが与えられています。また、作成すべきタスクのリストも与えられています。与えられた関数とパラメータを使ってタスクを作成する必要があります。作成したタスクのリストを返してください。",
    },
    {
        "role": "user",
        "content": """
            Backlogの課題を更新してください｡
            対象の課題は APP-103 です。
            課題の件名を｢アプリケーションに新規機能とヘルプページを追加する｣とします｡
            課題種別をタスクに変更します｡
            優先度を中にします。
            期限日は2023年11月30日です｡
            """,
    },
]

# 使用する関数と､与える引数
response = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=messages,
    function_call="auto",
    functions=functions,
)
logging.info("response %s", response)

llm_response = response.choices[0].message


if llm_response.function_call:
    # 選択した関数を実行
    available_functions = {
        "create_task": create_task,
        "update_task": update_task,
    }

    function_name = llm_response.function_call.name
    function_to_call = available_functions[function_name]
    function_args = json.loads(llm_response.function_call.arguments)

    # LLMが選択した引数を関数に渡す
    function_response = function_to_call(function_args)
    logging.info("function_response %s", function_response)

    # LLMのレスポンスと関数の実行結果をmessagesに追加
    messages.append(llm_response)
    messages.append(
        {
            "role": "function",
            "name": function_name,
            "content": function_response,
        }
    )

    logging.info("messages %s", messages)

    # messagesを再度LLMに渡す
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
    )

    message = response.choices[0].message
    print(message.content)

else:
    print("No further action.")
```


# まとめ

OpenAPIのFunction Callingとタスク管理ツール Backlog のAPIを連携し､自然言語で課題の追加や課題情報の更新が行えるかを検証しました｡LLMがプロンプトの内容にもとづいて使用する関数とパラメータを判断し､それを利用してBacklog APIにつなぐことで課題の登録や更新を行うことができました｡自然言語でBacklogの課題を操作できるため､チャットアプリや音声アシスタントとの会話から課題を起票したり内容を確認するといったことが簡単に行えそうです｡