LoginSignup
0
1

Flutterでスマホアプリから各種AIとチャットする(GPT-4o、Gemini-1.5-flashの結果もあり)

Last updated at Posted at 2024-05-14

はじめに

本記事は以下(Flutterにフォーカス)の更に細かい部分である「AIとチャットする」部分にフォーカスした記事です。

本記事のアプリはあるシステム(以下)の一部です。

全体像の記事は以下で投稿しています。気になった方は一読お願いします
※アレクサとやり取りした動画も載せています

使用したAIとモデル

  • OpenAI GPT-3.5 Turbo
  • OpenAI GPT-4 Turbo
  • OpenAI GPT-4o
  • Google Gemini-1.0-pro-latest
  • Google Gemini-1.5-flash-latest
  • Google Gemini-1.5-pro-latest
  • Anthropic Claude Haiku
  • Anthropic Claude Sonnet
  • Anthropic Claude Opus

OpenAI GPT-4oのせいで

現時点で「コード紹介」の部分などが一部が完成していない状態です。
ただ、2024/05/14に公開されたOpenAIのGPT-4oがすごくて、記事が完成してないの公開してしまいました。。

能書きはいいから結果見せろ、と思った人は以下のリンクで結果にジャンプします
結果にジャンプ!

補足

本記事はスマホアプリの開発初心者が拙いながらもFlutterで作ったアプリ開発の一部を紹介する内容になっています。

特定のデザインパターンに基づいて実装もしていないため、Flutterのベストプラクティスとかけ離れている箇所も多々あると思います。ご容赦ください:bow:

対象読者

  • Flutterの開発に興味がある人
    →開発の雰囲気を感じられると思います
  • FlutterとAIを連携させたい人
    →各AIの連携に使用したパッケージ(外部ライブラリ)を紹介します
    →各AIのレスポンス時間も参考程度に記載しました

本記事の紹介範囲

  1. 開発環境
  2. 画面紹介

開発環境の紹介

  • MacBook
    2016年製の為、最新OSのサポートから外れてます
  • Flutter 3.19.6
  • Dart 3.3.4
  • VScode
  • 実機確認:Pixel7

リポジトリ

以下で公開してます:relaxed:
※とりあえず、本記事のバージョンは「develop_v2」ブランチです
アホみたいなブランチの切り方でごめんなさい:bow:

画面紹介

設定画面

以下の設定内容をチャット画面で使用しています

  • AIのレスポンス
    • 口調/キャラクター名
      各AIに共通する設定
  • 使用するAI
    • ChatGPT、Gemini、Claudeが選択可能
    • APIキー
      各AIのAPIキーを設定
    • モデル
      主要なモデルが選択可能

画面イメージ(GIF)

1000009171.gif

コード紹介

クリックで↓に展開します
setting_screen.dart

チャット画面

口調:ツンデレ
裏で文字数上限も付与してます。

具体的には以下のプロンプトで各AIに送信しています。

  • 回答は200文字以内
  • 口調は{ツンデレ}
  • チャットの入力内容

また、Alexaで使用する際は7〜8秒でタイムアウトしてしまうため、参考として「処理時間」もチャットに表示させています。

リクエスト結果

ChatGPT3.5Turbo

処理時間  : 4/5点
レスポンス: 3/5点
処理時間は早い方ですが、レスポンスは微妙ですね。(ツンデレ感0)

ChatGPT4-Turbo

処理時間  : 3/5点
レスポンス: 3/5点
処理時間は普通。レスポンスは微妙ですね。(ツンデレ感0)

ChatGPT4-o

2024/05/14に使えるようになった最新版です
流石ですね!
4-Turboの2倍早いと謳ってるだけあり、ガチで2倍早くなってます
ちょっと感動しました笑

処理時間  : 5/5点
レスポンス: 4/5点
処理時間がかなり早いです。
レスポンスも良い感じです。

Gemini-1.0-pro-latest

処理時間  : 4.5/5点
レスポンス: 4/5点
処理時間も早いです。レスポンスも良い感じですね。

Gemini-1.5-flash-latest

こちらも2024/05/14に使えるようになった最新版です
低遅延・低コストのモデルが売りなだけあってめちゃくちゃ早いです。

処理時間  : 6/5点
レスポンス: 4/5点
まさかの1秒台はビビりました。
ChatGPT4-oを超えです。
現時点ではAlexaでAI使うならこいつが最適解かもしれません。
レスポンスも良い感じです。

