スクエニ ITエンジニア ブログ

[番外編] ハマグリ式! GPT-4 API を使った Jira issue 作成君を Python で書いてみた

はじめに

この記事を見つけたけど、後で見ようと思ったそこのあなた!

ぜひ下のボタンから、ハッシュタグ #ハマグリ式 でポストしておきましょう!

こんにちハマグリ。貝藤らんまだぞ。 今回は、ハマグリ式! Jira issue 作成君を Python で書いてみた をお届けします!

初級って?

ハマグリ式では、下記のようにレベルを設定しています。

  1. 初級者:初めてクラウドサービスを利用する人で、基本的な操作(例:ファイルの保存や、サーバーの起動)をインターフェースを通じて行うことができます。また、シンプルなセキュリティルールの設定や、一部の問題のトラブルシューティングに対応できます。
  2. 中級者:より深い知識を持ち、コードを用いて操作を自動化したり、より複雑なタスク(例:自動でサーバーの数を増減させる)を行います。また、より高度な監視や、全体のシステム設計と実装について理解があります。
  3. 上級者:幅広く深い知識を持ち、大規模で複雑なシステムを設計、実装、維持する能力があります。最先端のテクノロジーを活用し、安全性、耐障害性、効率性を最大化するためのソリューションを提供します。

今回は上記と関係の薄い生成系 AI についての記事であるため、番外編に分類しています。

ハマグリ式って?

貝藤らんまが作成するブログ記事のブランド名です。あまり気にせず読み飛ばしてください。

何を書くの?

以下の通りです。

  • この記事で書くこと
    • GPT-4 API を使った Jira issue を作成するプログラム
    • プログラムの拡張案
  • この記事で書かないこと
    • GPT-4 の解説
    • python コードの解説
    • AI やプログラムの精度の検討
    • プログラムの拡張案の比較検討

免責事項

  • この記事に書かれていることは弊社の意見を代表するものではありません。
  • この記事に書かれていることには一定の調査と検証を実施しておりますが、間違いが存在しうることはご承知おき下さい。
  • 筆者の専門外の内容については断定を避けておりますが、あらかじめ間違いが存在しうることはご承知おきください。
  • 記事の内容は、記事執筆時点 (2024/1) での情報です。ご承知おきください。

GPT-4 API を使った Jira issue 作成君を Python で書いてみる

巷で話題の GPT-4 、せっかくなら業務に組み込みたいですよね。

OpenAI API の Function Calling を使えば色々と応用ができそうです。

今回は比較的いろんなジャンルの業務で使われていそうなサービスである Jira と連携する簡単なスクリプトを作って動かしてみます。

ちなみにこの記事で使うのは Atlassian Jira Cloud だぞ。

Function Calling についての補足

Function calling は GPT の API がユーザーの用意した関数を理解して適宜利用してくれる機能です。

直近の tools オプションの有効化により複数関数を同時並列で実行できるようになり、複雑なタスクも解決できそうです。

ただし今回は単純な課題を解決するので、関数は1つのみ用意しています。

参考:

設計

下図のようなシンプルな python コードを作成していきます。

コード

コードは結果だけ記載します。

まずは認証情報を保存します。今回は venv で環境ディレクトリを作成してみます。

$ python -m venv venv
$ vim .env

.env は以下です。

source venv/bin/activate
export OPENAI_API_KEY=****
export JIRA_EMAIL=****@****
export JIRA_API_TOKEN=****

続いて環境を構築して肝心のコードを作成していきます。

$ source .env
(venv) $ echo openai >> requirements.txt
(venv) $ echo jira >> requirements.txt
(venv) $ pip install -r requirements.txt
$ vim main.py

main.py は以下です。

import os
import sys
from jira import JIRA
from openai import OpenAI
import json

def jira(tasks):
    jira_email = os.getenv('JIRA_EMAIL')
    jira_token = os.getenv('JIRA_API_TOKEN')
    jira_server = 'https://****.atlassian.net'

    jira_client = JIRA(server=jira_server, basic_auth=(jira_email, jira_token))

    created_issues = []

    for task in tasks:
        issue_dict = {
            'project': {'key': task.get('project', '')},
            'summary': task.get('summary', ''),
            'description': task.get('description', ''),
            'issuetype': {'name': 'Task'},
            'assignee': {'name': task.get('assignee')}
        }

        new_issue = jira_client.create_issue(fields=issue_dict)
        created_issues.append(f"{new_issue.key}")

    return f"タスク作成完了: {', '.join(created_issues)}"

