ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

ruby-openai で OpenAI / Azure OpenAI Service を使ってみる

みなさんこんにちは、ユニファの赤沼です。 この記事は Unifa Advent Calendar 2023 の2日目の記事です。

adventar.org

ルクミーでは ChatGPT を活用した機能の提供を進めています。最近では多くの ChatGPT 活用事例が世に出ており、サンプルコードなども豊富になっていますが、大半は Python によるものかと思います。 弊社ではサーバサイドのコードは大半が Ruby で書かれていまして、 OpenAI の API 利用も Ruby のコードから ruby-openai gem を使っていますが、Ruby によるサンプルコードはまだあまりないようでした。Azure OpenAI Service の API を利用するサンプルはさらに少ないという状況でしたので、 ruby-openai gem を使って OpenAI API や Azure OpenAI Service の API にアクセスするためのサンプルコードを紹介したいと思います。

github.com

OpenAI API へのアクセス

まずは OpenAI API(not Azure) へアクセスするケースです。

Gemfile

Gemfile に下記を記載して bundle install を実行します。

gem "ruby-openai"

API KEY の設定

OpenAI の API KEY を取得して、ハードコードを避けるために環境変数に設定します。ここまでで最低限の下準備は完了です。

$ export OPENAI_API_KEY="sf-XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

ChatCompletion

まずはシンプルに ChatCompletion API を使ってみます。会話の履歴が存在すると想定して下記のように書いて実行します。

公式の Python のライブラリだと OPENAI_API_KEY という環境変数に API KEY が設定されていれば勝手に使ってくれますが、 ruby-openai では明示的に指定する必要があるためインスタンス生成時に環境変数から読み出して設定しています。

require "json"
require "openai"

openai = OpenAI::Client.new(
    access_token: ENV.fetch("OPENAI_API_KEY")
)

response = openai.chat(
  parameters: {
    model: "gpt-3.5-turbo",
    messages: [
      {role: "system",    content: "You are a helpful assistant."},
      {role: "user",      content: "こんにちは!私はジョンと言います"},
      {role: "assistant", content: "こんにちは、ジョンさん!なんでもお手伝いいたします。どのようなお力添えができますか?"},
      {role: "user",      content: "私の名前がわかりますか?"}
    ]
  }
)

puts JSON.pretty_generate(response)

上記のコードを実行すると下記のようなレスポンスが返り、 choices[0].message.content に過去の会話内容を踏まえた回答の内容が含まれています。

{
  "id": "chatcmpl-8PnFIg1XwnH8uZS4MZofm43FyMy92",
  "object": "chat.completion",
  "created": 1701158192,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "はい、おっしゃった通り、ジョンさんですね。先ほどお名前を教えていただいたので、覚えていますよ。どのようなご質問やご要望がありますか?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 81,
    "completion_tokens": 66,
    "total_tokens": 147
  }
}

Streaming Chat

次にストリーミングで表示する方法です。基本的な内容は先程のケースと同様ですが、 stream パラメータに proc でブロックを渡し、ストリーミングの chunk が返される度に実行する内容を指定しています。

require "openai"

openai = OpenAI::Client.new(
    access_token: ENV.fetch("OPENAI_API_KEY")
)

openai.chat(
    parameters: {
        model: "gpt-3.5-turbo",
        messages: [
            {role: "system", content: "You are a helpful assistant."},
            {role: "user",   content: "自己紹介してください。"}
        ],
        stream: proc do |chunk, _bytesize|
            print chunk.dig("choices", 0, "delta", "content")
        end
    }
)

これを実行すると下記のような内容が徐々に表示されていきます。

こんにちは!私はあなたの助手です。私の名前はAIアシスタントといいます。私はさまざまなタスクをサポートすることができます。例えば、質問に答えたり、情報を検索したり、リマインダーを設定したり、日程を管理したり、翻訳を行ったりすることができます。私は常に学習して進化しているので、どんどん質問や指示をしてください。私の目標は、あなたの日常生活をスムーズにすることです。どうぞお気軽にご利用ください!

Function Calling

次に Function Calling も試してみます。まずは Function Calling で呼び出すメソッドを用意しておきます。メソッドの中身はダミーなので固定の内容を返します。

SaaS のサービス名称とカテゴリを渡すとそのサービスを提供している会社の情報を返してくれるという体のメソッドです。

require "json"
require "openai"

def get_saas_provider_info(service_name:, category: "childcare")
  provider_info = {
    service_name: service_name,
    company_name: "Unifa",
    category:     "childcare",
    tag:          [:startup, :iot]
  }

  return JSON.pretty_generate(provider_info)
end