Gemini-1.5-pro-latest

処理時間  : 4.5/5点
レスポンス: 4/5点
処理時間も早いです。レスポンスも良い感じですね。

Claude(Haiku)

処理時間  : 1/5点
レスポンス: 5/5点
処理時間は遅いですね。Alexaだとタイムアウトします。
ただ、レスポンスは良い感じですね。

Claude(Sonnet)

処理時間  : 1/5点
レスポンス: 5/5点
処理時間は遅いですね。Alexaだとタイm( ry
※「プログラミングX言語」ってなんだ?

Claude(opus)

処理時間  : 1.5/5点
レスポンス: 5/5点
処理時間は遅いですね。Alexaだとタイm( ry
※「プログラミングC言語」ってなんだ?

まとめ

2024/05/15時点では個人的に「Gemini-1.5-flash」がベストですね。
Alexa(対話)で使うなら回答精度も大事ですが、何より早さが重要かと思いますので。

次点で「ChatGPT4-o」です。
Claudeは回答の精度は高いですが、遅すぎて残念ながらAlexaでは使えないですね。。。

コード紹介

UI部分

ポイント

  • flutter_chat_ui
    • チャット部分はこのパッケージがすべてよしなにやってくれます(めちゃくちゃ便利)
    • 初めは自前で実装してましたが、これに変えてからコードが80行くらい削減できました

コード

クリックで↓に展開します
chat_ai_screen.dart
// Flutterとその他のパッケージをインポート
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:math';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';

import 'package:alexa_to_ai/database/database.dart';
import 'package:alexa_to_ai/services/ai_service.dart';
import 'package:alexa_to_ai/widgets/alert_dialog.dart';

class ChatAIScreen extends StatefulWidget {
  const ChatAIScreen({super.key});

  // 画面名
  static String name = 'AIチャット画面';

  @override
  ChatAIScreenState createState() => ChatAIScreenState();
}

class ChatAIScreenState extends State<ChatAIScreen> {
  // 設定画面で保存した内容をローカルDBから取得
  final settingModel = settingModelBox.get(settingModelBoxKey);

  // メッセージを格納するリスト
  List<types.Message> messages = [];

  // AIのユーザ情報(名前に使用するAIの情報を使用するので後で初期化)
  late types.User _ai;
  // ユーザ情報
  final _user = const types.User(
    id: 'user',
  );

  late AIService aiService;

  // 画面描画後に1度だけ呼び出されるメソッド
  @override
  void initState() {
    super.initState();
    aiService = AIService();

    // チャットで表示するAIのユーザ情報を初期化
    _ai = types.User(
      id: 'ai',
      // 使用するAI
      firstName: settingModel!.selectedType,
      // AIのモデル
      lastName: settingModel!.getAIModel().model,
    );

    WidgetsBinding.instance.addPostFrameCallback((_) {
      // ローカルDBに未保存のない場合(初期インストール後に設定画面で保存してない場合など)
      // アラートを表示し、保存を促す
      if (!settingModel!.isSaved) {
        _showAlertDialog();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // アプリバーのタイトル以下のように使用中のAIを表示
        // AIチャット画面(ChatGPT)
        title: Text('${ChatAIScreen.name}(${settingModel!.selectedType})'),
      ),
      // 画面の主要な部分
      body: Chat(
        theme: const DefaultChatTheme(
          primaryColor: Colors.blueAccent, // メッセージの背景色の変更
          userAvatarNameColors: [Colors.blueAccent], // ユーザー名の文字色の変更
          //backgroundColor: Colors.black12, // チャット画面の背景色の変更
        ),
        user: _user,
        messages: messages,
        onSendPressed: _onPressedSendButton,
        showUserAvatars: true,
        showUserNames: true,
      ),
    );
  }

  /// 送信ボタンが押された際の処理
  void _onPressedSendButton(types.PartialText message) {
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );

    _addMessage(textMessage);

    // メッセージをAIに送信
    _sendMessageToAi(message.text);
  }

  void _addMessage(types.Message message) {
    setState(() {
      messages.insert(0, message);
    });
  }

  /// 設定が未保存の場合にアラートを表示
  void _showAlertDialog() {
    showDialog(
      context: context,
      builder: (context) {
        return CustomAlertDialog(
          titleValue: '設定が未保存です',
          contentValue: '設定画面でAIの口調などがカスタマイズできます',
          onOkPressed: () {
            Navigator.of(context).pop();
          },
        );
      },
    );
  }

  /// ユーザの入力文字列をAIに送信
  void _sendMessageToAi(String message) async {
    // ユーザの入力文字列に設定内容を付与し、AIに送信するプロンプトを作成
    String prompt = createPrompt(message);

    // AIの処理時間の計測開始
    Stopwatch timeTracker = Stopwatch()..start();

    // AIにリクエストを送信
    aiService.sendMessageToAi(prompt).then((responseText) {
      // AIからの返答をメッセージとして表示
      final aiTextMessage = types.TextMessage(
        author: _ai,
        createdAt: DateTime.now().millisecondsSinceEpoch,
        id: randomString(),
        text: responseText,
      );

      // AIの処理時間の計測終了
      timeTracker.stop();

      // 処理時間を秒単位で取得
      double time = timeTracker.elapsedMilliseconds / 1000;
      final timeTrackerTextMessage = types.TextMessage(
        author: _ai,
        createdAt: DateTime.now().millisecondsSinceEpoch,
        id: randomString(),
        text: '処理時間: $time秒',
      );
      // 処理時間を表示
      _addMessage(timeTrackerTextMessage);

      // AIのメッセージを表示
      _addMessage(aiTextMessage);
    });
  }

  String createPrompt(String message) {
    // 口調のプロンプト設定
    String aiTonePrompt = '';
    String aiTone = settingModel!.aiTone;
    if (aiTone.isNotEmpty) {
      aiTonePrompt = '口調は$aiTone。';
    }
    // TODO 最大文字数を設定画面でも可能に
    String maxCharLimit = '解答は200文字以内。';

    // ユーザの入力文字列に設定内容を付与し、AIに送信するプロンプトを作成
    String prompt = maxCharLimit + aiTonePrompt + message;
    return prompt;
  }

  String randomString() {
    final random = Random.secure();
    final values = List<int>.generate(16, (i) => random.nextInt(255));
    return base64UrlEncode(values);
  }
}

ロジック部分

ポイント

  • 送信先のAIは複数存在する(URLやパラメータも異なる)
    • 抽象クラスを用意し、AIごとに異なるRequest/Responseの処理は各AIの具象クラスに実装
    • enumで送信先のAIごとに分岐し、それぞれの具象クラスのインスタンスを生成
    • 生成した具象クラスの送信処理を実行

コード

クリックで↓に展開します
ai_service.dart
import 'package:alexa_to_ai/database/database.dart';
import 'package:alexa_to_ai/models/ai_model.dart';
import 'package:alexa_to_ai/services/ai_agent/ai_agent.dart';
import 'package:alexa_to_ai/services/ai_agent/chat_gpt_agent.dart';
import 'package:alexa_to_ai/services/ai_agent/claude_agent.dart';
import 'package:alexa_to_ai/services/ai_agent/gemini_agent.dart';
import 'package:flutter/material.dart';

class AIService {
  // AIのリクエストなどを管理するクラス(実行時に各AIの分岐で設定)
  late AIAgent _aiAgent;

  /// AIにリクエストを送信
  /// prompt: ユーザのチャット入力内容に「設定内容(キャラクター、口調など)」を付与した文字列
  Future<String> sendMessageToAi(String prompt) async {
    debugPrint('prompt=$prompt');

    // 設定画面で保存した内容をローカルDBから取得
    final settingModel = settingModelBox.get(settingModelBoxKey);
    AIModel aiModel = settingModel!.getAIModel();

    // 選択したAIの種別によって、処理を分ける
    switch (settingModel.getKeyFromSelectedType()) {
      case AITypes.chatGPT:
        debugPrint('call ChatGPTAgent');
        _aiAgent = ChatGPTAgent();
        break;
      case AITypes.gemini:
        debugPrint('call GeminiAgent');
        _aiAgent = GeminiAgent();
        break;
      case AITypes.claude:
        debugPrint('call ClaudeAgent');
        _aiAgent = ClaudeAgent();
        break;
    }
    // 各AIにリクエストを送信
    Future<String> responseText = _aiAgent.sendMessage(prompt, aiModel);
    return responseText;
  }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1