6月13日にOpenAIのAPIであるgpt-3.5-turbo
とgpt-4
に更新がありました。
Function Calling
と呼ばれるその機能は非常に革新的であり、これまでのAIプロダクト開発をさらに加速させることが間違いないものでした。
本記事ではその新機能を組み込んだLINE Botを作成し、機能の理解を深めるとともにその有用性を広く拡散したいと思います。
作成したプロダクト
-
こんばんは
のように従来のChatGPTで実施していた会話は問題なく実施できます - Qiitaに関する質問をした際は、QiitaAPIを叩いて情報を取得した上で、その結果に基づいた返答を返します。
本実装のGitHubはこちら
実行環境
Python 3.11.3
MacOS 12.6.6
pyenv-venvで仮想環境を構築requirements.txt
aiohttp==3.8.4
aiosignal==1.3.1
anyio==3.7.0
async-timeout==4.0.2
attrs==23.1.0
certifi==2023.5.7
charset-normalizer==3.1.0
click==8.1.3
fastapi==0.97.0
frozenlist==1.3.3
future==0.18.3
h11==0.14.0
idna==3.4
line-bot-sdk==2.4.2
multidict==6.0.4
openai==0.27.8
pydantic==1.10.9
python-dotenv==1.0.0
requests==2.28.2
sniffio==1.3.0
starlette==0.27.0
tqdm==4.65.0
typing_extensions==4.6.3
urllib3==1.26.16
uvicorn==0.22.0
yarl==1.9.2
- LINE Botを構築する上で、serverはFastAPIで構築しています
ファイル構成
基本的なPython
によるLINE Bot
作成はこちらの記事をお読みください。
.
├── README.md
├── main.py
├── requirements.txt
└── src
└── request_openai.py
サンプルコード
from src.request_openai import make_completion
import logging
import os
from dotenv import load_dotenv; load_dotenv()
from fastapi import FastAPI, Request
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
CHANNEL_SECRET = os.environ.get('CHANNEL_SECRET') or 'CHANNEL_SECRETをコピペ'
CHANNEL_ACCESS_TOKEN = os.environ.get('CHANNEL_ACCESS_TOKEN') or 'CHANNEL_ACCESS_TOKENをコピペ'
app = FastAPI()
line_bot_api = LineBotApi(channel_access_token=CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(channel_secret=CHANNEL_SECRET)
logger = logging.getLogger(__name__)
@app.post("/webhook")
async def callback(request: Request):
signature = request.headers['X-Line-Signature']
body = await request.body()
logger.info("Request body:" + body.decode())
try:
handler.handle(body.decode(), signature)
except InvalidSignatureError:
logger.warning("Invalid signature")
return "Invalid signature"
return "OK"
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
messages=[
{"role": "system", "content": "You are a Qiita website engineer."},
{"role": "user", "content": event.message.text},
]
functions=[
{
"name": "get_tag_info",
"description": "Get the qiita info in a given tag",
"parameters": {
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "Tech word, e.g. Python, JavaScript, React"
},
"unit": {
"type": "string",
"enum": ["followers_count", "items_count"]
}
},
"required": ["tag"]
}
}
]
reply_text = make_completion(messages, functions)
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text = reply_text)
)
ポイント1:functionsを定義する
サンプルコードのようにChatGPTに実施してほしい関数の型のようなものをここで定義します。
import json
import os
from dotenv import load_dotenv; load_dotenv()
import openai; openai.api_key = os.getenv('OPENAI_API_KEY')
import requests
def get_tag_info(tag: str):
try:
res = requests.get(f'https://qiita.com/api/v2/tags/{tag}')
return res.json()
except ValueError as e:
print(e)
def make_completion(messages: list, functions: list):
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
functions=functions
)
if completion.choices[0].finish_reason == 'function_call':
params = json.loads(completion.choices[0].message.function_call.arguments)
tag = params['tag']
function_name = completion.choices[0].message.function_call.name
function = globals()[function_name]
res = function(tag)
add_dict = {"role": "function", "name": "get_tag_info", "content": json.dumps(res)}
messages.append(add_dict)
return make_completion(messages, functions)
else:
return completion.choices[0].message.content
ポイント2:functionsを引数にして、openai.ChatCompletion.create
文脈から判断して、先ほど定義したfunctionsにあった関数について、JSON形式で返してくれます
上にの写真にあるPythonのタグフォロワー数
について聞いている場合、
completion.choices[0].message.function_call.arguments
に
"{\n \"tag\": \"python\",\n \"unit\": \"followers_count\"\n}"
のようなJSONにパース可能な文字列がChatGPTのAPIから返ってきています。
さらに、
completion.choices[0].finish_reason == 'function_call'
として返ってくるので、ここでFunction Callingが実行されていることも明確にわかります。
ポイント3:ChatGPTが生成したJSONを用いてQiitaAPIを叩く
paramas['tag'] = 'Python'
となるようなJSONが返ってきているので、これを用いてQiitaAPIを叩く
ポイント4:QiitaAPIを叩いた結果を合わせて、再度ChatGPTのAPIを叩いて出力された結果をテキストとしてLINEに返す
今回のコードでは make_completion()の中にさらにmake_completion()を定義することで、再帰関数として実装しています。
つまりどういうことか
設定した条件に合致した会話となった場合、あらかじめ設定した関数の名前と引数して使えるJSONデータが返ってきます。今回のコードは、get_tag_info
でQiitaAPIをtagについて叩くわけですが、その引数となるtagの名前にBotのメッセージ送信者のメッセージに基づくJSONデータが生成されて返ってきます。
これを利用することで、自然な会話の流れからAPIを叩いたり、スクレイピングを指示したりすることができます。(スクレイピングさせるときは、トークン量に注意しましょう)
また、functionsがlistで指定していることを考えると、複数のfunctionを指定し、文脈に応じて異なる処理を実行させることができそうです。
例えば、Qiitaについて聞かれた場合はQiitaAPI、食べ物について聞かれた時は食べログのAPIを実行するといった分岐が簡単に実装できるようになりました。
本記事が少しでも皆様の理解の助けとなれば幸いです。
詳細は改めてこちらの記事を参照ください。
以上まとめ
楽しい
追記:シンプルにJSONを返させることも可能
これまで指定した色のRGBをJSONで返してほしいというプロンプトを書いても、前後に余計な文章が入ることが問題でした。
これもFunction Callingで解消できます。
from dotenv import load_dotenv; load_dotenv()
import openai; openai.api_key = os.getenv('OPENAI_API_KEY')
import requests
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=[
{"role": "user", "content": "What is the RGB value of pink"},
],
functions=[
{
"name": "get_rgb_values",
"description": "Get the rgb values of the given color",
"parameters": {
"type": "object",
"properties": {
"red": {
"type": "number",
"description": "value from 0 to 255"
},
"green": {
"type": "number",
"description": "value from 0 to 255"
},
"blue": {
"type": "number",
"description": "value from 0 to 255"
},
},
"required": ["red", "green", "blue"]
}
}
]
)
print(completion)
これに対して、こんな感じで返ってきます。
{
"id": "chatcmpl-7Se3nbb4VJQRwKMYZ0nsDiDhe9Ylt",
"object": "chat.completion",
"created": 1687061531,
"model": "gpt-3.5-turbo-0613",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"function_call": {
"name": "get_rgb_values",
"arguments": "{\n \"red\": 255,\n \"green\": 192,\n \"blue\": 203\n}"
}
},
"finish_reason": "function_call"
}
],
"usage": {
"prompt_tokens": 94,
"completion_tokens": 30,
"total_tokens": 124
}
}
これで、ChatGPTに指定した色の値を作らせて、フルカラーLEDをその色に光らせるということも可能と思われます。