メソッドを独自に用意しているので、 ChatGPT がどのメソッドを使うか判断するための情報を下記の様に定義しておきます。

内容としては、メソッド名、メソッド内容の説明、メソッド引数の説明 となります。

functions = [
  {
    name: "get_saas_provider_info",
    description: "Retrieves information about the company providing a specified Software as a Service.",
    parameters: {
      type: "object",
      properties: {
        service_name: {
          type: "string",
          description: "The name of the SaaS service for which to retrieve the provider's information."
        },
        category: {
          type: "string",
          description: "The category of the SaaS service (e.g., CRM, accounting, project management)."
        }
      },
      required: ["service_name"]
    }
  }
]

上記の情報を使用して ChatCompletion API を呼び出します。

functions パラメータで上記の情報を渡します。

openai = OpenAI::Client.new(
    access_token: ENV.fetch("OPENAI_API_KEY")
)

messages = [
  {role: "user", content: "ルクミーを提供している会社はどこですか?"}
]

response = openai.chat(
  parameters: {
    model:     "gpt-3.5-turbo",
    messages:  messages,
    functions: functions
  }
)

response_message = response.dig("choices", 0, "message")

response_message には下記のような内容が格納されます。

{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "get_saas_provider_info",
    "arguments": "{\n  \"service_name\": \"ルクミー\"\n}"
  }
}

続いて実行可能な機能のリストを Hash として用意しておきます。今回はダミーメソッドのみ定義しておきます。

先程の response_message から機能の名前を取り出し、上記の Hash から該当する機能を取得して、 これも response_message から取り出したメソッドのパラメータとともに該当の機能を実行します。

available_functions = {
  "get_saas_provider_info" => method(:get_saas_provider_info)
}

function_name    = response_message.dig("function_call", "name")
function_to_call = available_functions[function_name]
function_args    = JSON.parse(response_message.dig("function_call", "arguments"))

function_response = function_to_call.call(
  service_name: function_args.dig("service_name"),
  category:     function_args.dig("category")
)

function_response の内容は下記のようになります。

{
  "service_name": "ルクミー",
  "company_name": "Unifa",
  "category": "childcare",
  "tag": [
    "startup",
    "iot"
  ]
}

ここまでの内容を元に再度 ChatCompletion API をコールします。

messages << response_message
messages << {
  role:    "function",
  name:    function_name,
  content: function_response
}

second_response = openai.chat(
  parameters: {
    model:    "gpt-3.5-turbo",
    messages: messages
  }
)

puts second_response.dig("choices", 0, "message", "content")

最終的に下記のような内容が出力されます。

ルクミーを提供している会社はUnifaです。

Azure OpenAI Service API へのアクセス

ここからは同様の内容を Azure OpenAI Service API を使って実行してみます。

環境変数の設定

まずは環境変数に下記のように API KEY とアクセス先のエンドポイントのベース、APIのバージョンを設定します。

エンドポイントにはモデルのデプロイ名(下記の例では gpt-35-turbo )まで含む形になります。

$ export AZURE_OPENAI_API_KEY="XXXXXXXXXXXXXXXXXXXXXXXX"
$ export URI_BASE="https://XXXXXXXXXXX.openai.azure.com/openai/deployments/gpt-35-turbo/"
$ export API_VERSION="2023-07-01-preview"

ChatCompletion

ChatCompletion API の使い方としては、まず Client の初期化時に渡す情報が増えます。

access_token 以外の情報も設定するために configure メソッドを使ってパラメータを設定します。

そして OpenAI の場合は chat メソッドの parameters で model を指定していましたが、エンドポイントの情報にモデルが含まれているので、model の指定は不要になっています。

require "json"
require "openai"

OpenAI.configure do |config|
  config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY")
  config.uri_base     = ENV.fetch("URI_BASE")
  config.api_type     = :azure
  config.api_version  = ENV.fetch("API_VERSION")
end

openai = OpenAI::Client.new

response = openai.chat(
  parameters: {
    messages: [
      {role: "system",    content: "You are a helpful assistant."},
      {role: "user",      content: "こんにちは!私はジョンと言います"},
      {role: "assistant", content: "こんにちは、ジョンさん!なんでもお手伝いいたします。どのようなお力添えができますか?"},
      {role: "user",      content: "私の名前がわかりますか?"}
    ]
  }
)

puts JSON.pretty_generate(response)

これを実行すると下記のようなレスポンスが返ります。

choices[0].message.content に過去の会話内容を踏まえた回答の内容が含まれるのは同様ですが、 prompt_filter_results という情報が追加されています。これは Azure が提供している有害なコンテンツを検出するためのフィルタリング処理の結果が記載されているもので、今回はいずれも "filtered": false "severity": "safe" なので問題ないコンテンツということになります。

