このハンズオンではOpenAIのサービス「ChatGPT」のAPIを自前のアプリケーションに組み込むことでAIアプリケーション開発を体験していただく内容となっています。今回は手軽にChatGPTに組み込むアプリケーションの題材としてLINE botに導入することで自分だけのAIチャットボットを作る体験をしていきます。
なお、開発環境はすべてブラウザ上で完結するのでお使いのパソコンで手軽に体験していただけます。
本ハンズオンは以下の構成になっております。
Step1ではOpenAIのAPIキーを取得してAPIからChatGPTとやり取りをしてみます。
Step2ではLINEbotを動かすための設定を進めていきます。
Step3ではStep1とStep2をあわせてChatGPTを使えるLINEbotを構築していきます。
Step4では応用編として、6/13にリリースされた新機能である「Function Calling」を使ってChatGPTの機能を拡張していきます。
次のページから早速ハンズオンを進めていきましょう。
今回は簡単に環境構築をできるようにGitpodというサービスを使用してオンラインの開発環境を構築します。以下のリポジトリにアクセスします。
https://github.com/Miura55/chatgptbot-handson
レポジトリの下部にあるREADMEにある Open in Gitpod
をクリックすることで今回のハンズオンの開発を構築します。
以下のようにオンラインのIDEが起動されたら開発環境構築は完了です。
ここまでできたらハンズオンの準備は完了です。次のステップでからハンズオンを進めていきましょう。
https://platform.openai.com/ を開き、View API keys
を開きます。
APIキーを作成してない場合は、Create new secret keys
をクリックしてAPIキーを新規で作成します。
必要があればシークレットキーを管理するための名前を設定します。今回はhandson
という名前を設定します。
APIキーが表示されたら手元にメモしておきます。一度Done
ボタンをクリックして画面を閉じると再度APIキーを確認することができません。
メモしたAPIキーは.envファイルの OPENAI_API_KEY
に記入します。(以下のYOUR_API_KEY
の箇所をご自分のAPIキーに書き換えます)
OPENAI_API_KEY=YOUR_API_KEY
実際にAPIを実行してみます。まずはコマンドラインからAPIを実行してAPIを動かしてみます。
Gitpodのターミナルで以下のコマンドを実行します。YOUR_API_KEY
の箇所はご自分のAPIキーに書き換えます。
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "こんにちは!"}]
}'
ターミナルに貼り付けるときに以下のダイアログが表示されたらコピペを許可してもらっていいです。
APIを実行して以下のようにリクエストしたメッセージに対する返答がレスポンスで返ってきたらAPIは正常に実行されています。
{
"id": "chatcmpl-ExampleId",
"object": "chat.completion",
"created": 1686153797,
"model": "gpt-3.5-turbo-0301",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 8,
"total_tokens": 18
},
"choices": [
{
"message": {
"role": "assistant",
"content": "こんにちは!お元気ですか?"
},
"finish_reason": "stop",
"index": 0
}
]
}
Chat GPTをAPIから動かせたところで次のページからChat GPTのAPIを使ったアプリケーションを作っていきましょう
続いてはアプリケーションにChatGPTのAPIを組み込んでみます。
今回はLINE上で動くAIチャットボットを作っていきます。
まずはLINE botを動かすためには「Messaging API」を利用します。ここではそのMessaging APIを接続するために必要な接続情報を用意します。
以下のドキュメントを参考にLINE DevelopersコンソールでMessaging APIのチャネルを作成します。
https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console
チャネルを作成したらMessaging APIの接続情報を取得します。
作成したチャネルのページの「チャネル基本設定」タブを選択して、画面下部にある「チャネルシークレット」をコピーします。
コピーするときは、チャネルシークレットの右端にあるクリップボードアイコンをクリックするとコピーすることができます。
コピーしたチャネルシークレットは.envファイルの LINE_CHANNEL_SECRET
に記入します。(YOUR_CHANNEL_SECRET
の箇所をご自分のチャネルシークレットキーに書き換えます)
LINE_CHANNEL_SECRET=YOUR_CHANNEL_SECRET
次に「Messaging API設定」タブを選択して、画面下部にある「チャネルアクセストークン」に移動します。
チャネル作成時にはチャネルアクセストークンが生成されてない状態なので、「発行」ボタンをクリックしてチャネルアクセストークンを発行します。
チャネルアクセストークンが発行されたら.envファイルの LINE_CHANNEL_ACCESS_TOKEN
に記入します。( YOUR_CHANNEL_ACCESS_TOKEN
の箇所をご自分のアクセストークンに書き換えます。)
LINE_CHANNEL_ACCESS_TOKEN=YOUR_CHANNEL_ACCESS_TOKEN
今回はGitpod上でLINE bot用にサーバーを立ち上げます。まずはサーバーを立ち上げて動くかどうかを確認してみます。
サーバーはGitpodのターミナル上で以下のコマンドを実行すると立ち上がります。
uvicorn app:app --reload --host 0.0.0.0 --port 5000
起動後、GitpodのIDE下部にある「ports」タブをクリックして、ポート番号が5000番の行にある鍵のアイコンをクリックしてURLを外部に公開します。これによりLINEのプラットフォームから現在起動中のLINE botのサーバーにアクセスできるようになります。
左側にあるクリップボードアイコンをクリックしてパブリックになったサーバーのURLをコピーして手元にメモしておきます。このURLがLINE botを接続するために必要なWebhook URLのベースになります。
LINE Developerコンソールに移動して「Messaging API設定」に移動し、「Webhook設定」のWebhook URLに「先程コピーしたGitpodのURL+/callback」の形でURLを設定します。
設定を終えたら「検証」ボタンをクリックしてLINE botを動かすサーバーに接続できるか確認します。
検証ボタンをクリックして以下のダイアログが表示されたらサーバーに正常に接続できています。もしそれ以外のエラーが出た場合は設定したWebhook URLが正しいかどうかや、URLがパブリックに設定されているかをご確認ください。
LINE botとやり取りをするために更に設定を行います。「Messaging API設定」タブにある「LINE公式アカウント機能」の項目にある「応答メッセージ」の項目にある「編集」をクリックします。
応答メッセージの項目を無効にします。こうすることでLINE botのサーバーとやり取りをできるようになります。無効に設定をすると自動で保存されます。
これでLINE botを動かす準備ができました。スマホのLINEアプリのQRコードリーダーからLINE Developerコンソールの「Messaging API設定」タブにあるQRコードを読み取って友だち追加します。
友だち追加したらトーク画面に移動して任意のメッセージを送信します。以下のように送信したメッセージがbotから返ってきたらオウム返しボットは正常に動作しています。もし何も返ってきてない場合はアクセストークンが正しいか確認をしてください。(Gitpod側のターミナルにも何かエラーが出てないか確認をするとエラーの原因がわかることもあります。)
ここまでできたらLINE botが動かせている状態です。次のステップではいよいよChatGPTと連携するLINE botを作っていきましょう。
ここではLINE botでChatGPTのAPIを呼び出してAIチャットボットを作っていきます。
先程オウム返しに使ってた handle_message
関数を以下のコードに書き換えます。
@handler.add(MessageEvent, TextMessage)
def handle_message(event):
# ChatCPTのAPIを叩く
response = openai.ChatCompletion.create(
model='gpt-3.5-turbo-0613',
messages=[
{
"role": "system",
"content": "日本語で応答してください"
},
{
"role": "user",
"content": event.message.text
}
]
)
# ChatCPTの応答をLINEに返す
chat_response = response['choices'][0]['message']['content']
res_message = TextSendMessage(text=chat_response)
linebot.reply_message(event.reply_token, res_message)
プログラムを書き換えたら「Ctrl + S(Macの方はCmd + S)」で保存をします。保存をすると自動で更新がかかるので、ボットにメッセージを送信できる状態になります。
それでは実際にメッセージを送ってみます。トーク画面で何かメッセージを送って以下のようにChatGPTからの返答が返ってきたらボットは正常に動作しています。
これでChatGPTが動作するLINE botができました。ChatGPTがとても優秀なのはお分かりいただけるかと思いますが、いくつか弱点もあります。
その一つがリアルタイムの情報の回答に対応していないことです。例えば以下のように明日の天気を聞いても回答できないと返答されます。
この問題を解決する方法として、2023年6月13日にリリースされた新機能「Function calling」を使う方法があります。
https://openai.com/blog/function-calling-and-other-api-updates
この機能は簡単に言うとChatGPTの機能拡張を開発者が簡単に実装できるというものです。この機能を使えば天気のようなリアルタイムの情報の回答にも対応できるようになります。
というわけで次のステップでは応用編としてこのFunction callingという機能を使って指定した場所の天気をChatGPTが回答してくれるようにしていきます。
このステップでは、外部のAPIと連携してChatGPTの機能を拡張する体験をしていきます。そのサンプルとして、ここでは天気APIを使って指定した場所の明日の天気を解答する仕組みを作っていきます。
なお、今回はAPI連携の全体像をつかめるように実装は簡易的にしています。
まずはStep3で書き換えたhandle_message関数を削除して、以下の2つの関数をapp.pyの下に追加します。
def ask_weather(location):
import json
import requests
import collections
from datetime import datetime
from geopy.geocoders import Nominatim
# 天気を調べたい都市の緯度と経度を取得する
geolocator = Nominatim(user_agent="ChatGPT")
location = geolocator.geocode(location)
lat = location.latitude
lng = location.longitude
# フリーの天気予報API(Open Metro)を使って天気を取得する
param = {
'latitude': lat,
'longitude': lng,
'current_weather': 'true',
'hourly': ['relativehumidity_2m','precipitation_probability','weathercode','temperature_80m'],
'forcast_days': 1,
'start_date': datetime.now().strftime('%Y-%m-%d'),
'end_date': datetime.now().strftime('%Y-%m-%d'),
'timezone': 'Asia/Tokyo'
}
weather_res = requests.get('https://api.open-meteo.com/v1/forecast', params=param).json()
humidity = max(weather_res['hourly']['relativehumidity_2m'])
precipitation_probability = max(weather_res['hourly']['precipitation_probability'])
max_temperature = max(weather_res['hourly']['temperature_80m'])
min_temperature = min(weather_res['hourly']['temperature_80m'])
weathre_codes = weather_res['hourly']['weathercode']
code_counter = collections.Counter(weathre_codes)
weathre_code = code_counter.most_common()[0][0]
# レスポンスを整形
res = {
'humidity': humidity,
'precipitation_probability': precipitation_probability,
'max_temperature': max_temperature,
'min_temperature': min_temperature,
'weathre_code': weathre_code
}
return json.dumps(res)
@handler.add(MessageEvent, TextMessage)
def handle_message(event):
import json
# ユーザー関数のモデルを定義してメッセージと一緒にリクエストを送ることで関数にアクセスできるか判定する
response = openai.ChatCompletion.create(
model='gpt-3.5-turbo-0613',
messages=[
{
"role": "system",
"content": "日本語で応答してください"
},
{
"role": "user",
"content": event.message.text
}
],
functions=[
{
"name": "ask_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state",
},
},
"required": ["location"],
},
}
],
function_call="auto",
)
chat_response = response['choices'][0]['message']
# function_callを実行できるかどうかを判定
if chat_response.get('function_call'):
logger.info(chat_response)
function_name = chat_response['function_call']['name']
function_arguments = json.loads(chat_response['function_call']['arguments'])
# 関数を呼び出す(ユーザー関数のモデルを元に解析した値を使用する)
location = function_arguments.get('location', '名古屋')
function_response = ask_weather(location)
# 再度メッセージと関数のレスポンスを含めてAPIを実行する
response = openai.ChatCompletion.create(
model='gpt-3.5-turbo-0613',
messages=[
{
"role": "system",
"content": "日本語で応答してください"
},
{
"role": "user",
"content": event.message.text
},
{
'role': 'function',
'name': function_name,
'content': function_response
}
],
)
# ChatCPTの応答をLINEに返す
chat_response = response['choices'][0]['message']['content']
res_message = TextSendMessage(text=chat_response)
linebot.reply_message(event.reply_token, res_message)
かなりコードの量が増えましたが、軽くコードを解説します。LINEで送信したメッセージを受け取ったときに実行するChatGPTのAPIのリクエストボディの中に新たに fuctions
というパラメータを追加しています。
このパラメータは6/13のアップデートで追加された新機能の「Function calling」を実行しています。Function callingを使ってメッセージを返答するにはAPIを2回実行する必要があります。
1回目のAPIの実行では使用する関数名と説明を追加して、入力されたメッセージから取り出す値の説明と型を定義します。
今回のケースであれば天気の情報を取得するといった文脈のメッセージが来たときに地名を取得するようにしています。
functions=[
{
"name": "ask_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state",
},
},
"required": ["location"],
},
}
],
1回目のAPIを実行したときのレスポンスのJSONの message
の値は以下の通りです。
メッセージを解析して取得した値は function_call.arguments
にまとめて入っていますが、逆に該当する値をメッセージから取得できない場合は function_call
自体レスポンスに存在しません。
Pythonであればこのargumentsの値を json.loads()
で読み込んで続きの処理を実行すれば問題ないです。
{
"role": "assistant",
"content": null,
"function_call": {
"name": "ask_weather",
"arguments": "{\n \"location\": \"\u540d\u53e4\u5c4b\"\n}"
}
}
argumentsが存在したときには以下の関数が実行されます。簡単にやっていることを説明すると、指定した場所の経度、緯度を取得してそこからAPIキーなしで使うことができる天気予報APIであるOpen Metroを使って天気を取得して関数の戻り値をJSONに整形しています。
Open MetroのAPIの仕様についてはここでは詳細に解説しないので興味ある方は英語ですが以下のドキュメントが参考になります。
https://open-meteo.com/en/docs
ここで整形したJSONがそのまま2回目のChatGPTのAPIを実行するとき使うので、キーの名前は意味のある命名をすることがポイントです。
def ask_weather(location):
import json
import requests
import collections
from datetime import datetime
from geopy.geocoders import Nominatim
# 天気を調べたい都市の緯度と経度を取得する
geolocator = Nominatim(user_agent="ChatGPT")
location = geolocator.geocode(location)
lat = location.latitude
lng = location.longitude
# フリーの天気予報API(Open Metro)を使って天気を取得する
param = {
'latitude': lat,
'longitude': lng,
'current_weather': 'true',
'hourly': ['relativehumidity_2m','precipitation_probability','weathercode','temperature_80m'],
'forcast_days': 1,
'start_date': datetime.now().strftime('%Y-%m-%d'),
'end_date': datetime.now().strftime('%Y-%m-%d'),
'timezone': 'Asia/Tokyo'
}
weather_res = requests.get('https://api.open-meteo.com/v1/forecast', params=param).json()
humidity = max(weather_res['hourly']['relativehumidity_2m'])
precipitation_probability = max(weather_res['hourly']['precipitation_probability'])
max_temperature = max(weather_res['hourly']['temperature_80m'])
min_temperature = min(weather_res['hourly']['temperature_80m'])
weathre_codes = weather_res['hourly']['weathercode']
code_counter = collections.Counter(weathre_codes)
weathre_code = code_counter.most_common()[0][0]
# レスポンスを整形
res = {
'humidity': humidity,
'precipitation_probability': precipitation_probability,
'max_temperature': max_temperature,
'min_temperature': min_temperature,
'weathre_code': weathre_code
}
return json.dumps(res)
上記の関数の実行結果を元にChatGPTのAPIをもう一度実行します。
ここでは1回目にAPIを実行したときと同様にLINEのメッセージを含めて関数の実行結果の文字列を "role": "function"
として含めてAPIを実行することで関数の実行結果を元にしてChatGPTから返答が出力されます。
messages=[
{
"role": "system",
"content": "日本語で応答してください"
},
{
"role": "user",
"content": event.message.text
},
{
'role': 'function',
'name': function_name,
'content': function_response
}
],
これまで今回のような他のAPIと連携をしたいときにはAPI連携に誘導するためにプロンプト(ChatGPTのやり取りをするときの導入のこと)を工夫する必要がありましたが、function callingを使えばより簡単にシステム連携を実現できるようになります。
システム連携以外にも1回目のAPIの実行だけでメッセージの内容を解析する用途でも使うことができます。
それでは最後の動作確認です。
LINEアプリを開いて「名古屋の天気を教えて」という感じで知りたい場所の天気をbotに尋ねて天気の情報が返信されたら動作確認成功です。
以上でハンズオンの内容はすべて終了です。お疲れ様でした!最後に片付けを説明します。
Gitpodはフリープランであればタイムアウトでインスタンスが止まるだけで請求は来ないのでハンズオンの復習をするために残してもいいですし、削除したい場合は https://gitpod.io/workspaces から削除することができます。
Messaging APIのチャネルも残しても料金が発生するわけではありませんが、削除するときには「チャネル基本設定」タブの一番下にある「チャネルの削除」にある削除ボタンをクリックします。
その後表示されるダイアログにある「LINE Official Account Managerを表示」をクリックしてOfficial Account Managerに遷移します。
Official Account Managerの画面に遷移したらチェックボックスにチェックを入れて「アカウントを削除」をクリックしたらチャネルと公式アカウントが両方削除されます。
今回はChatGPTのAPIを使ったアプリケーション開発をしてきました。ChatGPTはとても便利なサービスで開発者向けのAPIも提供されているところから最近では様々な企業がChatGPTを連携したサービスを提供しています(参考)。
これらのサービスはプロンプトを工夫することで目的の返答を返すようにしていましたが、今回紹介したFunction Callingを使うことでより簡単に目的に合わせたサービスを提供しやすくなります。