高木のブログ

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

実行結果

001.gif

補足

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'

参考


SNS でシェアする


ytkg

Written by ytkg, Twitter, GitHub