{
  "id": "chatcmpl-8PpiWb5pAtKNC2kNrDb3Y8naJ4xT1",
  "object": "chat.completion",
  "created": 1701167692,
  "model": "gpt-35-turbo",
  "prompt_filter_results": [
    {
      "prompt_index": 0,
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "はい、先ほどおっしゃった通り、あなたの名前はジョンさんですね。ご安心ください、お名前は覚えました。"
      },
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "usage": {
    "prompt_tokens": 81,
    "completion_tokens": 46,
    "total_tokens": 127
  }
}

Streaming Chat

次にストリーミングです。こちらも先程と同様に違いは初期化時に渡す情報と、 chat メソッドで model の指定が不要という点です。

require "openai"

OpenAI.configure do |config|
  config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY")
  config.uri_base     = ENV.fetch("URI_BASE")
  config.api_type     = :azure
  config.api_version  = ENV.fetch("API_VERSION")
end

openai = OpenAI::Client.new

openai.chat(
  parameters: {
    messages: [
      {role: "system", content: "You are a helpful assistant."},
      {role: "user",   content: "自己紹介してください。"}
    ],
    stream: proc do |chunk, _bytesize|
      print chunk.dig("choices", 0, "delta", "content")
    end
  }
)

これを実行すると下記のような内容が徐々に表示されていきますが、 OpenAI と比較すると一気に出力されるような感じです。

私はAIアシスタントです。私の目的は、人々の質問や問題に対して助けとなる情報やアドバイスを提供することです。私は幅広いトピックについて知識を持っており、言語、文法、翻訳、時事問題、科学、歴史、エンターテイメントなど、さまざまな分野に関する情報を提供できます。どんな質問でもお気軽にどうぞ。私はあなたのお手伝いをします!

Function Calling

最後に Function Calling も実行してみます。呼び出すメソッドとメソッドの定義情報は OpenAI の場合と同様なので割愛します。

メソッドの情報を使用して ChatCompletion API を呼び出すところは Client の初期化以外は基本的に同様ですが、 Azure OpenAI Service の場合、レスポンスの choices[0].message"content": nil が含まれないようで、後でリクエストを投げる際にそれによってエラーになってしまうため、 "content": nil を設定しています。

OpenAI.configure do |config|
  config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY")
  config.uri_base     = ENV.fetch("URI_BASE")
  config.api_type     = :azure
  config.api_version  = ENV.fetch("API_VERSION")
end

openai = OpenAI::Client.new

messages = [
  {role: "user", content: "ルクミーというSaaSを提供している会社はどこですか?"}
]

response = openai.chat(
  parameters: {
    messages:  messages,
    functions: functions
  }
)

response_message = response.dig("choices", 0, "message")
response_message[:content] = nil if response_message[:content].nil?

ちなみにプロンプトの内容を OpenAI の場合と同じように「ルクミーを提供している会社はどこですか?」とすると、コンテンツフィルターに有害認定されてしまうようで下記のようなエラーになってしまったため、プロンプトを若干変更しています。 (sexualtrue になっていますが何が性的と判断されたかはわかりません。)

 {
  "error": {
    "message": "The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766",
    "type": null,
    "param": "prompt",
    "code": "content_filter",
    "status": 400,
    "innererror": {
      "code": "ResponsibleAIPolicyViolation",
      "content_filter_result": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": true,
          "severity": "medium"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  }
}

最後に再度 ChatCompletion API をコールします。 model の指定が不要という点以外は OpenAI の場合と同様です。

messages << response_message
messages << {
  role:    "function",
  name:    function_name,
  content: function_response
}

second_response = openai.chat(
  parameters: {
    messages: messages
  }
)

puts second_response.dig("choices", 0, "message", "content")

最終的に下記のような内容が出力されます。

ルクミーはUnifaという会社が提供しています。

まとめ

ruby-openai は OpenAI 公式の Python のライブラリと比較してしまうともちろん機能としては追いついていないですが、基本的な処理を実行するだけであれば十分に使えるかと思います。アップデートも継続的に行われているので今後より充実してくるかと思います。

Azure OpenAI Service の利用に当たっては、基本的な使い方は大きく違わないものの、今回試した中でも "content": nil が含まれていなかったり、ストリーミングでの出力の挙動が若干異なるなど気をつけないと引っかかるポイントはそれなりに多そうです。

ユニファではLLMの活用含めて共に保育をHackしてくれる仲間を募集中です!少しでも興味をお持ちいただけた方は、ぜひ一度カジュアルにお話しましょう!

unifa-e.com