はじめに
前回の記事、OpenAIのFunction callingとBacklog APIを使って、自然言語で課題を作成,更新するでは、ハードコーディングしたプロンプトを用いてBacklog課題の登録や更新を行いました。今回は、Slack Appを作成し、ボットにメンション形式でプロンプトを渡してBacklog課題の登録や更新を行います。
僕はBacklogに課題を登録する際にどのような件名にするか迷うことがあります。そこで、課題の内容をもとに件名を自動作成するようにしました。
また、OpenAI APIに課題詳細の一部を自動作成してもらうようなプロンプトを作ってみました。
このボットはChatGPTのように履歴をもとにした会話を続けることはできないため、シンプルなアシスタントとして捉えてもらえればと思います。
今回はSlackボットを作成しましたが、別記事で同様の動作をするTypetalkボットの作成を紹介しています。
デモ画面
実際に動かしてみた画面のスクリーンショットがこちらです。このように、ボットに自然言語で依頼すると、Backlog課題の登録や更新が行えます。
(アニメーションGIFが記事内で表示されないため、スクリーンショットを載せます。)
プロンプトの入力
スレッドで課題の作成を確認
実際に作成されたBacklog課題
はじめに、課題登録を依頼する以下のプロンプトをポストしました。OpenAI APIのFunction callingが「課題を登録」というキーワードをもとに、課題登録を行うfunctions定義を選択し、Backlog APIに渡すパラメータ名と具体的な値を判断しました。
課題種別や優先度、期日などはそれぞれプロンプトにあるものが反映され、依頼内容をもとに課題タイトルや課題本文が作成されました。
APPプロジェクトに課題を登録してください。
課題本文は以下のとおりです。課題本文の概要をもとに課題のタイトルをつけてください。
開発工程には、アプリケーション開発工程10項目を以下の制約条件と出力形式に従って作成してください。
開発工程とは、例えば要件定義や基本設計などです。
出力形式
|タスク|解説|
|---|---|
|要件定義|要件定義を行う|
課題種別は要望です。
優先度は高です。
開始日は2023年12月5日、期限日は4ヶ月後です。
"""課題本文
## 開発工程
{ここに開発工程10項目を出力する}
## プロジェクトの概要
新規にXXXを実現する機能を実装する。
テストユーザーによるテストを行い、不具合がないことを確認した後、本番環境にリリースする。
## 目的
ユーザーからのフィードバックによると、XXXができないことが課題として挙げられている。
この課題を解決するため、XXXを実現する機能を実装する。
## 完了条件
- テストユーザーによるテストを行い、不具合がないこと
- 本番環境へのリリースを行い、ユーザーからの不具合報告がないこと
## 数値目標
- テストユーザーによるテストにおいて、軽微なバグの件数を10件以下にする
- テストユーザーによるテストにおいて、重大なバグの件数を0件にする
- リリース後、1週間以内にユーザーからの不具合報告が0件になる
"""
次に、課題更新を依頼する以下のプロンプトをポストしました。この場合は、Funcrtion Callingが「課題を更新」というキーワードをもとに更新を行うfunctions定義を選択しました。
そして、指定した課題キーをもつBacklog課題が更新されました。
Backlogの課題を更新してください。
対象の課題は APP-326 です。
課題の件名を「アプリケーションに新規機能とヘルプページを追加する」とします。
課題種別をタスクに変更します。
優先度を中にします。
期限日は2023年12月27日です。
以降、環境構築やコードの詳解です。
参考情報
前提条件
-
以下のリソースを作成・削除・変更できる権限をもつAWSユーザーを利用すること
- AWS IAM
- AWS Lambda
- AWS CloudFormation
- Amazon API Gateway
- Amazon S3
- Amazon CloudWatch Logs
-
使用するAWSリージョンは、us-east-1 (リージョンの制約はないため、他のリージョンも可)
-
OpenAIのAPIキーを取得済み
-
Slack Appを作成するためのアカウントや権限を持っている
-
BacklogのAPIキーやコード内で設定するissueTypeId(課題種別ID)などの準備ができている (Backlog APIの利用に必要なAPIキーやパラメータの確認方法は、以下の記事が参考になります。)
環境構築
OSのバージョン
Windows 11上のWSLでUbuntu 23.04を動かしています。
PRETTY_NAME="Ubuntu 23.04"
NAME="Ubuntu"
VERSION_ID="23.04"
VERSION="23.04 (Lunar Lobster)"
VERSION_CODENAME=lunar
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=lunar
LOGO=ubuntu-logo
Python環境
$ python3 --version
Python 3.11.4
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip3 install --upgrade pip
$ pip3 --version
pip 23.3.1 from /home/xxx/.venv/lib/python3.11/site-packages/pip (python 3.11)
AWS環境構築
aws configure
コマンドでデフォルトのリージョンやクレデンシャルを設定するか、もしくは~/.aws/configや~/.aws/credentialsを用意します。
AWS ChaliceではなくAWS SAMを使う
以下の記事ではAWS Chaliceを使ってSlack Appを作成しました。
今回も同様にAWS Chaliceを使って構築を行おうとしたのですが、Lambdaのコールドスタートの影響を回避できない事象に遭遇しました。
以下の様なコメントもあり、AWS ChaliceではコールドスタートやSlack APIのタイムアウト制約に遭遇してしまうようです。そのため、AWS SAMを使うことにしました。
AWS SAM CLIインストール
AWS上でサーバーレスアプリケーションを構築、実行するAWS SAMを使用します。
Installing the AWS SAM CLI の手順に従い、AWS SAM CLIをインストールします。今回はx86_64環境でLinux OSを使用するため、x86_64 - command line installer
の手順を実行します。
$ sam --version
SAM CLI, version 1.103.0
Slack Appの作成
Slac APIを開き、From scratchからSlack Appを作成します。ここでは、App Nameをbacklog-assistant
とします。
Basic Information画面のApp Credentialsに表示されているクレデンシャルはSlackSigningSecret
として後述のデプロイ時に使用します。
OAuth & Permissions画面のOAuth Tokens for Your WorkspaceにあるBot User OAuth Tokenは、SlackBotToken
として後述のデプロイ時に使用します。
OAuth & Permissions画面のBot Token Scopesにapp_mentions:readとchat:writeを追加します。
アプリケーションの構築
ディレクトリ構造は以下のとおりです。
.
├── openai_slack_backlog_assitant
│ ├── __init__.py
│ ├── app.py
│ └── requirements.txt
├── samconfig.toml
└── template.yaml
__init__.py
は空のファイルです。
requirements.txtは以下のとおりです。openaiライブラリはv1.0.0で仕様が大幅に変更されたため、バージョン範囲を指定します。詳しくは、v1.0.0 Migration Guideに記載されています。
slack-bolt
slack-sdk
langchain
openai>=1.0.0
template.yaml (長いので折りたたんでいます。クリックして展開)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Register tasks using a Backlog task management system.
Resources:
BacklogAssitantFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: openai_slack_backlog_assitant/
Handler: app.lambda_handler
Runtime: python3.11
Role: !GetAtt LambdaRole.Arn
Timeout: 180
MemorySize: 384
Architectures:
- x86_64
Environment:
Variables:
SLACK_SIGNING_SECRET: !Ref SlackSigningSecret
SLACK_BOT_TOKEN: !Ref SlackBotToken
OPENAI_API_KEY: !Ref OpenaiApiKey
BACKLOG_API_KEY: !Ref BacklogApiKey
BACKLOG_BASE_URL: !Ref BacklogBaseUrl
Events:
Slack:
Type: Api
Properties:
Method: POST
Path: /slack/events
TimeoutInMillis: 1000
LambdaRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: backlog-assitant-lambda-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: allow-lambda-invocation
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
- lambda:InvokeAsync
Resource: "*"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
BacklogAssitantLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${BacklogAssitantFunction}
RetentionInDays: 14
Parameters:
SlackSigningSecret:
Type: String
Default: ""
SlackBotToken:
Type: String
Default: ""
OpenaiApiKey:
Type: String
Default: ""
BacklogApiKey:
Type: String
Default: ""
BacklogBaseUrl:
Type: String
Default: ""
Outputs:
BacklogAssitantApi:
Description: "The URL of Slack Event Subscriptions"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/slack/events"
BacklogAssitantFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt BacklogAssitantFunction.Arn
BacklogAssitantFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt LambdaRole.Arn
AWS SAM テンプレートファイル(template.yaml)に、作成するAWSリソースを定義します。Lambda関数用ロールやLambdaの環境変数、API Gatewayの設定が含まれます。
Lambdaのメモリサイズは384MBに設定しました。256MBでも動作しますが、OpenAI APIからのレスポンス時間が1~2分かかるためメモリ量を調整しました。この結果、レスポンス時間は数秒に短縮されました。この調整については、Why calling OpenAI API from Lambda is much slower than calling from EC2 t2.micro が参考になりました。
samconfig.toml (長いので折りたたんでいます。クリックして展開)
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1
[default]
[default.global.parameters]
stack_name = "openai-slack-backlog-assitant"
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
resolve_s3 = true
region = "us-east-1"
[default.package.parameters]
resolve_s3 = true
[default.sync.parameters]
watch = true
[default.local_start_api.parameters]
warm_containers = "EAGER"
[default.local_start_lambda.parameters]
warm_containers = "EAGER"
SAM CLIの実行設定ファイル(samconfig.toml)に、SAM CLIを実行する際の設定を定義します。AWS SAMのチュートリアル: Hello World アプリケーションのデプロイを実行した際に作成されるsamconfig.tomlをもとにしています。今回の例では、以下の点を変更しています。
-
[default.global.parameters]
セクションのstack_name
を"sam-app"から"openai-slack-backlog-assitant"に変更 -
[default.deploy.parameters]
セクションにregion
指定を追加 -
[default.deploy.parameters]
セクションのcapabilities
を"CAPABILITY_IAM"から"CAPABILITY_NAMED_IAM"に変更
app.py (長いので折りたたんでいます。クリックして展開)
import json
import logging
import os
import re
import time
from datetime import datetime, timedelta, timezone
from typing import Any
import requests
from langchain.callbacks.base import BaseCallbackHandler
from langchain.chat_models import ChatOpenAI
from langchain.schema import FunctionMessage, HumanMessage, LLMResult, SystemMessage
from openai import OpenAI
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
# BacklogのURlとAPIキー、OpenAIのAPIキーを環境変数から取得する
BACKLOG_BASE_URL = os.environ.get("BACKLOG_BASE_URL")
BACKLOG_API_KEY = os.environ.get("BACKLOG_API_KEY")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
# チャットの更新間隔
CHAT_UPDATE_INTERVAL_SEC = 1
# システムメッセージ
OPENAI_SYSTEM_TEXT = """
あなたはタスクマネージャーです。あなたには関数のリストと、関数で使用するパラメータが与えられています。
与えられた関数とパラメータを使ってタスクを作成する必要があります。
タスクを作成し、作成したタスクを返してください。
"""
# ログの設定
SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(message)s",
level=logging.INFO
)
# OpenAI APIのクライアントを作成する
client = OpenAI(api_key=OPENAI_API_KEY)
# Slackアプリの設定
app = App(
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
token=os.environ["SLACK_BOT_TOKEN"],
process_before_response=True,
)
def create_task(arguments):
"""
課題を登録する関数
arguments: Function callingが判定した、Backlog APIに渡すパラメータ
"""
# Backlog APIに渡すパラメータを設定する
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 = requests.post(
f"{BACKLOG_BASE_URL}/api/v2/issues?apiKey={BACKLOG_API_KEY}",
data=data,
headers=headers,
timeout=10,
)
return json.dumps(response.json())
def update_task(arguments):
"""
課題を更新する関数
arguments: Function callingが判定した、Backlog APIに渡すパラメータ
"""
# Backlog APIに渡すパラメータを設定する
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 = 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.json())
class SlackStreamingCallbackHandler(BaseCallbackHandler):
"""
Slackのメッセージをストリーミングで更新するためのコールバックハンドラ
"""
last_send_time = time.time()
message = ""
def __init__(self, channel, ts):
self.channel = channel
self.ts = ts
self.interval = CHAT_UPDATE_INTERVAL_SEC
# 投稿を更新した累計回数カウンタ
self.update_count = 0
def on_llm_new_token(self, token: str, **kwargs) -> None:
"""
LLMがトークンを生成したら、メッセージを更新する
メッセージをストリーミングで更新する
"""
self.message += token
now = time.time()
if now - self.last_send_time > self.interval:
app.client.chat_update(
channel=self.channel,
ts=self.ts,
text=f"{self.message}\n\n..."
)
self.last_send_time = now
self.update_count += 1
# update_countが現在の更新間隔X10より多くなるたびに更新間隔を2倍にする
if self.update_count / 10 > self.interval:
self.interval = self.interval * 2
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
"""
LLMの実行が終了したら、注意書きを追加してメッセージを更新する
https://api.slack.com/block-kit
"""
message_context = """
OpenAI APIで生成される情報は不正確または不適切な場合があります。
"""
message_blocks = [
{
"type": "section",
"text":
{
"type": "mrkdwn",
"text": self.message
}
},
{
"type": "divider"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": message_context
}
],
},
]
app.client.chat_update(
channel=self.channel,
ts=self.ts,
text=self.message,
blocks=message_blocks,
)
# @app.event("app_mention")
def handle_app_mentions(event, say):
"""
Slackのメンションを受け取ったら、メッセージをOpenAI APIに送信する
"""
channel = event["channel"]
thread_ts = event["ts"]
# Slackでメンションを受け取ったら、メッセージからメンションを取り除く
user_message = re.sub(r'<@.*?> ', "", event["text"])
# botからの回答を待つために、一度メッセージを送信する
result = say("\n\nお待ちください...", thread_ts=thread_ts)
ts = result["ts"]
# 利用可能な関数とプロンプトを設定する
functions = [
{
# 課題を登録する関数
"name": "create_task",
"description": """
Create tasks using a Backlog task management system.
""",
# Backlog APIに合わせてパラメータを定義する
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"example": "〇〇を××する",
"description": " 件名だけ読めば「何を」「どうすれば」タスクが終わるのかわかる件名を付ける。",
},
"projectId": {
"type": "number",
"description": """
課題を登録するプロジェクトのID
APPプロジェクトの場合、projectIdは 129821
""",
},
"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 tasks using a Backlog task management system.
""",
"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": OPENAI_SYSTEM_TEXT,
},
{
"role": "user",
"content": user_message + "\n" +
"現在時刻: " + str(datetime.now(timezone(timedelta(hours=+9), 'JST')))
}
]
logging.info("messages: %s", messages)
# プロンプトと関数をOpenAI APIに渡す
response = client.chat.completions.create(
model="gpt-3.5-turbo-1106",
messages=messages,
temperature=0,
functions=functions,
function_call="auto"
)
response_message = response.choices[0].message
logging.info("response_message: %s", response_message)
# モデルが関数呼び出し行うと判断した場合
if response_message.function_call:
# 実行する関数名
available_functions = {
"create_task": create_task,
"update_task": update_task,
}
# function callが選択した関数名
function_name = response_message.function_call.name
# function callが選択した関数名に対応するPython関数
function_to_call = available_functions[function_name]
# function callが選択した関数に渡すパラメータ
function_args = json.loads(response_message.function_call.arguments)
# Python関数を実行し結果を取得
function_response = function_to_call(function_args)
# システムメッセージのプロンプト
messages = [SystemMessage(content=OPENAI_SYSTEM_TEXT)]
# Slackで入力した内容
messages.append(HumanMessage(content=user_message))
# Python関数の実行した結果から得られた情報
messages.append(FunctionMessage(name=function_name, content=function_response))
callback = SlackStreamingCallbackHandler(channel=channel, ts=ts)
llm = ChatOpenAI(
model="gpt-3.5-turbo-1106",
temperature=0,
streaming=True,
callbacks=[callback],
)
logging.info("messages: %s", messages)
# モデルにプロンプトを送信し、結果をSlackに送信する
llm(messages)
def respond_to_slack_within_3_seconds(ack):
ack()
app.event("app_mention")(
ack=respond_to_slack_within_3_seconds,
lazy=[handle_app_mentions]
)
def lambda_handler(event, context):
logging.info(json.dumps(event))
retry_counts = event.get("multiValueHeaders", {}).get("X-Slack-Retry-Num", [0])
if retry_counts[0] != 0:
logging.info("Skip slack retrying(%s).", retry_counts)
return {}
slack_handler = SlackRequestHandler(app=app)
return slack_handler.handle(event, context)
ボットプログラムの本体です。メンションをストリーミングで出力するコールバックハンドラや、Bolt for Python を使ったアプリをAWS Lambda上で動かしやすくするためのLazy Listenersを利用しています。 Function callingを扱う箇所は、OpenAIのドキュメントなどを参考にしています。
ビルド
template.yamlがあるディレクトリで、ビルドコマンドを実行します。
$ sam build
ビルドに成功すると、いかのようなメッセージが表示されます。
Starting Build use cache
Manifest is not changed for (BacklogAssitantFunction), running incremental build
Building codeuri: /home/xxx/openai_slack_backlog_assitant runtime: python3.11 metadata: {} architecture: x86_64 functions:
BacklogAssitantFunction
Running PythonPipBuilder:CopySource
Running PythonPipBuilder:CopySource
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
デプロイ
sam deployコマンドを実行し、デプロイを行います。引数には、前述のSlack App設定画面で取得したSlackSigningSecretやSlackBotToken、OpenAPIのAPIキー、BacklogのAPIキー、BacklogのURlを指定します。BacklogのURL内のドメインは、ユーザーによってはbacklog.jpの場合があります。
ここではコマンドラインの引数に直接キーを渡していますが、本運用する場合はAWS Secrets Managerにキーを保存するなど安全に管理するほうが良いと思います。
sam deploy \
--parameter-overrides SlackSigningSecret=xxxxxxxxxx \
SlackBotToken=xoxb-xxx-xxxx-xxxxx \
OpenaiApiKey=sk-xxxxxxxxxx \
BacklogApiKey=xxxxxxxxxx \
BacklogBaseUrl=https://xxxx.backlog.com/
デプロイが成功すると、以下のような情報がコンソールに出力されます。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key BacklogAssitantApi
Description API Gateway endpoint URL for Prod stage for BacklogAssitantFunction function
Value https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/slack/events
Key BacklogAssitantFunction
Description Hello World Lambda Function ARN
Value arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:openai-slack-backlog-assit-BacklogAssitantFunction-xxxxxxxxxxxx
Key BacklogAssitantFunctionIamRole
Description Implicit IAM Role created for Hello World function
Value arn:aws:iam::xxxxxxxxxxxx:role/backlog-assitant-lambda-role
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Slack Appの設定
メンションイベントに応答するために、Event Subscriptions画面のSubscribe to bot eventsにapp_mentionを追加します。
Event Subscriptions画面のEnable EventsをOnにし、Request URLにさきほど出力されたURL https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/slack/events を入力します。
Verified✓
と表示されれば、正しいURLが入力されたことになります。
設定を追加後、画面最下部にあるSave Changesをクリックし内容を保存します。
Slack Workspaseにアプリをインストール
Install App画面のInstall to Workspaceをクリックし、Slack AppをWorkspaceにインストールします。
インストールに成功すると、Thank you!画面が表示されます。Slackアプリをインストールしている場合はclick here
のリンク、Webブラウザを使用している場合は、this link
をクリックしてSlackを開きます。
動作確認
任意のチャンネンルに@backlog-assistant
を招待し、メンション形式で依頼をポストします。動作しない場合のログやOpenAI APIが返すレスポンスは CloudWatch Logs に出力されているログが参考になります。
作成したリソースの削除
最後に、作成したアプリケーションを削除する手順です。リソースを削除するには sam delete
コマンドを実行します。
$ sam delete
コードについて
コードのなかから、Function callingのポイントとなる課題の登録と更新を行うfunctionを抜粋して紹介します。
課題の登録
Backlog APIの課題の追加のリクエストパラメーターをもとにcreate_task関数を定義しました。LLMが自然言語をもとに使用するパラメータと値を判断できるよう、descriptionに日本語に対応する値を記述しました。issueTypeIdはBacklogのスペースによって異なるため前述の前提条件のとおり、確認が必要です。
create_task関数
functionsにcreate_taskという名前の関数を定義します。summary
,projectId
,issueTypeId
などBacklog APIのパラメーター名を定義し、それぞれどのような値が入るかをdescriptionに記述します。期日に関するパラメータは日付のフォーマットが決まっているため、format
とexample
を用いて例を示します。Backlog APIにはprojectIdを渡す必要があります。課題登録を依頼するポストではAPPプロジェクトに課題を登録してください。
としたため、プロジェクトキーAPP
をprojectIdに変換する必要があります。そのため、projectId{}のdescriptionでprojectIdとAPPの対応を補足しました。このような補足も自然言語で記述でき、OpenAI APIが読み取って内容を判断してくれます。
functions = [
{
# 課題を登録する関数
"name": "create_task",
"description": """
Create tasks using a Backlog task management system.
""",
# Backlog APIに合わせてパラメータを定義する
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"example": "〇〇を××する",
"description": " 件名だけ読めば「何を」「どうすれば」タスクが終わるのかわかる件名を付ける。",
},
"projectId": {
"type": "number",
"description": """
課題を登録するプロジェクトのID
APPプロジェクトの場合、projectIdは 129821
""",
},
"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",
],
},
}
]
LLMからのレスポンス
前述の課題登録を依頼するポストのメッセージとfunctionsをOpenAI APIに渡すと、LLMから以下のレスポンスが得られました。name='create_task'
とあるように、LLMによってcreate_taskが選択され、課題を登録する依頼に記述した内容がそれぞれパラメータに当てはめられているのが分かります。アプリケーション開発工程10項を作成してください。
と指示した内容も反映されています。(descriptionには実際は\nが出力されていますが、表記上分かりやすくするため改行をいれました。)
ChatCompletionMessage(
content=None,
role='assistant',
function_call=FunctionCall(
arguments='{
"summary": "開発工程のタスク作成",
"projectId": 129821,
"description":
"|タスク|解説|
|---|---|
|要件定義|要件定義を行う|
|基本設計|基本設計を行う|
|詳細設計|詳細設計を行う|
|実装|機能を実装する|
|単体テスト|単体テストを実施する|
|結合テスト|結合テストを実施する|
|システムテスト|システムテストを実施する|
|ユーザーテスト|ユーザーテストを実施する|
|テスト結果分析|テスト結果を分析する|
|リリース|本番環境にリリースする|
## プロジェクトの概要
新規にXXXを実現する機能を実装する。
テストユーザーによるテストを行い、不具合がないことを確認した後、本番環境にリリースする。
## 目的
ユーザーからのフィードバックによると、XXXができないことが課題として挙げられている。
この課題を解決するため、XXXを実現する機能を実装する。
## 完了条件
- テストユーザーによるテストを行い、不具合がないこと
- 本番環境へのリリースを行い、ユーザーからの不具合報告がないこと
## 数値目標
- テストユーザーによるテストにおいて、軽微なバグの件数を10件以下にする
- テストユーザーによるテストにおいて、重大なバグの件数を0件にする
- リリース後、1週間以内にユーザーからの不具合報告が0件になる",
"issueTypeId": 624226,
"startDate": "2023-12-05",
"dueDate": "2024-04-05",
"priorityId": 2
}',
name='create_task'),
tool_calls=None
)
ChatCompletionMessage内のargumentsの内容をPython関数のcreate_task()に渡しBacklog APIにリクエストを送信します。そして、課題を登録する依頼のポストとBaclog APIからのレスポンスを合わせてLLMに渡し、ボットの返信メッセージを作成します。
課題情報の更新
つづいて、課題の更新です。Backlog APIの課題情報の更新のリクエストパラメーターをもとにupdate_task関数を追加します。課題の追加とほぼ同じですが、更新対象を特定するためにissueKeyパラメータを定義しています。
update_task関数
functions = [
{
# 課題を更新する関数
"name": "update_task",
"description": """
Update tasks using a Backlog task management system.
""",
"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
""",
},
},
},
}
]
LLMからのレスポンス
前述の課題情報の更新を依頼するポストのメッセージとfunctionsをOpenAI APIに渡すと、LLMから以下のレスポンスが得られました。name='update_task'
とあるように、LLMによってupdate_taskが選択され、課題情報の更新依頼に記述した内容がそれぞれパラメータに当てはめられているのが分かります。
ChatCompletionMessage(
content=None,
role='assistant',
function_call=FunctionCall(
arguments='{
"issueKey": "APP-326",
"summary": "アプリケーションに新規機能とヘルプページを追加する",
"issueTypeId": "624224",
"priorityId": "3",
"dueDate": "2023-12-27"
}',
name='update_task'
),
tool_calls=None
)
ChatCompletionMessage内のargumentsの内容をPython関数のupdate_task()に渡しBacklog APIにリクエストを送信します。そして、課題情報の更新を依頼するポストとBaclog APIからのレスポンスを合わせてLLMに渡し、ボットの返信メッセージを作成します。
まとめ
OpenAI APIのFunction callingを使用すると、自然言語による入力に応じた関数を呼び出しやBacklog APIの実行に必要なパラメータの組み立てを簡単に実装することができました。functions内のパラメータ定義でも優先度が'低'の場合、 PriorityId は 4
といった記述で条件分岐やパラメータと値の紐付けが行えるため、API連携の実装が簡単に行えました。
課題の登録を依頼するポストでは、以下のように開発工程とは
と例示したことで安定して意味のある出力が得られました。この例示を省略した場合、"項目1","項目2"...といった無意味な出力となる場合があります。Backlog課題を一定のフォーマットで記述できるよう、LangChainのtemplateと組み合わせたり出力フォーマットを指定するなどの工夫でより精度の高い出力が得られると思います。
課題の本文には、アプリケーション開発工程10項目を以下の制約条件と出力形式に従って作成してください。
開発工程とは、例えば要件定義や基本設計などです。
一方で期待したとおりの出力が得られない点もありました。ポストしたプロンプトに現在時刻をタイムゾーン付きで追加しています。そのため、期限日は1週間後です。
とすることで、課題の期日が登録日の1週間後に設定されます。しかし、期限日は明明後日(しあさって)です。
とすると、なぜか明後日の日付が出力されました。gpt-3.5-turbo-1106、gpt-4-1106-preview、ChatGPT 3.5のいずれも誤った結果を出力したため、現時点の言語モデルが明明後日
を正しい日付に変換できないのかもしれません。
ほかにも、課題登録を依頼するプロンプトにはプロジェクトの概要
や目的
などを含めていますが、Backlog APIにそれらが渡されない場合がありました。
今回のアプリケーションでは自然言語をもとにBacklog課題を操作することができました。これを応用して、課題やWikiの要約を依頼することもできそうです。OpenAIのSpeech to textを利用して音声を文字起こしした結果からBacklogを操作し、その結果をText to speachを利用して音声で応答することもできそうです。