🐨

FlutterでChatGPTを使ったアプリを作る

2024/04/13に公開

実装の前に

今回はFlutterから直接GPTにリクエストを送りません。
以下の記事に記載されている通り、非推奨です。
今回はAzure OpenAI Serviceを使いますが同様のことが言えると思います。

https://zenn.dev/kawanji01/articles/909e68406d604c

また、GPTへのリクエスト以外にもRAGやDBとの連携もしていくことになるので、どっちみちAPIを作成した方がベストです。

使うサービス・技術

  1. Azure OpenAI Service(GPT4を使用)
  2. App Service
  3. FastAPI(Python)
  4. Flutter

インフラ環境の構築

1. AOAIの利用申請

サブスクリプションを作成して、以下のページから利用の申請をしてください。
おそらく2,3営業日後くらいに承認のメールが来ると思います。

https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUNTZBNzRKNlVQSFhZMU9aV09EVzYxWFdORCQlQCN0PWcu


2. AOAIリソース作成とGPTのデプロイ

リージョンは「Sweden Central」にします。


AI Studioに移動して、モデルのデプロイをします。
左サイドバーの「デプロイ」から、「新しいデプロイの作成」を選択


今回はGPT4-turboを使用します。
名前は適当でいいです。
記入したら「作成」を選択


API側の実装

1. 環境構築

今回はFastAPIを使用します。
超ミニマムに作っていきます。

  1. プロジェクト作成 ...mkdir gpt_api
  2. 実行ファイル作成...touch main.py
  3. 仮想環境を構築...python -m venv .venv
  4. 仮想環境を起動...source .venv/bin/activate
  5. パッケージを導入...pip install fastapi uvicorn dotenv openai==1.14.0


2. ロジックの実装

main.py
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


デプロイする場合は以下の記事を参考にやってみてください。
https://qiita.com/ikeike_ryuryu/items/a9996a11eae00b6b2ddf


4. アプリ側の実装

1. Flutterのプロジェクトを作成

  1. flutterのバージョン確認...flutter --version
    比較的新しいバージョンになっていたら大丈夫です。

  2. flutterの動作環境確認...flutter doctor
    Xcodeでエラーになってますが、Android環境では問題ないようなので、一旦このまま進めます。

  3. プロジェクト作成...flutter create ai_chat_app
    以下の感じになれば成功

  4. ローカルに起動...cd ai_chat_app & flutter run
    今回はAndroidエミュレーター上に立ち上げたいので、実行する前にエミュレーターを起動しておいてください。
    以下のような感じでアプリが立ち上がったらOK

2. 必要なパッケージの導入

HTTPリクエストをするので、パッケージを導入
flutter pub add http


一旦httpパッケージさえあれば動かせますが、グローバルにステートを管理したい方はriverpodを導入してください。
flutter pub add flutter_riverpod


3. UIのインプットエリアを実装

TextFieldを下側に配置します。

main.dart
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を使って一覧表示できるようにします。

main.dart
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メソッドを実行します。

main.dart
 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からのレスポンスのクラス定義

main.dart
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にリクエストを送る処理を実装します。

main.dart
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内で実行する関数を変更してください

main.dart
onSubmitted: (value) {
  // addUserText(value);
  requestMessageToGPTApi(value);
},


実際に動かしてみると、数秒後にレスポンスが返ってきました。
成功です。

次回

次はGPTからのレスポンスをストリーミングで受け取れるようにしようと思います。

ヘッドウォータース

Discussion