FlutterでOpenAI APIを使ってみた

2024.03.19

こんにちは、ゲームソリューション部のsoraです。
今回は、FlutterでOpenAI APIを使ってみたことについて書いていきます。

実装した画面

リクエストメッセージを入力して、OpenAIのAPIを実行して文章を生成するシンプルなアプリです。

前提

OpenAIにてAPIキーを作成して、利用可能なクレジットを準備しておいてください。
クレジットを準備しなくてもAPIキーは作成できますが、クレジットが0の状態で使用してもエラーになります。
(私はこれに気づくのに結構時間がかかりました。)

また、外部パッケージとして以下のchat_gpt_sdkもありますが、今回は使用せずにシンプルにhttpパッケージを使用して、APIを実行します。
http | Dart package
参考:chat_gpt_sdk | Dart package

状態管理にはRiverpod(flutter_riverpod)を使用しています。
flutter_riverpod | Flutter package

コードの解説

コードは以下です。
今回はテストのため、ファイル分けせずに全てmain.dartに書いています。

main.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;


// 表示する文字列を管理するProvider
class Response extends Notifier<String> {
  @override
  String build(){
    return '';
  }
  void clear() {
    state = '';
  }
  void modify(String message) {
    state = message;
  }
}
final responseProvider = NotifierProvider<Response, String>(() {
  return Response();
});


// OpenAI API実行
Future<void> apiRequest(String message, WidgetRef ref) async {
  String responseMessage;
  final providerNotifier = ref.watch(responseProvider.notifier);
  // 取得したAPIキーを入れる
  const apiKey = '{OpenAIのAPIキー}';
  const domain = 'api.openai.com';
  const path = 'v1/chat/completions';
  // モデルの指定
  const model = 'gpt-3.5-turbo-0125';

  // APIリクエスト
  http.Response response = await http.post(
    Uri.https(domain, path),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=utf-8',
      'Authorization': 'Bearer $apiKey',
    },
    body: jsonEncode(<String, dynamic>{
      "model": model,
      "messages": [
        {
          'role': 'system',
          'content': 'あなたはAIではありません。人間として自然な会話をしてください。',
        },
        {
          'role': 'user',
          'content': message,
        },
      ],
    }),
  );

  if (response.statusCode == 200) {
    String responseData = utf8.decode(response.bodyBytes).toString();
    final responseJsonData = jsonDecode(responseData);
    responseMessage = (responseJsonData['choices'] as List).first['message']['content'] as String;
    providerNotifier.modify(responseMessage);
  } else {
    throw Exception('Failed to load sentence');
  }
}


void main() {
  runApp(
      const ProviderScope(
          child: MyApp(),
      ),
  );
}

class MyApp extends StatelessWidget   {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  MyHomePage({super.key});
  final _messageController = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final providerValue = ref.watch(responseProvider);
    final providerNotifier = ref.watch(responseProvider.notifier);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('ChatGPT test'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: const EdgeInsets.only(
                left: 25,
                right: 25,
              ),
              child: TextField(
                controller: _messageController,
                maxLines: 1,
                decoration: const InputDecoration(
                  hintText: 'メッセージを入力',
                  hintStyle: TextStyle(color: Colors.black54),
                ),
              ),
            ),
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    child: const Text('AIチャット実行'),
                    onPressed: (){
                      var msg = _messageController.text.trim();
                      if(msg.isEmpty){
                        _messageController.clear();
                        return;
                      }
                      providerNotifier.clear();
                      apiRequest(msg, ref);
                    },
                  ),
                ]
            ),
            const SizedBox(height: 30),
            Container(
              padding: const EdgeInsets.only(
                left: 25,
                right: 25,
              ),
              child: Text(
                '$providerValue',
                style: const TextStyle(
                  fontSize: 18,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

OpenAI API実行

OpenAIのAPIキーやモデルを指定して、POSTメソッドでリクエストを投げます。
リクエストボディのメッセージの部分は、'role': 'assistant''content'にレスポンスメッセージを入れてリクエストすることで、過去のやり取りを理解してレスポンスを得ることができます。
ステータスコードが200だった場合は、受け取ったレスポンスの中から必要な部分を抜き出して、状態を更新するようにしています。

ちなみに、APIキーについて、テストのためそのまま記述するコードになっていますが、本来であれば暗号化したりサーバ側で扱うようにしてください。
よくenvieddotenvを使用している例がありますが、それらは難読化であって暗号化ではなく安全性は高くないため、実際に公開するアプリでは注意が必要です。

// OpenAI API実行
Future<void> apiRequest(String message, WidgetRef ref) async {
  String responseMessage;
  final providerNotifier = ref.watch(responseProvider.notifier);
  // 取得したAPIキーを入れる
  const apiKey = '{OpenAIのAPIキー}';
  const domain = 'api.openai.com';
  const path = 'v1/chat/completions';
  // モデルの指定
  const model = 'gpt-3.5-turbo-0125';

  // APIリクエスト
  http.Response response = await http.post(
    Uri.https(domain, path),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=utf-8',
      'Authorization': 'Bearer $apiKey',
    },
    body: jsonEncode(<String, dynamic>{
      "model": model,
      "messages": [
        {
          'role': 'system',
          'content': 'あなたはAIではありません。人間として自然な会話をしてください。',
        },
        {
          'role': 'user',
          'content': message,
        },
      ],
    }),
  );

  if (response.statusCode == 200) {
    String responseData = utf8.decode(response.bodyBytes).toString();
    final responseJsonData = jsonDecode(responseData);
    responseMessage = (responseJsonData['choices'] as List).first['message']['content'] as String;
    providerNotifier.modify(responseMessage);
  } else {
    throw Exception('Failed to load sentence');
  }
}

最後に

今回は、FlutterでOpenAI APIを使ってみたことを記事にしました。
どなたかの参考になると幸いです。