OpenAI APIをSpringのWebアプリケーションに組み込んでみる

# はじめに

Spring初心者による学習の一環として、ChatGPTのAPIをWebアプリケーションに組み込んでみました。ただ組み込んだだけです。学習の記録として、記事にまとめさせていただきます。

なお、誤りや改善の余地がございましたら、優しくご指摘いただけますと幸いです。

# 完成イメージ

主な処理の流れと完成図を以下に示します。

1. ユーザーは、ブラウザのフォームにメッセージを入力し、ボタンをクリックします。
2. サーバーは、ユーザーのメッセージを受け取り、それを元にAPIリクエストを生成します。
3. サーバーは、生成されたAPIリクエストを使用して、外部APIに接続します。
4. サーバーは、外部APIより受け取ったレスポンスからChatGPTのメッセージを抽出し、それをブラウザに送信します。
5. ユーザーは、ブラウザに表示されたChatGPTからのメッセージを確認します。

![スクリーンショット 2024-01-09 143907.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/506fc10a-1116-61ce-ce0f-e12d3c652e81.png "メッセージを入力し、ボタンをクリック")

![スクリーンショット 2024-01-09 144352.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654286/0ce388b3-5b44-c6ad-0120-fab23d3a6a52.png "ChatGPTからのメッセージが表示される")

# 開発環境

使用した言語やツールを以下に示します。

| 分類 |  |
| --- | --- |
| IDE | Eclipse Version: 2023-06 (4.28.0) |
| バックエンド | Java17, Spring Boot 3.2.1 |
| フロントエンド | HTML, Thymeleaf, Bootstrap |
| ビルドツール | Gradle |


# Springスタータープロジェクトの作成

以下の4つの依存関係を含め、Springスタータープロジェクトを作成します。

- Spring Boot DevTools
- Spring Web
- Lombok
- Thymeleaf

