【未経験】Rails7でChat GPTを導入した練習用アプリを作成してみた

# はじめに

RailsアプリにChatGPTを導入する方法に関する記事があまりなかったため、自分用にまとめつつ共有できたらと思い、この記事を書かせていただきます。

今回はPFにChatGPTを導入したかったので、練習としてChatGPTを導入するだけのシンプルなアプリを作成してみたのですが、[公式サイト](https://platform.openai.com/docs/guides/chat/introduction)や[rubyのGem](https://github.com/alexrudall/ruby-openai)通りに書いたら、簡単に導入できました！

この記事の目的は、初学者の方にChatGPTの導入が簡単であることを知ってもらうことです。私なりの解釈で書いているので、間違っている部分やコードの書き方が悪い部分があるかもしれません。また、ほとんどgemをコピペしてOpenAIの導入ができるため、**今回の記事は不要な方も多い**かと思いますが、ご了承ください。

今回私がChat GPTの練習のために使ったアプリは明日の予定を入力すると、Chat GPTがショートストーリーを作成してくれるというとても簡単なアプリです。明日が不安で寝られない人が明日が来るのが楽しみになるアプリにしたいという思いで一応作成してみました！明日の予定を入力すると起きるかもしれない嬉しいハプニングを含んだショートストーリーを作成するようにプロンプトを書いています。（起承転結の承が転になっていますが…）
[![Image from Gyazo](https://i.gyazo.com/008f299a3d9e387777f8ff3c88a71926.png)](https://gyazo.com/008f299a3d9e387777f8ff3c88a71926)
[![Image from Gyazo](https://i.gyazo.com/738c785ebea399e5ac2e8e71af91bc3f.png)](https://gyazo.com/738c785ebea399e5ac2e8e71af91bc3f)

# 目次
1. OpenAI APIとは
1. OpenAI APIキーの取得
1. 実際にコードを書いてみる

## 1. OpenAI APIとは

Chat GPTを使うにはまずOpenAI APIを使います。何それ？となった方は「[ChatGPT を提供する OpenAI の API 入門！初心者にも分かりやすく解説](https://book.st-hakky.com/docs/chatgpt-api-for-businessperson/)」という記事で紹介されています。

最初に注意点として、OpenAI APIを使用するにはお金がかかります。[こちらの公式ページ](https://openai.com/pricing#language-models)に金額の詳細が書かれています。現時点では1000トークンあたり0.002ドルかかるそうです。ですが、最初の18ドル分は無料で使うことができます。

公式の[このページ](https://platform.openai.com/tokenizer)でトークン数を計算してくれます。
[OpenAIのAPI料金の計算方法](https://zenn.dev/umi_mori/books/chatbot-chatgpt/viewer/how_to_calculate_openai_api_prices)の記事で日本語のトークン数の計算方法について説明してくださっています。
※入力・出力共にお金がかかります。

## 2. OpenAI APIキーの取得

1. [公式ページ](https://openai.com/blog/openai-api)からまずサインアップする
1. 右上に表示されている自分のアカウント名をクリック→View API keys
1. APIキーを取得（コピーしておく。このキーは自分だけがみられる場所にコピーする。コードに直接書くのはNGですので、取り扱いに注意。）
※ちなみに、左側のナビゲーションバーのUsageからいくら分使っているのかわかります。

## 3. 実際にコードを書いてみる
ここからは[公式サイト](https://platform.openai.com/docs/guides/chat/introduction)と[rubyのGem](https://github.com/alexrudall/ruby-openai)に詳しく書かれているので、そちらをまず読んでみてください！
ここから先は私なりの解釈で書いたコードになりますので、ご了承ください！

### 1. Gemをインストール
ruby-openaiというrubyでOpenAIを使うためのGemがあるので、インストール。
これを使うとすごく簡単にOpenAIを導入できます！
`gem "ruby-openai"`→`bundle install`

### 2. APIキーを環境変数に入れる
先ほどお伝えしたように、APIキーを直接コードに書いてはいけない（不正利用されます）ので、環境変数に入れます。.envファイルや~zshrcファイルに書くとかいろいろありそう？なのですが、私は以前Qiitaで見た「[Rails5.2から追加された credentials.yml.enc のキホン](https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39)」に書かれている`credentials.yml.enc`を使う方法でやってみました！公式は[こちら](https://railsguides.jp/security.html#%E7%8B%AC%E8%87%AA%E3%81%AEcredential)。

.envファイルはGitHubに間違えてあげてしまいそう（.gitignoreに入れ忘れたりしそう）だったのと、zshrcファイルにこれ以上コードを書いたらわけわからんくなりそうだったので（すでにわけわからない）、Rails5.2から追加されてるらしいし使ってみようと思い、使ってみました。
詳細の説明は「[【Rails】世界で一番わかりやすい！！「credentials.yml.enc」+「master.key」使い方徹底攻略！](https://hirocorpblog.com/rails-credentials-master/)」や「[Railsのcredentials.yml.encとmaster keyをDockerで安全に扱う](https://techblog.lclco.com/entry/2021/07/27/110000)」の記事を読みました。

でも、これを使うのがいいのかわかりません…だれか知ってたら教えてください！！

### 3. テーブルを作成
テーブルが必要かなどは作りたいアプリによって違うと思いますが、今回私が作ったアプリは予定を入力すると、ChatGPTがショートストーリーを作成してくれるというものなので、`Storyモデル`にユーザーが予定を入力する`planカラム`とChatGPTの回答を入れる`contentカラム`を作成しました。

```ruby:マイグレーションファイル
class CreateStories < ActiveRecord::Migration[7.0]
  def change
    create_table :stories do |t|
      t.string :plan
      t.text :content

      t.timestamps
    end
  end
end
```
 

### 4. モデル
書く場所はモデルでいいのかはわからないのですが、コントローラーに書くよりはましだろうと思い、モデルに書いています。また、Storyモデルに書いてもいいのかなとかも思ったのですが、OpenAiを使うので、わかりやすくOpenAI用のモデルファイルを作成しました。（なので、モデルにこのファイルを置かなくてもいいと思うんですけど、どこに置いたらいいのかわからなかった…）

```ruby:app/models/open_ai.rb
class OpenAi
  require 'ruby/openai'

  def self.generate_story(plan)
    client = OpenAI::Client.new
    response = client.chat(
      parameters: {
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: "「タイトルは#{plan}」です。私が主人公の起承転結の嬉しいハプニングが起きるように考えて、ショートストーリーを300字以内で書いてください。出力形式: 起：承：転：結：" }],
      }
    )
    response.dig('choices', 0, 'message', 'content')
  end
end
```

```ruby:config/initializers/openai.rb
OpenAI.configure do |config|
  config.access_token = Rails.application.credentials.openai[:api_key]
end
```

ほとんど公式通りです。詳細は公式をご覧ください。
newメソッドでOpenAIを作成します。今回はchatを使っています。chatの他にもcompletions, edits, Embeddings, FilesなどいろいろOpenAIを使ってできるそうです！

newの引数に`config`に書いてある`access_token`をそのまま書いてもいいのですが、Fatモデルもあると聞くので、今回はconfigに書いてみました。でも、わかりにくい気もする…
`access_token`の他にもタイムアウトの設定や組織IDを書くこともできます。
今回のように`access_token`だけならモデルに書いちゃった方がいい気もするけど、まあこのままいきます。

`response`以下はChatGPTに送るプロンプト部分です。modelとmessageは必須です。
- `model`：公式の[こちら](https://platform.openai.com/docs/models/overview)から選びます。現時点では4はまだwaitelistにjoinしてと書いてあり、限定版のようです。なので、その下の3.5-turboがいいかなと思い選びました。
- `messages`：ChatGPTに送るメッセージのことで、role＝役割と、content＝内容を書きます。
`role`は`user,system,assistant`があるのですが、今回はユーザーの入力からストーリーを作成してもらうので、userを選日ました。
`content`にはChatGPTに入力したい内容を書きます。今回は引数にユーザーが入力した予定が入るようにしているので、ユーザーの入力を含めたプロンプトメッセージを書いています。

最後の行で、ChatGPTの回答を取得しています。responseは以下のように返ってくるそうです。
以下公式からコピペ
```
{
 'id': 'chatcmpl-6p9XYPYSTTRi0xEviKjjilqrWU2Ve',
 'object': 'chat.completion',
 'created': 1677649420,
 'model': 'gpt-3.5-turbo',
 'usage': {'prompt_tokens': 56, 'completion_tokens': 31, 'total_tokens': 87},
 'choices': [
   {
    'message': {
      'role': 'assistant',
      'content': 'The 2020 World Series was played in Arlington, Texas at the Globe Life Field, which was the new home stadium for the Texas Rangers.'},
    'finish_reason': 'stop',
    'index': 0
   }
  ]
}
```
なので、[digメソッド](https://docs.ruby-lang.org/ja/latest/method/Hash/i/dig.html)でchoicesの0番目の配列のmessageのcontentを取得しています。

###  5. コントローラー

コントローラーにモデルのメソッドを使って書いていきます。
```ruby:app/controllers/stories_controller.rb
class StoriesController < ApplicationController
  def top; end

  def index; end

  def new
    @story = Story.new
  end

  def show
    @story = Story.find(params[:id])
  end

  def create
    @story = Story.new(story_params)
    if @story.save
      story = OpenAi.story_generate(@story.plan)
      @story.conten = story
      redirect_to @story
    else
      render :new
    end
  end

  private

  def story_params
    params.require(:story).permit(:plan, :content)
  end
end

```

createアクションの`＠story`でFormに入力されたデータを入れて保存できたら、OpenAiクラスの`story_generate`メソッドを呼び出します。引数は`@storyのplanカラム`です。planにユーザーに入力してもらった予定が入っています。

これで、あとはビューを書いたら終わりなんですけど、これだとファットコントローラーと言われそうなので、モデルに寄せていきます。
コールバックという技を最近知ったので、使ってみます（たぶんもっと前に勉強したと思います…）。メソッド名はChatGPTに考えてもらいました（メソッド名とか変数とか考えられない…）。
before_createはストーリーを保存する前にそのメソッドを行うということです。

```ruby
# app/models/story.rb
class Story < ApplicationRecord
  before_create :create_story_from_plan

  private

  def create_story_from_plan
    story = OpenAi.generate_story(self.plan)
    self.content = story
  end
end

# app/controllers/stories_controller.rb
def create
  @story = Story.new(story_params)
  if @story.save
    redirect_to @story
  else
    render :new
  end
end
```

これでコントローラーがすっきりしました！これでいいのかはわかりません…

### 6. ルーティング
```ruby
Rails.application.routes.draw do
  root to: 'stories#top'
  resources :stories, only: %i[show new create]
end
```

### 7. ビュー
初めて自分でちゃんとHTMLを書いたので、HTMLは絶対に真似しないでください。適当に書いています。ちゃんと勉強します…。ちなみにCSSはTailwindとDaisyUIを使ってみました。
```erb
<% # new.html.erb %>
<div class="container pt-3">
  <h1 class="text-4xl font-bold mb-6">AIショートストーリー作成</h1>

  <p class="mb-10">
    あなたの明日の予定を入力してください。AIがあなたの予定をもとにショートストーリーを作成します。
  </p>
  <%= form_with model: @story, method: :post, data: { turbo: false }, class: "mb-6" do |form| %>
    <%= form.label :plan, "明日の予定", class: "block text-lg mb-2" %>
    <%= form.text_field :plan, class: "input input-bordered input-secondary w-full max-w-x mb-2" %>
    <div class="mt-4">
      <%= form.submit "作成", class: "btn btn-primary" %>
    </div>
  <% end %>
</div>

<% # show.html.erb %>
<div class="container pt-3">
  <h1 class="mb-8 text-2xl">AIが作成したショートストーリー</h1>
  <div class="card lg:card-side bg-base-100 shadow-xl mb-8">
    <div class="card-body">
      <h2 class="card-title">ストーリー</h2>
      <p class="mb-8">明日の予定：<%= @story.plan %></p>
      <p><%= html_safe_newline(@story.content) %></p>
    </div>
  </div>
  <div class="text-center">
    <%= link_to 'もういちど', new_story_path, class: 'btn btn-outline btn-accent'%>
  </div>
</div>
```

Rails7系で書いているので、Turboを使わないように`data: { turbo: false }`と書いています。（`ruby-openai`のgemにはTurbo Streamsのやり方の書かれているので、勉強します…！）

`html_safe_newline`メソッドはChatGPTが改行を`\n`と書くので、それをHTMLの<br>に変更するようなメソッドをヘルパーに書いています。（たぶんプロンプトを改良したらいいのだと思うんですけど、うまくいかなかったので…）

```ruby:app/helpers/story_helper.rb
module StoriesHelper
  def html_safe_newline(str)
    h(str).gsub(/\R/, "<br>").html_safe
  end
end
```

上のメソッドをコメントでご指摘いただき修正させていただきました！
以前は`gsub(/\n|\r|\r\n/, "<br>")`のように書いていたのですが、これだと|の前が該当する場合、後ろの部分は無視されてしまうため、`\r\n`の際`<br><br>`のようになってしまうそうです。
そのため、先に`\r\n`を書く必要があり、さらに`\R`を使うことで他の改行の仲間（U+0085 NEXT LINE、U+2028 LINE SEPARATOR、U+2029 PARAGRAPH SEPARATOR）も含めて`<br>`に変換してくれるそうです！！
公式によると`\R`は文字クラスの中では使用できないそうですが、今回文字クラスではないため、`\R`を使ってみることにしました。アプリ上で検証してみたところ無事`<br>`に変換されました！

正規表現の勉強不足で間違えたメソッドを記載してしまい申し訳ありません…正規表現勉強します…！

# まとめ

これでフォームに明日の予定を入力するとショートストーリーが作成される簡単アプリが完成しました！
めちゃくちゃ簡単ですよね！！
プロンプトなどを考えていったら、もっとトークンを節約できるそうなので、その辺りを改善していきたいです。（英語の方がトークンを使わないので、プロンプトは英語で書いてもらった方がよさそう(?)）

今回一番苦労した点はOpenAIのプロンプトに引数を渡すためにの、文字列に変数を表示させる式展開です。めっちゃ凡ミスなのですが、ずっと`'`で囲んでいて、でも、式展開時は`"`で囲まないといけないです。なので、ずっと引数がプロンプトに反映されず予定と頓珍漢な答えばかりを返してきて、「なんで！！！！！」となっていました…凡ミスでした…progateレベルのミスで、コードを適当に書いてはいけないと反省しました！！

あと、ChatGPTを導入したいのに、実装方法をChatGPTに聞くことはできないです。ruby-openaiの存在を知らないので、ruby-openaiを消そうとしてきます…
Rails7もChatGPTには聞けないし、ちゃんと公式を見て勉強しないといけないですね…コピペエンジニアにならないように勉強します…

手探りでアプリを作成したので、まちがっているところなどあるかもしれません…
間違っていたり、もっといい方法があるよ！といった場合は教えていただけると嬉しいです🙇‍♀️
ほとんどgemのコピペでOpenAIの導入はできるので、今回の記事は不要な方が多かったと思いますが、どなたかのお役に立てたら嬉しいです。