Ruby で ChatGPT API を触る(Stream 編)
2023/04/25
前回、Ruby で ruby-openai gem を使って、ChatGPT API を触ってみた
今回は ChatGPT の Web 版のように返答を少しずつ表示するやつをやってみる
ruby-openai はまだ Stream には対応していないので、Faraday を使って実装した
完成物
コード
app.rb
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'faraday'
end
url = 'https://api.openai.com'
openai_api_key = 'APIキー'
headers = { Authorization: "Bearer #{openai_api_key}" }
faraday = Faraday.new(url: url, headers: headers) do |conn|
conn.request :json
end
params = {
model: 'gpt-3.5-turbo',
messages: [
{ role: 'user', content: 'こんにちは!' }
],
stream: true
}
faraday.post('/v1/chat/completions') do |req|
req.body = params.to_json
req.options.on_data = Proc.new do |chunk|
chunk.split('data: ')[1..].each do |data|
text = JSON.parse(data)
choice = text['choices'][0]
return if choice['finish_reason'] == 'stop'
print choice['delta']['content']
end
end
end
実行結果
補足
chunk の中身
req.options.on_data = Proc.new do |chunk|
p chunk
end
data: JSON
の形で返ってくる
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xEF\xBC\x81\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE7\xA7\x81\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xAF\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"AI\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xAE\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x82\xA2\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x82\xB7\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x82\xB9\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x82\xBF\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x83\xB3\xE3\x83\x88\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xA7\xE3\x81\x99\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x80\x82\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE4\xBD\x95\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x8B\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x8A\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE6\x89\x8B\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE4\xBC\x9D\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x84\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xA7\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x8D\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x82\x8B\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x93\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xA8\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x8C\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x82\xE3\x82\x8A\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\xBE\xE3\x81\x99\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xE3\x81\x8B\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{\"content\":\"\xEF\xBC\x9F\"},\"index\":0,\"finish_reason\":null}]}\n\n"
"data: {\"id\":\"chatcmpl-78sf0vt4xuqm6MnLtqEBOA0WqeQ9I\",\"object\":\"chat.completion.chunk\",\"created\":1682351094,\"model\":\"gpt-3.5-turbo-0301\",\"choices\":[{\"delta\":{},\"index\":0,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n"
最後の chunk
最後の chunk には data: [DONE]
が来るので JSON.parse
すると当然コケる
そのため、finish_reason: 'stop'
が含まれている1つ前の chunk で止めるようにしている
return if choice['finish_reason'] == 'stop'