はじめに
RailsアプリにChatGPTを導入する方法に関する記事があまりなかったため、自分用にまとめつつ共有できたらと思い、この記事を書かせていただきます。
今回はPFにChatGPTを導入したかったので、練習としてChatGPTを導入するだけのシンプルなアプリを作成してみたのですが、公式サイトやrubyのGem通りに書いたら、簡単に導入できました!
この記事の目的は、初学者の方にChatGPTの導入が簡単であることを知ってもらうことです。私なりの解釈で書いているので、間違っている部分やコードの書き方が悪い部分があるかもしれません。また、ほとんどgemをコピペしてOpenAIの導入ができるため、今回の記事は不要な方も多いかと思いますが、ご了承ください。
今回私がChat GPTの練習のために使ったアプリは明日の予定を入力すると、Chat GPTがショートストーリーを作成してくれるというとても簡単なアプリです。明日が不安で寝られない人が明日が来るのが楽しみになるアプリにしたいという思いで一応作成してみました!明日の予定を入力すると起きるかもしれない嬉しいハプニングを含んだショートストーリーを作成するようにプロンプトを書いています。(起承転結の承が転になっていますが…)
目次
- OpenAI APIとは
- OpenAI APIキーの取得
- 実際にコードを書いてみる
1. OpenAI APIとは
Chat GPTを使うにはまずOpenAI APIを使います。何それ?となった方は「ChatGPT を提供する OpenAI の API 入門!初心者にも分かりやすく解説」という記事で紹介されています。
最初に注意点として、OpenAI APIを使用するにはお金がかかります。こちらの公式ページに金額の詳細が書かれています。現時点では1000トークンあたり0.002ドルかかるそうです。ですが、最初の18ドル分は無料で使うことができます。
公式のこのページでトークン数を計算してくれます。
OpenAIのAPI料金の計算方法の記事で日本語のトークン数の計算方法について説明してくださっています。
※入力・出力共にお金がかかります。
2. OpenAI APIキーの取得
- 公式ページからまずサインアップする
- 右上に表示されている自分のアカウント名をクリック→View API keys
- APIキーを取得(コピーしておく。このキーは自分だけがみられる場所にコピーする。コードに直接書くのはNGですので、取り扱いに注意。)
※ちなみに、左側のナビゲーションバーのUsageからいくら分使っているのかわかります。
3. 実際にコードを書いてみる
ここからは公式サイトとrubyのGemに詳しく書かれているので、そちらをまず読んでみてください!
ここから先は私なりの解釈で書いたコードになりますので、ご了承ください!
1. Gemをインストール
ruby-openaiというrubyでOpenAIを使うためのGemがあるので、インストール。
これを使うとすごく簡単にOpenAIを導入できます!
gem "ruby-openai"
→bundle install
2. APIキーを環境変数に入れる
先ほどお伝えしたように、APIキーを直接コードに書いてはいけない(不正利用されます)ので、環境変数に入れます。.envファイルや~zshrcファイルに書くとかいろいろありそう?なのですが、私は以前Qiitaで見た「Rails5.2から追加された credentials.yml.enc のキホン」に書かれているcredentials.yml.enc
を使う方法でやってみました!公式はこちら。
.envファイルはGitHubに間違えてあげてしまいそう(.gitignoreに入れ忘れたりしそう)だったのと、zshrcファイルにこれ以上コードを書いたらわけわからんくなりそうだったので(すでにわけわからない)、Rails5.2から追加されてるらしいし使ってみようと思い、使ってみました。
詳細の説明は「【Rails】世界で一番わかりやすい!!「credentials.yml.enc」+「master.key」使い方徹底攻略!」や「Railsのcredentials.yml.encとmaster keyをDockerで安全に扱う」の記事を読みました。
でも、これを使うのがいいのかわかりません…だれか知ってたら教えてください!!
3. テーブルを作成
テーブルが必要かなどは作りたいアプリによって違うと思いますが、今回私が作ったアプリは予定を入力すると、ChatGPTがショートストーリーを作成してくれるというものなので、Storyモデル
にユーザーが予定を入力するplanカラム
とChatGPTの回答を入れるcontentカラム
を作成しました。
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用のモデルファイルを作成しました。(なので、モデルにこのファイルを置かなくてもいいと思うんですけど、どこに置いたらいいのかわからなかった…)
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
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
:公式のこちらから選びます。現時点では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メソッドでchoicesの0番目の配列のmessageのcontentを取得しています。
5. コントローラー
コントローラーにモデルのメソッドを使って書いていきます。
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はストーリーを保存する前にそのメソッドを行うということです。
# 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. ルーティング
Rails.application.routes.draw do
root to: 'stories#top'
resources :stories, only: %i[show new create]
end
7. ビュー
初めて自分でちゃんとHTMLを書いたので、HTMLは絶対に真似しないでください。適当に書いています。ちゃんと勉強します…。ちなみにCSSはTailwindとDaisyUIを使ってみました。
<% # 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の
に変更するようなメソッドをヘルパーに書いています。(たぶんプロンプトを改良したらいいのだと思うんですけど、うまくいかなかったので…)
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の導入はできるので、今回の記事は不要な方が多かったと思いますが、どなたかのお役に立てたら嬉しいです。