def run_conversation(prompt=sys.argv[1]):
    client = OpenAI()
    messages = [{"role": "user", "content": prompt}]
    tools = [
        {
            "type": "function",
            "function": {
                "name": "jira",
                "description": "Create new tasks in JIRA",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "tasks": {
                            "type": "array",
                            "description": "List of tasks to be created in JIRA",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "project": {
                                        "type": "string",
                                        "description": "Project key in JIRA"
                                    },
                                    "summary": {
                                        "type": "string",
                                        "description": "The summary of the task. This is used as the title of the task"
                                    },
                                    "description": {
                                        "type": "string",
                                        "description": "The detailed description of the task"
                                    },
                                    "assignee": {
                                        "type": "string",
                                        "description": "The assignee of the task"
                                    }
                                },
                                "required": ["project", "summary", "description"]
                            }
                        }
                    },
                    "required": ["tasks"],
                },
            },
        }
    ]

    while True:

        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=tools,
            tool_choice="auto",
            n=1
        )

        print("response: ", response)

        response_message = response.choices[0].message
        tool_calls = response_message.tool_calls

        if tool_calls:
            available_functions = {
                "jira": jira
            }

            function_args_mapping = {
                "jira": ["tasks"]
            }

            messages.append(response_message)
            for tool_call in tool_calls:
                function_name = tool_call.function.name
                function_to_call = available_functions[function_name]
                function_args = json.loads(tool_call.function.arguments)
                args_to_pass = {arg: function_args.get(arg) for arg in function_args_mapping[function_name]}
                function_response = function_to_call(**args_to_pass)

                if function_name == "jira":
                    return

                # Function calling の tools オプションで関数を複数用意する場合、結果を格納してループを継続
                # 今回は jira 関数のみなのでこの append 文は実質不要
                messages.append(
                    {
                        "tool_call_id": tool_call.id,
                        "role": "tool",
                        "name": function_name,
                        "content": function_response,
                    }
                )
 
if __name__ == '__main__':
    run_conversation()

もっといい書き方がありそうだけど、動いたからよしとするぞ。

結果

結果は以下のようになりました。

※情報を適宜編集・マスクしているので、完全に同じ出力が再現できるかどうかは保証できません。

$ python3 jira_create.py "テスト issue を作成してください。summary, description は仮置きでいい 
ので考えてください。担当者はなしでいいです。"
response: ChatCompletion(id='chatcmpl-ABCD1234EFGH5678IJKL9012MNOP', choices=[Choice(finish_reason='tool_calls', index=0, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_ABCD1234EFGH5678IJKL9012', function=Function(arguments='{\n  "tasks": [\n    {\n      "project": "Hamaguri",\n      "summary": "Test Issue",\n      "description": "This is a placeholder for creating a test issue to confirm the JIRA integration functionality."\n    }\n  ]\n}', name='jira'), type='function')]), logprobs=None)], created=1705988037, model='gpt-4', object='chat.completion', system_fingerprint='fp_de12345678', usage=CompletionUsage(completion_tokens=61, prompt_tokens=241, total_tokens=302))type='function')

もっと拡張してみる

操作するフィールドを増やす

もちろん、期限やラベルなどを操作することも可能です

コード中の以下2箇所へ、ドキュメントを参考にキーバリューを追加するとよいでしょう。

issue_dict = {
    'project': {'key': task.get('project', '')},
    'summary': task.get('summary', ''),
    'description': task.get('description', ''),
    'issuetype': {'name': 'Task'},
    'assignee': {'name': task.get('assignee')}
}
"properties": {
    "project": {
        "type": "string",
        "description": "Project key in JIRA"
    },
    "summary": {
        "type": "string",
        "description": "The summary of the task. This is used as the title of the task"
    },
    "description": {
        "type": "string",
        "description": "The detailed description of the task"
    },
    "assignee": {
        "type": "string",
        "description": "The assignee of the task"
    }
},

ただし以下の文章から考えるに、API へ渡す properties が多すぎると GPT の解釈が不安定になるかもしれません。正確な動作を期待するなら検証したほうがよいでしょう。

https://platform.openai.com/docs/guides/function-calling

The model can choose to call one or more functions; if so, the content will be a stringified JSON object adhering to your custom schema (note: the model may hallucinate parameters).

ドキュメントに明記されているわけじゃないが、properties も LLM が「理解」していないとは断言できないぞ。

エージェント構成

AI に tool_call をいくつも読み込ませて、複雑なタスクを一括でやらせるのは精度が下がります。

精度を保ちながら複雑なタスクを実行させるにはエージェントを分割するのがよさそうです。

参考:

下図のようにクラスや関数を分割して、情報をやり取りするような構成が考えられます。

デフォルトプロンプトの追加

言わずもがな、LLM はプロンプトによって精度が上がったり下がったりします。

以下のようにプロンプトを工夫したり、system プロンプトを追加することで精度向上が見込めるでしょう。

messages = [{"role": "user", "content": "あなたはJIRA issue に関する指示を受け、遂行するAIです。あなたとは別のissue作成エージェント、issue更新エージェント、issue検索エージェントを活用して指示の完了を目指します。エージェントは一度に複数使用してはいけません。エージェントの応答に含まれる情報を使って他のエージェントへ指示する、ということを繰り返してください。指示は以下です。「" + prompt + "」ただし、issueの削除はステータスを中止にすることと考えてください。"}

role を system にして指示を格納しておくとより良い出力が期待できるかもしれないぞ。

まとめ

以上、ハマグリ式! GPT-4 API を使った Jira issue 作成君を Python で書いてみた でした!

実際に機能を拡張していくと、Jira API のエラーハンドリングや Jira のプロジェクトリスト、メンバー一覧等も必要になってくると思います。

かといって Jira を操作したいというプログラムに高度な機能を盛り込みすぎても微妙なので、

  • Slack の Jira アプリに無い機能を優先する
  • 抽象的な部分を関数化するのではなく、できるだけ LLM によせて開発していく

などを意識していくとよいのではないでしょうか。

ぜひ下のボタンから、ハッシュタグ #ハマグリ式 で感想をポストしてください!

今後ともハマグリ式をどうぞよろしくお願いいたします!

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!