🐙

FlutterアプリにChatGPT APIを組み込んでみた

2023/03/04に公開
2

はじめに

ChatGPT使っていますか?自分はさっそくChatGPT Plusを契約してしまいました💸

話題のChatGPTですが3/2にAPIが公開されました。
今回はFlutterからChatGPT APIを利用していこうと思います。

作ったもの

ChatGPTとやりとりできるシンプルなアプリです。

メッセージを送るとChatGPTが答えてくれています。
また、もう一度メッセージを送ったときも前回の会話に基づいた返答になっています👍

ChatGPTとは?

OpenAIが公開しているチャット用にチューニングされた言語モデル

実践編

FlutterからChatGPT APIを利用する

OpenAIの公開しているドキュメントがあるので、httpパッケージやdioパッケージを利用して叩くことができます。

https://platform.openai.com/docs/api-reference/chat

また、非公式ですがdart_openaiという、OpenAIの公開しているAPIをdartから簡単に扱えるパッケージがあります。
更新頻度高く良さそうなのと、自前でモデル定義するより楽そうだったので、今回はこちらを使っていきます。

API Keyを取得する

以下のページからキーを取得しましょう。(初回は登録が必要かもしれません)
https://platform.openai.com/account/api-keys

dart_openaiを使う準備

mainでOpenAI.apiKeyに先程取得したキーを設定します

import 'package:dart_openai/openai.dart';

void main() {
  OpenAI.apiKey = キー;
  runApp(const MainApp());
}

キーは秘匿情報なのでflutter_dotenvなどを使って、publicリポジトリ等で公開しないよう気をつけましょう。
以下は自分がキーの設定を実装したときのコミットです。
https://github.com/K9i-0/flutter_chatgpt_sample/commit/2a558db3b716f8fd0756695b1ef18bb0326172a3

ChatGPT APIのざっくりとした仕組み

ChatGPT APIはモデルとメッセージのリストを指定してAPIを叩きます。

モデル

GPTのモデルです、現状gpt-3.5-turboを設定すれば良いです。

メッセージのリスト

メッセージはロールとコンテンツに別れます。

コンテンツはテキストです。
ロールはメッセージの送信者に相当し、assistant, user, systemの3種類があります。

  • assistant : ChatGPTのメッセージ
  • user : 人間のメッセージ
  • system : 調整用メッセージ

複数回のやりとりを行うとき、これまでのメッセージも含めてAPIを叩くことで、文脈を踏まえた回答をしてくれます。
調整用メッセージは「あなたはリードエンジニアです」など、会話の前提を設定するのに使います。こちらはUIには表示しないのが良さそうです。

他にもパラメーターはあるので、詳しくは公式ドキュメントを参照してください。
https://platform.openai.com/docs/api-reference/chat

日本語だとこの記事がわかりやすそうでした。
https://ai-create.net/magazine/2023/03/02/post-10647/

dart_openaiでChatGPT APIを叩く

OpenAI.instance.chat.createというメソッドを叩けばよいです。

先ほど説明したモデルとメッセージのリストを指定しています。

OpenAIChatCompletionModel chatCompletion = await OpenAI.instance.chat.create(
    model: "gpt-3.5-turbo",
    messages: [
      OpenAIChatCompletionChoiceMessageModel(
            content: "hello, what is Flutter and Dart ?",
            role: "user",
        ),
    ],
);

チャットUI作ってみた

おなじみのriverpodを使って画面を作ってみました。

Provider実装

riverpodを使って、ChatGPTとやりとりしたメッセージのリストを状態に持ち、新しいメッセージを送れるNotifierProviderを作ります。
以下、実装コードと解説です。

初期化時は空のリストにしています。systemロールのメッセージを指定しても良いかもしれません。

sendMessageでは以下の処理を行います。

  1. 新規メッセージのモデルを作る
  2. stateに1で作ったメッセージを反映(ChatGPT待ちの前に、UIに送信メッセージを反映する)
  3. APIを叩く
  4. 結果をstateに反映

class Messages extends _$Messages {
  
  List<OpenAIChatCompletionChoiceMessageModel> build() => [];

  Future<void> sendMessage(String message) async {
    // メッセージをuserロールでモデル化
    final newUserMessage = OpenAIChatCompletionChoiceMessageModel(
      content: message,
      role: 'user',
    );
    // メッセージを追加
    state = [
      ...state,
      newUserMessage,
    ];
    // ChatGPTに聞く
    final chatCompletion = await OpenAI.instance.chat.create(
      model: 'gpt-3.5-turbo',
      // これまでのやりとりを含めて送信
      messages: state,
    );
    // 結果を追加
    state = [
      ...state,
      chatCompletion.choices.first.message,
    ];
  }
}

