RailsでChatGPT（OpenAI API）を使ってみる

:::note info
**この記事のポイント：**  Ruby on RailsアプリにChatGPTの出力を組み込んでみました。APIを叩けるか試してみるのが主目的で、アプリの機能（使い道）の提案としては大したものではありません。
:::

:::note warn
一部有料の内容を含みます（OpenAI API利用料）
Rails初学者が学習目的で執筆しております。誤り、改善点などぜひご指摘ください
:::

# はじめに

ChatGPTをはじめとするLLM（大規模言語モデル、生成系AI）の発展により、身近なソリューションに様々な変革が起きています。自分は普段Pythonで自然言語関連のタスクを扱うことが多いのですが、最近学習を始めたRuby on Railsにおいても、LLMを組み合わせることで何かおもしろいことができるのではないかと自然に考えが及びましす。

ということで今回はRailsアプリにChatGPTの出力を表示させられるのか、その動線を確かめてみました。

### 参考にしたサイト

[Use ChatGPT API with Ruby on Rails](https://medium.com/@k7aled.el3bady123/use-chatgpt-api-with-ruby-on-rails-50bb736cb975)

# この記事の取り組みの完成品
掲示板アプリの上部にAIで生成したウェルカムメッセージを表示させています。また、その日の誕生日の有名人について紹介させています。実用性については一旦棚上げしています。
![スクリーンショット 2023-12-13 22.06.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/078baa4b-0e76-eb2c-e35c-c7433603a522.png)

---

# 前提

実際はこれも必要ないのですが、ここではベースとしてシンプルな掲示板アプリを予め用意しておきました

- `Boards`コントローラ：`index`,`create`アクションをもつ
- `Board`モデル：`title`,`body`をもつ

---

# 全体のながれ

- OpenAI APIキーを取得する
- Rails credentialにAPIキーを記述する
- OpenAIのAPIを叩く関数を定義する
- Controllerを記述する
- Viewを記述する

---

# OpenAI APIキーを取得する

:::note warn
OpenAI APIは有料です。利用は自己責任でお願いします。
:::

RailsでChatGPTを用いる事前準備として、[OpenAI](https://openai.com/blog/openai-api)社公式サイトにてアカウント登録をし、APIキーを取得します。APIキーは他人に知られることがないように注意しましょう。

### OpenAI APIサイト

[OpenAI API](https://openai.com/blog/openai-api)

- アカウントがない場合は新規作成します。

![スクリーンショット 2023-12-14 20.43.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/3e279b0b-1626-7850-870f-f90423ea5ecc.png)

- 右側APIを選択します

![スクリーンショット 2023-12-14 20.45.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/2f9678a8-625f-ccc1-0aa6-9b68230344fc.png)


- 必ずしも必要な作業ではありませんが、左側ペインからModelの確認をしましょう。`GPT-3.5 turbo`や`GPT-4`が有名です。リンクを辿ると料金表を確認することができます。下の画像はあくまでこの記事投稿時点のものです。

![スクリーンショット 2023-12-14 20.58.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/792de5eb-73ef-40f9-ba93-8c7c484be617.png)


### クレジットカードの登録

- Settings, Billing, Payment methodsと順に辿り、APIキーを利用するためにクレジットカードを登録します。なお、Pricingでは過去を含む使用状況を確認することができます

![スクリーンショット 2023-12-14 21.10.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/6a9923b6-acb3-aa55-365c-085d602f8f19.png)
支払い予定0.02ドル・・・w

### 利用上限額の設定

- 月の上限額やアラート連絡を送付する基準額を予め設定することが可能です。安全に使用するためには重要な設定ですね。Settings, Limits から設定しましょう。

![スクリーンショット 2023-12-14 21.15.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/c84c1b2c-7891-1fae-3442-6122f50774e4.png)



私は月15ドルを超えたらリクエストがrejectされる設定です。

### APIキーの発行

- 左側メニューからAPI Keysを選択し、`Create new secret key` を選択し、キー発行に進みます。
![スクリーンショット 2023-12-14 21.02.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/15264d66-a5eb-8dfd-f651-60dac8ad2450.png)


- APIキーの名前を求められるので設定します。その後、キー番号が表示されます。

:::note alert
このAPIキーはウインドウを閉じると２度とフルで表示されませんので、この時点で安全な場所にコピペし保存してください。絶対に他人に教えてはいけません。
:::
![スクリーンショット 2023-12-14 21.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/e3e64349-c654-2fc6-7edc-5c69475f29b1.png)

---

# Rails credentialにAPIキーを記述する

個人的にはOpenAI APIを叩くことよりも今回勉強になった内容かもしれません。

外部のAPIキーなどの機密情報をコードにベタ打ちすることは御法度ですが、なんらか別のファイルに書き記していたとしてもそれごと流出してしまうと意味がありません。悪意のあるサイバー攻撃のみではなく、Githubに自らうっかりアップしてしまうというケースだって考えられます。

そういったリスクを極力防ぐために、RailsにはパスワードやAPIキーといった秘匿すべき情報を暗号化させる仕組みが整えられています。これを用いて先ほど取得したOpenAI APIキーを安全に保存してみましょう。

[Rails セキュリティガイド - Railsガイド](https://railsguides.jp/security.html#独自のcredential)


### credentialの記述

機密情報を保存している`config/credentials.yml.enc`ファイル自体が暗号化されている関係で若干特殊な方法で編集します。なお、Dockerの場合はコンテナ内から操作してください。


- テキストエディタであるvimがインストールされていない場合は先にインストールしましょう
```bash
apt-get update
apt-get install vim
```

- 編集を開始する
```bash
EDITOR="vim" bin/rails credentials:edit
```
:::note warn
git cloneした場合など、秘密鍵がローカルにない場合はエラーになるのでクレデンシャルファイルを一度削除します
```bash
rm config/credentials.yml.enc
```
:::

- vimで開くと下記のように表示されます。ここに`chatgpt_api_key: skxxxxxxxxxx` と追記することが目標です。
- `矢印キー`でカーソルを操作し、任意の箇所で`i`を押すと挿入モードに切り替わります。`chatgpt_api_key: skxxxxxxxxxx(上で入手したAPIキー)`　を入力した上で`escキー`で挿入モードを終えたのち、`:wq`そして`Enter`を押せば保存完了です。

![スクリーンショット 2023-12-14 22.25.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/71745c6f-9493-6d52-5cdf-c05defa03ae1.png)
色々と機密が入っています

- vimの操作は初めてだと戸惑うのでネットの情報を参考にしましょう

[【Vim】初めてのVim - Qiita](https://qiita.com/qux42/items/7a3900e7a986faee9a14)

### .gitignoreファイルの確認

gitを使う場合、`.gitignore`ファイルに`/config/master.key`　の記述が含まれていることを確認しておきます。これがあれば秘密鍵をgithubに上げてしまうようなことはありません。おそらくデフォルトで記述されていると思いますが念のため。

---

# OpenAIのAPIを叩く関数を定義する

実装に入ります。ここでは、OpenAIのAPIを叩く関数は`app/service/chatgpt_service.rb`に記述しています。コントローラに関数を定義するとコードが肥大化したり、他に流用できなくなったりするので今回みたいな独立性の高いコードは単体のファイルで定義する方が好ましいようです。

### 必要なGemのインストール

HTTPクライアントであるhttpartyを用います。`Gemfile`に`gem 'httparty’` と記述し、インストールしましょう。

```bash
bundle install
```

なお、httpartyは重いといった言説を見かけるので他によい方法はあるかもしれません。未検証です。

### 関数の定義

`app/service/chatgpt_service.rb`　に下記のように定義します。このファイルは元々は存在しないので、新規で作成しましょう。細かい説明はコメントアウトでメモしました。

```ruby
class ChatgptService
  include HTTParty

  attr_reader :api_url, :options, :model, :message
  # モデルは予め設定しておく　ここでは3.5-turboを使う
  def initialize(message, model = 'gpt-3.5-turbo')
    # 機密ファイルを呼び出している
    api_key = Rails.application.credentials.chatgpt_api_key
    @options = {
      headers: {
        'Content-Type' => 'application/json',
        'Authorization' => "Bearer #{api_key}" # ここで 'api_key' を使用
      }
    }
    @api_url = 'https://api.openai.com/v1/chat/completions'
    @model = model
    @message = message
  end

  def call
	# userからのメッセージに対し一つ回答を与える
    body = {
      model:,
      messages: [{ role: 'user', content: message }]
    }
    response = HTTParty.post(api_url, body: body.to_json, headers: options[:headers], timeout: 100)
    raise response['error']['message'] unless response.code == 200
	# レスポンスはjson形式のため、下記の形で返答本文"content"のみを抽出する
    response['choices'][0]['message']['content']
  end

  class << self
    def call(message, model = 'gpt-3.5-turbo')
      new(message, model).call
    end
  end
end
```

---

# Controllerを記述する

:::note warn
実用性はあまり考えず、とりあえずOpenAI APIとRailsアプリを連携できているかの確認に重きを置いています。有益な活用方法は今後改めて考えることとします。
:::

ベースとなるアプリの`boards#index`　アクションにて、上で定義した`ChatgptService`を実行させるようにします。

### Controller

- `app/controller/boards_controller.rb` に下記のように記述します。
- indexページでウェルカムメッセージと今日誕生日の有名人を紹介させる内容にしました。
- APIのタイムアウトエラーが発生してもページが表示されるように処理を分岐させています。

```ruby
class BoardsController < ApplicationController
  def new
    @board = Board.new
  end
  
  def index
	# 日付を入手
    current_date = Date.today
	# 日付のフォーマットを調整
    formatted_date = current_date.strftime('%m月%d日')
    # ChatgptServiceでウェルカムメッセージを生成するが、タイムアウトエラーが起きたら別の処理を行う
		begin
    # プロンプトに日付を代入し、今日の誕生日の有名人を紹介させる
      @chatgpt = ChatgptService.call("簡単に挨拶してください。その際、#{formatted_date}が誕生日の有名人を簡単に紹介してください。")
	    # タイムアウトエラーが起きたときの処理。この場合は無難なAPIを使わず無難な内容にする
		rescue Net::ReadTimeout
      @chatgpt = "ようこそ！今日は#{formatted_date}です。"
    end
    @boards = Board.all.order(created_at: :desc)
    @board = Board.new 
  end

#　略
end
```

---

# Viewの記述

ブラウザでの見た目を整えます。といっても変数`@chatgpt`を表示しただけのもので、決してセンスある状態ではないのでご容赦ください。

### View

`app/views/index`　を編集します。

```html
<nav class="navbar navbar-light bg-light">
  <span class="navbar-text">
    <%= @chatgpt %>
  </span>
</nav>
```

---

# 完成

以上で完成です。問題なく動けば画像のように生成文を表示させられます。

やってみて幾つか気になったことを書くと・・・

- OpenAI APIのアクセスは数秒かかりますので、今回の活用だとページ表示時に毎回待たされることとなり正直いってナンセンスです。使い方はしっかりを考えないといけません。
- 今回の実験中、結構な頻度でタイムアウトエラーを起こしました。timeout設定を長めにとっても変わらず、Docker再起動するとしれっと直ったり・・まだ原因を特定できずにおります。HTTPクライアントも他にいいものがあるのかもしれません。

と、課題はまだまだ残りますが一旦はここまで。これからもGeek Outしていきます！

---