プロジェクトを作成したら、[こちら](https://github.com/bufferings/thymeleaf-extras-nl2br)のライブラリを依存関係に追加します。
```build.gradle
dependencies {
	implementation 'com.github.bufferings:thymeleaf-extras-nl2br:1.0.2'
}
```

# OpenAI APIのキーの取得

- [こちら](https://auto-worker.com/blog/?p=6988#)のサイトを参考に、OpenAI APIに登録し、APIキーを取得します。
- 設定ファイルに、エンドポイントと取得したAPIキーを追加します。
    - 今回はチャット機能を利用する為、エンドポイントは、公式リファレンスの「[Create chat completion](https://platform.openai.com/docs/api-reference/chat/create)」に記載のものを使用します。

```application.properties
#chatgpt
chatgpt.api.url=https://api.openai.com/v1/chat/completions
chatgpt.api.key=取得したAPIキー
```

# OpenAI APIをサーバー側に組み込み（Model）

### 1. エンティティの作成
- APIリクエスト・レスポンスを管理するためのエンティティクラスを作成します。
- [公式リファレンス](https://platform.openai.com/docs/api-reference/chat/create)を参考に、適切なフィールドを用意します。
- Lombokライブラリの`@Data`を付加し、記述を簡略化しています。

```ChatGPTRequest.java
@Data
public class ChatGPTRequest {

	private String model;
	private List<Message> messages;

	@Data
	@AllArgsConstructor
	public class Message {
		private String role;
		private String content;
	}
}
```

```ChatGPTResponse.java
@Data
public class ChatGPTResponse {

    private String id;
	private String object;
	private Long created;
	private String model;
	private String systemFingerprint;
	private Choices[] choices;
	private Usage usage;
 
	// 以下、省略
}
```


### 2. サービスクラスの作成
- APIにリクエストを送信し、レスポンスを受信するサービスクラスを作成します。
- サービスクラスの主な処理は、次の3つです。各処理をメソッドに分割しています。
    1. `generateAPIMessage()`：APIに接続しリクエスト送信・レスポンス受信（2・3を呼び出し）
    2. `createRequestEntity()`：リクエストエンティティの作成
    3. `extractAPIMessage()`：レスポンスからメッセージを抽出
- API接続には、SpringBootが提供する`RestTemplat`を使用します。`RestTemplate`の使い方は[こちら](https://b1san-blog.com/post/spring/spring-rest-template/)を参考にしています。

```ChatGPTService.java
@Service
public class ChatGPTService {

	@Value("${chatgpt.api.url}")
	private String chatGPTApiUrl;

	@Value("${chatgpt.api.key}")
	private String chatGPTApiKey;

	@Autowired
	private RestTemplate restTemplate;

	private static final String systemMessage = "日本語で回答してください。";

	// APIにリクエストを送信し、レスポンスからメッセージを取得する
	public String generateAPIMessage(String userMessage) {
		// 引数をもとにリクエスト生成
		RequestEntity<ChatGPTRequest> request = createRequestEntity(userMessage);

		try {
			// API接続
			ResponseEntity<ChatGPTResponse> response = restTemplate.exchange(request, ChatGPTResponse.class);
			// レスポンスからメッセージを抽出し返却
			return extractAPIMessage(response);
		}
		catch (RestClientException e) {
			e.printStackTrace();
			return "Error connecting to the API";
		}
	}

	// リクエストエンティティを生成する
	private RequestEntity<ChatGPTRequest> createRequestEntity(String userMessage) {
		// リクエストヘッダ生成
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);
		headers.setBearerAuth(chatGPTApiKey);

		// リクエストボディ生成
		ChatGPTRequest body = new ChatGPTRequest();
		body.setModel("gpt-3.5-turbo");
		body.setMessages(List.of(
				body.new Message("system", systemMessage),
				body.new Message("user", userMessage)));

		// リクエストエンティティ生成と返却
		return RequestEntity.post(chatGPTApiUrl).headers(headers).body(body);
	}

	// ChatGPTからのレスポンスを処理し、メッセージを抽出する
	private String extractAPIMessage(ResponseEntity<ChatGPTResponse> response) {
		// レスポンスが空の場合
		if (response == null) {
			throw new RuntimeException("Error processing the request");
		}

		// レスポンスボディまたはレスポンスボディのメッセージフィールドが空の場合
		ChatGPTResponse responseBody = response.getBody();
		if (responseBody == null || responseBody.getChoices().length == 0) {
			throw new RuntimeException("Error processing the response");
		}

		// １つ目の回答からメッセージを抽出し返却
		// ※ChatGPTは複数回答をレスポンスするが、今回は１つ目の回答のみを扱う
		return responseBody.getChoices()[0].getMessage().getContent();
	}

}
```

### 3. Bean定義

2つのライブラリのクラスをBean定義します。

- 自動生成されたApplicationクラスに追記します。
- `RestTemplate`は上記のサービスクラスで使用し、`Nl2brDialect`は後から登場するビューで使用します。

```DemoApplication.java
    @Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

	@Bean
	public Nl2brDialect dialect() {
		return new Nl2brDialect();
	}
```


# Controllerクラスの作成（Controller）

- コントローラクラスは、入力画面を表示します。また、ユーザーが入力した値をもとにサービスクラスを呼び出します。
- 次の2つのコントローラメソッドを作成します。
    1. `showForm()`：GETリクエストに対しViewを返却する
    2. `generateApiMessage()`：POSTリクエストに対しサービスを呼び出してからViewを返却する
- URLは次の2通りを設定しています。

| URL | Method | ContentType | 目的 |
| --- | --- | --- | --- |
| /chat | GET | HTML | 入力画面表示 |
| /chat | POST | HTML | 入力画面に結果表示 |

```ChatGPTController.java
@Controller
@RequestMapping("/chat")
public class ChatGPTController {

	@Autowired
	private ChatGPTService service;

	@GetMapping("")
	public String showForm() {
		return "form";
	}

	@PostMapping("")
	public String generateApiMessage(@RequestParam String userMessage, Model model) {
		String apiMessage = service.generateAPIMessage(userMessage);
		model.addAttribute("apiMessage", apiMessage);
		return "form";
	}

}
```

# フロントエンドとの連携（View）

- 今回は1画面のみを作成します。テンプレートエンジンとしてThymeleaf、デザインフレームワークとしてBootstrapを使用しています。
- 主に、次の2つのエリアを作成します。
    1. **入力フォーム**：ユーザーの入力をPOSTメソッドでサーバに送信
    2. **結果表示エリア**：サーバから受信したメッセージを表示
- メッセージオブジェクト`${apiMessage}`は、改行コード`\n`を、改行タグ`<br>`に変換して表示する必要があります。変換には、thymeleaf-extras-nl2brライブラリを使用しています。ライブラリの使い方は[こちら](https://github.com/bufferings/thymeleaf-extras-nl2br)を参考にしてください。

```form.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org" xmlns:nl2br="https://github.com/bufferings/thymeleaf-extras-nl2br">
<head><!-- 省略 --></head>
<body>
<div>
	<h1>ChatGPT API Demo</h1>
	<!-- 入力フォーム -->
	<form th:action="@{/chat}" method="post">
		<textarea id="userMessage" name="userMessage"></textarea>
		<label for="userMessage">Enter your message</label>
		<button type="submit">Generate Response</button>
	</form>

	<!-- 結果表示エリア -->
	<div>
		<h2>ChatGPT Response</h2>
		<p th:if="${apiMessage != null}" nl2br:text="${apiMessage}"></p>
		<p th:if="${apiMessage == null}">こんにちは。</p>
	</div>
</div>
</body>
</html>
```

- 最後に、Bootstrapを使用してUIを整えます。記事の趣旨を考慮し、Bootstrapの説明は省略します。
    <details><summary>BootstrapでUIを整えたコード</summary>
    
    ```form.html
    <!DOCTYPE html>
    <html lang="ja" xmlns:th="http://www.thymeleaf.org" xmlns:nl2br="https://github.com/bufferings/thymeleaf-extras-nl2br">
    <head><!-- 省略 --></head>
    <body>
    	<div class="container">
    		<h1 class="m-5">ChatGPT API Demo</h1>
    		<!-- 入力フォーム -->
    		<form th:action="@{/chat}" method="post" class="form-floating m-5">
    			<textarea class="form-control" id="userMessage" name="userMessage" style="height: 100px"></textarea>
    			<label for="userMessage">Enter your message</label>
    			<button type="submit" class="btn btn-outline-primary mt-3">Generate Response</button>
    		</form>
    
    		<!-- 結果表示エリア -->
    		<div class="m-5 p-3 bg-light rounded">
    			<h2 class="m-3">ChatGPT Response</h2>
    			<p class="message m-3" th:if="${apiMessage != null}" nl2br:text="${apiMessage}"></p>
    			<p class="message m-3" th:if="${apiMessage == null}">こんにちは。</p>
    		</div>
    	</div>
    </body>
    </html>
    ```
    </details>


# おわりに

OpenAI APIをSpringアプリケーションに組み込むことができました。
課題点として、ユーザーがレスポンスの待機中であることを認識しづらい点が挙げられます。今後は、ユーザーに待機中であることを伝える方法を調査し、実装する予定です。

お読みいただきありがとうございました。この記事が誰かのお役に立つことを祈っております。

# 参考文献

- [ChatGPT API Ajax連携 #Java - Qiita](https://qiita.com/oyaken0717/items/99e9ed6a14e5d7a905e7)
- [【2023年版】OpenAIのAPIキー発行・取得手順！ChatGPTやGPT-4、DALL-E3をAPI経由で利用可能 | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門 (auto-worker.com)](https://auto-worker.com/blog/?p=6988)
- [API Reference - OpenAI API](https://platform.openai.com/docs/api-reference/chat/create)
- [【Spring Boot】RestTemplateによるAPI呼び出し | b1san's Blog (b1san-blog.com)](https://b1san-blog.com/post/spring/spring-rest-template/)
- [GitHub - bufferings/thymeleaf-extras-nl2br: Thymelaef Dialect to support new line to <br>](https://github.com/bufferings/thymeleaf-extras-nl2br)