Widget実装

先程のProviderを使うWidgetを作ります。

ざっくり仕様

  • メッセージ表示部
    • ChatGPTとユーザーのメッセージを一覧表示する
    • ChatGPTとユーザーでメッセージの色を変える
    • systemのメッセージは表示しない
  • 入力フォーム
    • 入力があれば送信ボタンが押せる
    • ChatGPT待ち中は、送信ボタンの代わりにローディングを表示する

class HomeScreen extends HookConsumerWidget {
  const HomeScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final messages = ref.watch(messagesProvider);
    final messageController = useTextEditingController(text: 'Flutterとはなんですか?');
    final screenWidth = MediaQuery.of(context).size.width;
    final isWaiting = useState(false);

    return Scaffold(
      appBar: AppBar(
        title: const Text('ChatGPT Sample'),
      ),
      body: SafeArea(
        child: Column(
          children: [
            // メッセージ一覧
            Expanded(
              child: ListView.separated(
                keyboardDismissBehavior:
                    ScrollViewKeyboardDismissBehavior.onDrag,
                padding: const EdgeInsets.symmetric(horizontal: 16),
                itemCount: messages.length,
                itemBuilder: (context, index) {
                  final message = messages[index];
                  // systemロールのメッセージは表示しない
                  if (message.role == 'system') {
                    return const SizedBox();
                  }

                  return Align(
                    key: Key(message.hashCode.toString()),
                    alignment: message.role == 'user'
                        ? Alignment.centerRight
                        : Alignment.centerLeft,
                    child: ConstrainedBox(
                      constraints: BoxConstraints(
                        maxWidth: screenWidth * 0.8,
                      ),
                      child: DecoratedBox(
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(8),
                          color: message.role == 'user'
                              ? context.colorScheme.primary
                              : context.colorScheme.secondary,
                        ),
                        child: Padding(
                          padding: const EdgeInsets.symmetric(
                            vertical: 8,
                            horizontal: 16,
                          ),
                          child: Text(
                            message.content,
                            style: TextStyle(
                              color: message.role == 'user'
                                  ? context.colorScheme.onPrimary
                                  : context.colorScheme.onSecondary,
                            ),
                          ),
                        ),
                      ),
                    ),
                  );
                },
                separatorBuilder: (context, index) =>
                    const SizedBox(height: 16),
              ),
            ),
            // 送信フォーム
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: DecoratedBox(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(
                    color: context.colorScheme.primary,
                    width: 2,
                  ),
                ),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    Expanded(
                      child: TextField(
                        controller: messageController,
                        maxLines: null,
                        decoration: InputDecoration(
                          hintText: 'メッセージを入力',
                          hintStyle: TextStyle(
                            color: Theme.of(context)
                                .colorScheme
                                .onBackground
                                .withOpacity(0.6),
                          ),
                          contentPadding: const EdgeInsets.symmetric(
                            vertical: 12,
                            horizontal: 16,
                          ),
                          border: InputBorder.none,
                        ),
                      ),
                    ),
                    const SizedBox(width: 8),
                    // 送信ボタン
                    if (!isWaiting.value)
                      IconButton(
                        onPressed: () async {
                          if (messageController.text.isEmpty) {
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                content: Text('メッセージを入力してください'),
                              ),
                            );
                          } else {
                            final sendMessage =
                                ref.read(messagesProvider.notifier).sendMessage(
                                      messageController.text,
                                    );
                            isWaiting.value = true;
                            messageController.clear();
                            await sendMessage;
                            isWaiting.value = false;
                          }
                        },
                        icon: !isWaiting.value
                            ? const Icon(Icons.send)
                            : const SizedBox(
                                width: 16,
                                height: 16,
                                child: CircularProgressIndicator(),
                              ),
                      ),
                    // 送信中
                    if (isWaiting.value)
                      const IconButton(
                        onPressed: null,
                        icon: SizedBox(
                          width: 18,
                          height: 18,
                          child: CircularProgressIndicator(),
                        ),
                      ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

完成🥳

ここまでで冒頭のアプリが作れます。

まとめ

FlutterアプリにChatGPTを組み込んでみました。
かなりシンプルな例でしたが、工夫次第で夢が広がりそうですね。
今回はdart_openaiを使った例ですが、APIのパラメーターに関する知識など他の言語でも活かせるので、まずはFlutterで遊んでみるのも良いのではないでしょうか?

サンプルを実装したリポジトリを公開しているので、煮るなり焼くなり使ってみてください。

https://github.com/K9i-0/flutter_chatgpt_sample

GitHubで編集を提案

Discussion