はじめに
題名の通り、電話でChatGPTが質問に答えてくれるチャットボットシステムを構築してみました。
電話をかけて質問すると、ChatGPTのAPIを利用し、質問に答えてくれます。音声は、Amazon Connectで用意されているものを使用しています。
下記は、イメージ図になります
こちらは、電話をかけた時の動画になります
動画では、ChatGPTのレスポンスに時間がかかっているように思えます。
レスポンス時間を短縮する方法をブログ化しましたので、今回の記事を読んだ後に、参考にしてください。
構成図は、以下になります
構成図については、Connectのコンタクトフロー内で、Lexで質問内容を受け取り、音声から文字起こしされ(裏でAmazon Transcribeが利用)、Lambdaが文字起こしされた質問テキストをChatGPT APIにリクエストします。
レスポンス内容をLexに渡し、質問内容に答える流れになります。
下記の順番で構築方法を記載します。
- Lambdaを作成
- Lexを構築
- Connectのコンタクトフローを作成
Lambdaを作成
Lambdaでは、OpenAIのAPIキーやPythonで使用するopenaiライブラリが必要になります。
今回使用するコード以外は、下記の記事通りに作成しますので、ご確認ください。
Lambdaのコード
import json
from decimal import Decimal
import os
import openai
openai.api_key = os.environ['API_Key']
API_ENDPOINT = 'https://api.openai.com/v1/engine/davinci-codex/completions'
SLOT_DUMMY = {
"question_slot": {
"shape": "Scalar",
"value": {
"originalValue": "ダミー",
"resolvedValues": [
"ダミー"
],
"interpretedValue": "ダミー"
}
}
}
def decimal_to_int(obj):
if isinstance(obj, Decimal):
return int(obj)
def elicit_slot(slot_to_elicit, intent_name, slots):
return {
'sessionState': {
'dialogAction': {
'type': 'ElicitSlot',
'slotToElicit': slot_to_elicit,
},
'intent': {
'name': intent_name,
'slots': slots,
'state': 'InProgress'
}
}
}
def confirm_intent(message_content, intent_name, slots):
return {
'messages': [{'contentType': 'PlainText', 'content': message_content}],
'sessionState': {
'dialogAction': {
'type': 'ConfirmIntent',
},
'intent': {
'name': intent_name,
'slots': slots,
'state': 'Fulfilled'
}
}
}
def close(fulfillment_state, message_content, intent_name, slots):
return {
'messages': [{'contentType': 'PlainText', 'content': message_content}],
"sessionState": {
'dialogAction': {
'type': 'Close',
},
'intent': {
'name': intent_name,
'slots': slots,
'state': fulfillment_state
}
}
}
def get_openai_response(input_text):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": input_text},
]
)
print("Received response:" + json.dumps(response,default=decimal_to_int, ensure_ascii=False))
return response["choices"][0]["message"]["content"].replace('\n', '')
def ChatGPT_intent(event):
print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))
intent_name = event['sessionState']['intent']['name']
slots = event['sessionState']['intent']['slots']
input_text = event['inputTranscript']
if slots['question_slot'] is None:
return elicit_slot('question_slot', intent_name, SLOT_DUMMY)
confirmation_status = event['sessionState']['intent']['confirmationState']
if confirmation_status == "Confirmed":
return close("Fulfilled", 'それでは、電話を切ります', intent_name, slots)
elif confirmation_status == "Denied":
return close("Fulfilled", 'お力になれず、申し訳ありません。電話を切ります', intent_name, slots)
# confirmation_status == "None"
response_text = get_openai_response(input_text)
print("Received response_text:" + response_text)
return confirm_intent(
f'それでは、回答します。{response_text}。以上が回答になります。回答に納得したかたは、はい、とお伝え下さい。納得いかない場合、いいえ、とお伝え下さい',
intent_name, slots)
def lambda_handler(event, context):
print("Received event:" + json.dumps(event, default=decimal_to_int, ensure_ascii=False))
intent_name = event['sessionState']['intent']['name']
if intent_name == 'chatgpt':
return ChatGPT_intent(event)
コードの中身の補足をします。
- 変数名:
SLOT_DUMMY
- Lexでは、スロットに値を入れる必要があります。ただし今回、質問をする内容は、何でも聞けるようにするために、スロットにダミーの値を埋めるために、この変数を使います
- 関数名:
elicit_slot
- Lexのスロットが埋まっていない場合に使用します。
- スロットに設定しているプロンプト(Lexが話す内容)を聞きます。今回の場合、プロンプトは「質問内容を教えて下さい」になります
- Lexのスロットが埋まっていない場合に使用します。
- 関数名:
confirm_intent
- Lexのスロットが全て埋まった時に使用します。
- 確認プロンプトに設定しているプロンプトを聞きます。今回の場合、「それでは、回答します。.......」になります
- Lexのスロットが全て埋まった時に使用します。
- 関数名:
close
- 確認プロンプトの後、インテントを終了するときに使用します。
- クローズ時に設定しているプロンプトを聞きます。今回の場合、「それでは、電話をきります」になります
- 確認プロンプトの後、インテントを終了するときに使用します。
- 関数名:
get_openai_response
- APIからのレスポンスのうち、ChatGPTの返答をテキストで取得します。
text-davinci-003を使用した場合のコード
「gpt-3.5-turbo」ではなく、GPT-3.5系のモデルの一つである「text-davinci-003」のAPIを利用したい場合、get_openai_response
関数を以下に修正すると、使えます。
def get_openai_response(input_text):
response = openai.Completion.create(
engine="text-davinci-003",
prompt=input_text,
max_tokens=200
)
print("Received response:" + json.dumps(response,default=decimal_to_int, ensure_ascii=False))
return response.choices[0].text.replace('\n', '')
Lexを構築
Lexの構築をするため、ボットを作成し、インテントとインテントで使用するカスタムスロットタイプを作成します。
ボットの作成
- Lexのコンソール画面から、[ボットの作成]をクリックします。
- [空のボットを作成します]を選択し、適当なボット名を記載し、下記画像の通りに入力します。
- 日本語を選択し、[完了]をクリックします。
これでボットが作成できました。
ボットで使用するLambdaの紐付け
Lambdaの紐付け設定は、分かりにくいのですが、[エイリアス]→[対象のエイリアス]→[言語:Japanese (Japan)]を順にクリックすると、紐付ける設定画面がでます。
先程作成したLambdaを設定し、保存します。
カスタムスロットタイプ作成
ダミー用のカスタムスロットタイプを作成します。
スロットタイプの追加の[空のスロットタイプを追加]をクリックします。
スロットタイプ名をdummyにし、適当な値をいれます。名前も値は、何でもよくdummyでなくてもよいです。
インテントの作成
インテント名とサンプル発話を設定します。
インテント名は、chatgptにしました。インテント名を変更する場合、Lambdaのコードも変更しましょう。
サンプル発話は、このインテントがトリガーされるフレーズになります。
今回は、「質問」と設定したので、電話で「質問」と言うと、このインテントがトリガーされます。
スロットは以下のように設定します
- スロット名は、question_slot
- Lambdaでも使用するため、スロット名を変更する場合、Lambdaのコードも変更してください。
- スロットタイプは、先程作成したカスタムスロットタイプを指定
- プロンプトは、「質問内容を教えて下さい」
コードフックアクションは、有効にします。
有効にすることで、インテント使用中にLambdaが常に実行されます。
他は、デフォルトの設定でかまいません。
これで、Lexを保存し、Buildすると、インテントが使えるようになります。
Connectのコンタクトフローを作成
Connectインスタンスの作成や電話番号の取得方法などは、下記の記事を参考にしてください。
今回は、コンタクトフローの作成のみをご説明します。
LexとLambdaをConnectインスタンスに紐付ける
AWSマネジメントコンソールのConnect画面から[問い合わせフロー]を選択し、作成したLexとLambdaを追加します。
これで紐付け完了です。
コンタクトフロー
コンタクトフローは、下記の図の通りに作成します。
[音声の設定]のフローは、Mizukiを選択しました。
[顧客の入力を取得する]のフローは、以下の通りに設定しました。
- テキスト読み上げまたはチャットテキスト
- チャットジーピーティーにご質問があるかたは、質問、とお伝え下さい
- Lex ボットを選択
- 先程作成したボットを選択します。
- インテント
- 先程作成したインテントを記載します。
- 先程作成したインテントを記載します。
コンタクトフローは完成です。
これで、冒頭の動画の通り、電話をかけて、質問すると、ChatGPTが答えてくれます。