FlutterアプリにChatGPT APIを組み込んでみた
はじめに
ChatGPT使っていますか?自分はさっそくChatGPT Plusを契約してしまいました💸
話題のChatGPTですが3/2にAPIが公開されました。
今回はFlutterからChatGPT APIを利用していこうと思います。
作ったもの
ChatGPTとやりとりできるシンプルなアプリです。
メッセージを送るとChatGPTが答えてくれています。
また、もう一度メッセージを送ったときも前回の会話に基づいた返答になっています👍
ChatGPTとは?
OpenAIが公開しているチャット用にチューニングされた言語モデル
実践編
FlutterからChatGPT APIを利用する
OpenAIの公開しているドキュメントがあるので、httpパッケージやdioパッケージを利用して叩くことができます。
また、非公式ですがdart_openaiという、OpenAIの公開しているAPIをdartから簡単に扱えるパッケージがあります。
更新頻度高く良さそうなのと、自前でモデル定義するより楽そうだったので、今回はこちらを使っていきます。
API Keyを取得する
以下のページからキーを取得しましょう。(初回は登録が必要かもしれません)
dart_openaiを使う準備
mainでOpenAI.apiKeyに先程取得したキーを設定します
import 'package:dart_openai/openai.dart';
void main() {
OpenAI.apiKey = キー;
runApp(const MainApp());
}
キーは秘匿情報なのでflutter_dotenvなどを使って、publicリポジトリ等で公開しないよう気をつけましょう。
以下は自分がキーの設定を実装したときのコミットです。
ChatGPT APIのざっくりとした仕組み
ChatGPT APIはモデルとメッセージのリストを指定してAPIを叩きます。
モデル
GPTのモデルです、現状gpt-3.5-turboを設定すれば良いです。
メッセージのリスト
メッセージはロールとコンテンツに別れます。
コンテンツはテキストです。
ロールはメッセージの送信者に相当し、assistant, user, systemの3種類があります。
- assistant : ChatGPTのメッセージ
- user : 人間のメッセージ
- system : 調整用メッセージ
複数回のやりとりを行うとき、これまでのメッセージも含めてAPIを叩くことで、文脈を踏まえた回答をしてくれます。
調整用メッセージは「あなたはリードエンジニアです」など、会話の前提を設定するのに使います。こちらはUIには表示しないのが良さそうです。
他にもパラメーターはあるので、詳しくは公式ドキュメントを参照してください。
日本語だとこの記事がわかりやすそうでした。
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では以下の処理を行います。
- 新規メッセージのモデルを作る
- stateに1で作ったメッセージを反映(ChatGPT待ちの前に、UIに送信メッセージを反映する)
- APIを叩く
- 結果を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で遊んでみるのも良いのではないでしょうか?
サンプルを実装したリポジトリを公開しているので、煮るなり焼くなり使ってみてください。
Discussion
1.6.0でStream対応しました🥳
https://github.com/84d010m08 さん誤字の修正ありがとうございます〜