FlutterでChatGPTを使ったアプリを作る
実装の前に
今回はFlutterから直接GPTにリクエストを送りません。
以下の記事に記載されている通り、非推奨です。
今回はAzure OpenAI Serviceを使いますが同様のことが言えると思います。
また、GPTへのリクエスト以外にもRAGやDBとの連携もしていくことになるので、どっちみちAPIを作成した方がベストです。
使うサービス・技術
- Azure OpenAI Service(GPT4を使用)
- App Service
- FastAPI(Python)
- Flutter
インフラ環境の構築
1. AOAIの利用申請
サブスクリプションを作成して、以下のページから利用の申請をしてください。
おそらく2,3営業日後くらいに承認のメールが来ると思います。
2. AOAIリソース作成とGPTのデプロイ
リージョンは「Sweden Central」にします。
AI Studioに移動して、モデルのデプロイをします。
左サイドバーの「デプロイ」から、「新しいデプロイの作成」を選択
今回はGPT4-turboを使用します。
名前は適当でいいです。
記入したら「作成」を選択
API側の実装
1. 環境構築
今回はFastAPIを使用します。
超ミニマムに作っていきます。
- プロジェクト作成 ...
mkdir gpt_api
- 実行ファイル作成...
touch main.py
- 仮想環境を構築...
python -m venv .venv
- 仮想環境を起動...
source .venv/bin/activate
- パッケージを導入...
pip install fastapi uvicorn dotenv openai==1.14.0
2. ロジックの実装
import json
import pydantic
import os
import fastapi
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
from openai import AzureOpenAI
load_dotenv()
class Request(pydantic.BaseModel):
message: str
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.post("/chat")
async def chat_test(request: Request):
client = AzureOpenAI(
api_version="2023-12-01-preview",
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_key=os.environ["AZURE_OPENAI_KEY"],
)
messages = [
{"role": "system", "content": "あなたはサービス精神にあふれたAIアシスタントです。ユーザーからの質問にたくさん考えて丁寧な口調で答えます。"},
{"role": "user", "content": request.message}
]
response = client.chat.completions.create(
model=<your model name>,
messages=messages,
max_tokens=800,
temperature=0.7,
)
return {"finish_reason": response.choices[0].finish_reason, "role": response.choices[0].message.role, "message": response.choices[0].message.content}
3. 動作確認
uvicorn main:app --reload
でローカルに起動して、問題なく動けばOK
デプロイする場合は以下の記事を参考にやってみてください。
4. アプリ側の実装
1. Flutterのプロジェクトを作成
-
flutterのバージョン確認...
flutter --version
比較的新しいバージョンになっていたら大丈夫です。
-
flutterの動作環境確認...
flutter doctor
Xcodeでエラーになってますが、Android環境では問題ないようなので、一旦このまま進めます。
-
プロジェクト作成...
flutter create ai_chat_app
以下の感じになれば成功
-
ローカルに起動...
cd ai_chat_app & flutter run
今回はAndroidエミュレーター上に立ち上げたいので、実行する前にエミュレーターを起動しておいてください。
以下のような感じでアプリが立ち上がったらOK
2. 必要なパッケージの導入
HTTPリクエストをするので、パッケージを導入
flutter pub add http
一旦httpパッケージさえあれば動かせますが、グローバルにステートを管理したい方はriverpodを導入してください。
flutter pub add flutter_riverpod
3. UIのインプットエリアを実装
TextFieldを下側に配置します。
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final TextEditingController _textController = TextEditingController();
void initState() {
super.initState();
}
void dispose() {
_textController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 20),
),
Expanded(
child: Stack(
children: <Widget>[
Positioned(
bottom: 5,
left: 5,
right: 5,
child: Container(
height: 70,
width: 374,
child: TextField(
onSubmitted: (value) {
_textController.clear();
},
controller: _textController,
decoration: InputDecoration(
labelText: "Message",
fillColor: Colors.white,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide(color: Colors.white)),
)),
),
),
],
),
),
],
)),
);
}
}
こんなUIになればOK
3. UIのチャット一覧表示を実装
ListViewを使って一覧表示できるようにします。
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final TextEditingController _textController = TextEditingController();
// ↓追加
List<dynamic> messageList = [
{
"role": "user",
"message": "こんにちは",
},
{
"role": "copilot",
"message": "こんにちは!何かお手伝いできますか?",
},
];
void initState() {
super.initState();
}
void dispose() {
_textController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Container(
margin: const EdgeInsets.only(top: 20),
),
// ↓追加
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: messageList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
margin:
const EdgeInsets.only(right: 10, left: 10, bottom: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 1,
child: Container(
width: 20,
height: 20,
margin: EdgeInsets.only(right: 10),
decoration: BoxDecoration(
color: Colors.blue,
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(20),
),
),
),
Flexible(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(messageList[index]["role"]),
Text(
messageList[index]["message"],
softWrap: true,
),
],
),
)
],
));
},
),
Expanded(
child: Stack(
children: <Widget>[
Positioned(
bottom: 5,
left: 5,
right: 5,
child: Container(
height: 70,
width: 374,
child: TextField(
onSubmitted: (value) {
},
controller: _textController,
decoration: InputDecoration(
labelText: "Message",
fillColor: Colors.white,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide(color: Colors.white)),
)),
),
),
],
),
),
],
)),
);
}
}
こんなUIになればOK
5. テキストをチャット一覧に追加するロジック実装
setState
を使ってmessageListに追加します。
追加後は、入力フォームを空にしたいのでclearメソッドを実行します。
void addUserText(message) {
setState(() {
messageList.add({
"role": "user",
"message": message,
});
});
_textController.clear();
}
Expanded(
child: Stack(
children: <Widget>[
Positioned(
bottom: 5,
left: 5,
right: 5,
child: Container(
height: 70,
width: 374,
child: TextField(
onSubmitted: (value) {
// 追加
addUserText(value);
},
controller: _textController,
decoration: InputDecoration(
labelText: "Message",
fillColor: Colors.white,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(40),
borderSide: BorderSide(color: Colors.white)),
)),
),
),
],
),
),
こんな感じで入力したテキストが追加されればOK
6. GPTからのレスポンスのクラス定義
class GPTResponse {
GPTResponse({
required this.message,
required this.role,
required this.finish_reason,
});
String message;
String role;
String finish_reason;
factory GPTResponse.fromJson(Map<String, dynamic> json) => GPTResponse(
message: utf8.decode(json["message"].runes.toList()),
role: json["role"],
finish_reason: json["finish_reason"],
);
Map<String, dynamic> toJson() => {
"message": message,
"role": role,
"finish_reason": finish_reason,
};
}
7. APIへリクエストを送るロジックを実装
APIにリクエストを送る処理を実装します。
Future<void> requestMessageToGPTApi(requestMessage) async {
setState(() {
messageList.add({
"role": "user",
"message": requestMessage,
});
});
_textController.clear();
final url = Uri.parse("<your endpoint>");
Map<String, String> headers = {
'content-type': 'application/json; charset=UTF-8'
};
final body = json.encode({'message': requestMessage});
final response = await http.post(url, body: body, headers: headers);
final response_json = GPTResponse.fromJson(json.decode(response.body));
if (response.statusCode == 200) {
setState(() {
messageList.add({
"role": "copilot",
"message": response_json.message,
});
});
}
}
onSubmit内で実行する関数を変更してください
onSubmitted: (value) {
// addUserText(value);
requestMessageToGPTApi(value);
},
実際に動かしてみると、数秒後にレスポンスが返ってきました。
成功です。
次回
次はGPTからのレスポンスをストリーミングで受け取れるようにしようと思います。
Discussion