【GPT✖️Rails】GPTのAPIをRailsで使ってみた

# はじめに

こんにちは、エンジニア3年目の嶋田です。  
この記事を開いていただきありがとうございます！

今回の記事では、RailsアプリケーションでGPT APIを利用する方法についてまとめました。  
AIがますます注目される中、GPT APIをアプリケーションに統合して自然な会話や高度なテキスト生成を行うことが可能です。  

この技術は、テキスト要約、質問応答、自然な会話生成などに応用できます。
今回は、私自身が業務で取り組んだ実装方法の一例を少し変えて、実例を用いながら
RailsアプリケーションにGPTを統合する際の基本的な実装方法を紹介していきます！

私も実際に実装する上で勉強になったので備忘録としてまとめました。
参考にしていただければ幸いです。

## 目次
- [GPT APIとは](#gpt-apiとは)
- [RailsでAPIを利用するための準備](#railsでapiを利用するための準備)
- [APIリクエストの実装](#apiリクエストの実装)
- [まとめ](#まとめ)

## GPT APIとは

**GPT API**とは、OpenAIが提供する高度な言語モデル（GPTシリーズ）を外部から利用できるAPIです。このAPIを利用することで、アプリケーションでテキスト生成、要約、翻訳、質問応答などを行うことが可能です。

GPT APIはREST形式で、POSTリクエストを送信し、JSON形式の応答を受け取ります。利用できるモデルには複数のバージョンがあり、主に最新のGPT-4や、安価で高速なGPT-3.5が使われています。

詳しくはこちら👉 [OpenAI API Documentation](https://platform.openai.com/docs/api-reference/introduction)

## RailsでAPIを利用するための準備

### APIキーの取得

まず、GPT APIを使用するためには、OpenAIからAPIキーを取得する必要があります。  
公式サイトでアカウントを作成し、APIキーを取得してください。

次に、RailsアプリケーションでAPIキーをどのように管理するかを決定します。主に以下の2つの方法があります。

1. **`.env`ファイルを使用する方法**
2. **Railsの`credentials`を使用する方法**

この2つの方法について、メリット・デメリットを踏まえながら、それぞれの手順を説明します。

### `.env`ファイルでの管理

Railsで環境変数を管理するためには、`dotenv-rails`というGemを使用して、`.env`ファイルを活用する方法があります。これは開発初期の段階では簡単にセットアップできるため、よく使われる手法です。

#### メリット
- セットアップが非常に簡単で、環境変数の追加や変更が手軽です。
- 環境ごとに異なる`.env`ファイルを用意することで、開発環境や本番環境を簡単に切り替えられます。

#### デメリット
- `.env`ファイルは誤って漏洩するリスクがあり、セキュリティ面では脆弱です。
- 本番環境では`.env`ファイルを管理する手間がかかり、漏洩リスクが高まる可能性があります。

#### 手順

#### 1. **Gemfileに`dotenv-rails`と`ruby-openai`を追加**
```ruby
gem 'dotenv-rails'
gem 'ruby-openai'
```
- **dotenv-rails**は、Railsで環境変数を管理するためのGemです。これを使うことで、環境ごとに異なる設定を簡単に行えます。
    - [README](https://github.com/bkeepers/dotenv)
- **ruby-openai**は、OpenAIのAPIにアクセスするための公式Rubyクライアントです。このGemを使って、APIとやり取りできます。
        - [README](https://github.com/alexrudall/ruby-openai)

#### 2. **`.env`ファイルにAPIキーを追加**
プロジェクトのルートに`.env`ファイルを作成し、APIキーを追加します。

```env
OPENAI_API_KEY=your_openai_api_key_here
```

#### 3. **`config/initializers/openai.rb`でAPIキーを読み込む**
APIキーを使用して、OpenAI APIクライアントを初期化するための処理を行います。
```ruby
require 'openai'

OpenAI.configure do |config|
  config.access_token = ENV['OPENAI_API_KEY']
  config.log_errors = true
end
```

これにより、`.env`ファイルに保存されたAPIキーを使用してOpenAIのクライアントを初期化します。

### `credentials`での管理

`credentials`を使用する方法は、APIキーやその他の機密情報を暗号化して安全に管理するための方法です。Railsの標準機能として提供されており、セキュリティ面で非常に優れています。

#### メリット
- APIキーが暗号化されるため、セキュリティが高く、情報漏洩のリスクが低いです。
- Railsの標準機能として一貫して利用でき、変更履歴や管理がシンプルです。

#### デメリット
- `rails credentials:edit`コマンドを使うため、設定がやや複雑で、チーム内での共有や変更時に手間がかかることがあります。
- 複数の環境（開発・本番）に対応する際、少し手間が増えます。

#### 手順

#### 1. **エディタの設定**
まず、`rails credentials:edit`コマンドを実行するためには、環境変数`EDITOR`または`VISUAL`に使用するテキストエディタを設定する必要があります。以下のようにコマンドを実行して設定してください。

- nanoを使用する場合:

    ```bash
    EDITOR="nano" bin/rails credentials:edit
    ```

- Vimを使用する場合:

    ```bash
    EDITOR="vim" bin/rails credentials:edit
    ```

#### 2. **APIキーを追加**
エディタが開いたら、次のようにAPIキーを追加してください。`config/credentials.yml.enc`に追記されます。
```yaml
openai:
  development:
    api_key: your_openai_dev_key_here
  production:
    api_key: your_openai_prod_key_here
```

#### 3. **Gemfileに`ruby-openai`を追加**
RailsアプリケーションでOpenAIを使用するために、Gemfileに以下を追加し`bundle install`を実行します。
```ruby
gem 'ruby-openai'


#### 4. **`config/initializers/openai.rb`でAPIキーを読み込む**
APIキーを使用して、OpenAI APIクライアントを初期化するための処理を行います。
```ruby
require 'openai'

OpenAI.configure do |config|
  config.access_token = Rails.application.credentials.openai[Rails.env.to_sym][:api_key]
  config.log_errors = true
end
```
---

### `.env`ファイルと`credentials`の比較

| 管理方法            | メリット                                                                                         | デメリット                                                                           |
|---------------------|------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| `.env`ファイル       | セットアップが簡単で、開発環境での運用に便利。                                                      | セキュリティリスクがある。ファイルの漏洩や誤操作に注意が必要。                     |
| `credentials`       | 機密情報が暗号化されるため、セキュリティが非常に強い。                                              | 設定や変更時にコマンドが必要で、複数環境の設定に手間がかかる。                      |

`credentials`はセキュリティを重視する本番環境での運用に適しており、`dotenv-rails`を使用した`.env`ファイルは開発環境や簡易なプロジェクトに適しています。どちらを選ぶかは、プロジェクトの規模や運用方針に応じて決定してください。

---

## 要約生成機能の実装

ここからは、GPT APIを使ってURLの内容を要約する機能を実装していきます。

### 1. ルーティング設定

まず、`config/routes.rb`でルーティングを設定します。

```ruby
Rails.application.routes.draw do
  get 'summary/new', to: 'summary#new', as: 'new_summary'
  post 'summary/create', to: 'summary#create', as: 'create_summary'
  root 'summary#new'
end
```

### 2. フォームクラスの作成

次に、`app/forms/summary_form.rb`でフォームオブジェクトを作成し、要約生成のロジックを定義します。

```ruby
class SummaryForm
  include ActiveModel::Model

  attr_accessor :url, :result

  def initialize(params)
    @url = params[:url]
    @result = nil
  end

  def generate_summary
    prompt = build_prompt_from_url
    @result = generate_results(prompt)
    @result.present?
  end

  private

  def build_prompt_from_url
    <<~PROMPT
      以下のURLの内容を要約してください。
      URL: #{@url}
      要約を3つのポイントに分けて、簡潔にまとめてください。
    PROMPT
  end

  def generate_results(prompt)
    GptService.new(prompt).call
  rescue StandardError => e
    errors.add(:base, "要約生成に失敗しました: #{e.message}")
    nil
  end
end
```

### 3. サービスクラスの作成

次に、`app/services/gpt_service.rb`でGPT APIにリクエストを送信し、要約を生成するクラスを実装します。
※ エラーメッセージはユーザーに合わせて変えて下さい。

```ruby
class GptService
  def initialize(prompt)
    @prompt = prompt
  end

  def call
    client = OpenAI::Client.new
    response = client.chat(
      parameters: {
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: @prompt }]
      }
    )

    if response['choices'] && response['choices'].any?
      response['choices'][0]['message']['content']
    else
      'GPTからの応答がありません'
    end
  rescue OpenAI::Error => e
    handle_openai_error(e)
  rescue StandardError => e
    "システムエラーが発生しました。エラー内容: #{e.message}"
  end

  def handle_openai_error(error)
    case error
    when OpenAI::QuotaExceededError
      'GPTのクォータが超過しました。プランと請求情報を確認してください。'
    when OpenAI::AuthenticationError
      'GPTへの認証に失敗しました。APIキーを確認してください。'
    when OpenAI::ConnectionError
      'GPTサーバーとの通信に失敗しました。インターネット接続を確認してください。'
    else
      "OpenAIのエラーが発生しました: #{error.message}"
    end
  end
end
```

### 4. コントローラーの作成

`app/controllers/summary_controller.rb`で、フォームからの入力を処理し、要約結果を表示します。

```ruby
class SummaryController < ApplicationController
  def new
  end

  def create
    url = params[:url]
    summary_form = SummaryForm.new(url:)

    if summary_form.generate_summary
      session[:summary_result] = summary_form.result
      redirect_to new_summary_path, notice: '要約が正常に作成されました'
    else
      flash[:alert] = '要約作成に失敗しました'
      redirect_to new_summary_path
    end
  end
end
```

### 5. ビューの作成

`app/views/summary/new.html.erb`で、ユーザーがURLを入力するフォームと結果表示を作成します。

```erb
<div class="wrapper">
  <h1 class="summary-title">サイト要約生成</h1>

  <%= form_with url: create_summary_path, method: :post, class: "summary-form" do %>
    <div class="form-group">
      <div class="input-group">
        <%= text_field_tag :url, params[:url], placeholder: "要約したいサイトのURLを入力してください", class: "url-input" %>
        <%= submit_tag "要約を生成", class: "submit-button" %>
      </div>
    </div>
  <% end %>

  <% if session[:summary_result].present? %>
    <h2 class="summary-result-title">要約結果</h2>
    <div class="summary-result-container">
      <ul class="summary-result">
        <% session[:summary_result].split(/\r?\n/).each do |item| %>
          <li><%= item.strip %></li>
        <% end %>
      </ul>
    </div>
    <% session.delete(:summary_result) %>
  <% end %>
</div>
```

### スタイルの設定

最後に、画面のスタイルを設定します。

```css
.wrapper {
  width: 80%;
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
  box-sizing: border-box;
  border: 1px solid #ddd;
  border-radius: 10px;
  background-color: #f9f9f9;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.summary-title {
  text-align: center;
  margin-top: 30px;
  margin-bottom: 30px;
  font-size: 24px;
}

.input-group {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  width: 100%;
}

.url-input {
  flex-grow: 1;
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #ccc;
}

.submit-button {
  padding: 10px 20px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.summary-result-title {
  text-align: center;
  margin-top: 100px;
  margin-bottom: 30px;
  font-size: 20px;
}

.summary-result-container {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
}

.summary-result {
  background-color: #fff;
  padding: 15px;
  border-radius: 5px;
  border: 1px solid #ccc;
  width: 100%;
  line-height: 1.6;
  font-size: 1.1em;
}
```

#### 完成画面
![完成画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524119/d1ad7764-0e00-6bf6-2cd0-ba9d460320ca.png)

## まとめ
最後までお付き合いいただきありがとうございました。
この記事では、RailsアプリケーションにGPT APIを統合し、入力したURLの内容を要約する機能の実装方法を解説しました。GPT APIを使うことで、テキスト生成や要約機能を簡単にアプリケーションに統合できるため、さまざまなプロジェクトで応用できます。今後AIを実装する機会は増えていくかもしれないので、もっといい方法があればコメントにて教えて下さい。