Flutterでスマホアプリから各種AIとチャットする

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

https://qiita.com/parayan0429/items/299ddb48db4dafe9df28

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

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

:::

https://qiita.com/parayan0429/items/06c5350788afe6655171

## 使用したAIとモデル
- OpenAI　GPT-3.5 Turbo
- OpenAI　GPT-4 Turbo
- OpenAI　GPT-4o
- OpenAI　GPT-4o-mini
- OpenAI　o1-mini
- OpenAI　o1-preview
- Google Gemini-1.0-pro-latest
- Google Gemini-1.5-flash-latest
- Google Gemini-1.5-flash-8b-latest
- Google Gemini-1.5-pro-latest
- Google Gemini-Exp-1114
- Anthropic Claude-3-haiku
- Anthropic Claude-3-sonnet
- Anthropic Claude-3-opus
- Anthropic Claude-3.5-haiku
- Anthropic Claude-3.5-sonnet

### OpenAI　o1系モデルの開放について
o1系モデルは「条件を満たした課金者」のみに開放されていましたが、2024/11/20で課金した人全員に開放されたようです
私も11/20 14:40に開放メールが届きました
ずっと使ってみたかったので早速使ってみました:v:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/06233c9a-b44c-62b9-d9b5-e3b578082d76.png)

#### 条件を満たした課金者

https://qiita.com/youtoy/items/52b115700d4448a1a929

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

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

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

# 本記事の紹介範囲
1. 開発環境
1. 画面紹介

# 開発環境の紹介
- MacBook
2016年製の為、最新OSのサポートから外れてます
- Flutter 3.19.6
- Dart 3.3.4
- VScode
- 実機確認：Pixel7

# 画面紹介
## 設定画面
以下の設定内容をチャット画面で使用しています
- AIのレスポンス
    - 口調/キャラクター名
      各AIに共通する設定
- 使用するAI
    - ChatGPT、Gemini、Claudeが選択可能
    - APIキー
      各AIのAPIキーを設定
    - モデル
      主要なモデルが選択可能

### 画面イメージ(GIF)
![1000009171.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/f7bf94db-6eff-dbcf-cf14-0a357e06ddcc.gif)

### コード紹介

<details><summary>クリックで↓に展開します</summary>

```setting_screen.dart

```

</details>

# チャット画面
口調:ツンデレ
裏で文字数上限も付与してます。

具体的には以下のプロンプトで各AIに送信しています。
- 回答は200文字以内
- 口調は{ツンデレ}
- チャットの入力内容

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

## リクエスト結果
### ChatGPT3.5Turbo
処理時間 　: 4/5点
レスポンス: 3/5点
処理時間は早い方ですが、レスポンスは微妙ですね。(ツンデレ感0)
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/2ffa90bc-898a-ef09-8aff-4988175da2fc.png"/>

### ChatGPT4-Turbo
処理時間 　: 3/5点
レスポンス: 3/5点
処理時間は普通。レスポンスは微妙ですね。(ツンデレ感0)
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/4447c46d-207f-60e5-edb3-512f7005a2de.png"/>

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

処理時間 　: 5/5点
レスポンス: 4/5点
処理時間がかなり早いです。
レスポンスも良い感じです。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/09aa585d-d90d-fb9a-6fb3-1d2203eb103d.png"/>

### ChatGPT-4o-mini
2024/07/18リリース
4oと同等の性能です。
それでいてコストが95%以上安くなってます。
お得すぎでは？
※github Actionsでプルリク時のコードレビューに4oを使ってましたが、即miniに変えました。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/87866461-44c2-52a9-f194-c75b04abeefd.png"/>

処理時間 　: 5/5点
レスポンス: 5/5点
処理時間がかなり早いです。
レスポンスも良い感じです。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/5226dd06-210e-1aa9-8c1c-48656753a7e0.png"/>

### ChatGPT o1-preview
2024/09/12リリース
高度な推論力と複雑な問題解決に強いです。
ただし、速度とコスト面に難あり・・・

:::note warn
同じリクエストでもtoken使用量が膨大で、推論するので仕方ないかもしれませんが、4o-miniの10倍以上を消費してました。
- o1-preview：1790
- 4o-mini：160

レスポンスも13秒とだいぶ遅く、アレクサスキルでの使用には向かないことがわかりました
※元々は科学研究、複雑な数理モデルの使用を考えられているようなので完全に畑が違いましたね
:::

処理時間 　: 1/5点
レスポンス: 5/5点
処理時間がかなり遅いです。
レスポンスはかなり良い感じです。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/c9504e24-6528-e079-f707-60ce5ae013fb.png"/>


### ChatGPT o1-mini
2024/09/12リリース
o1シリーズの基本的な能力（思考力、推論）を継承しつつ、処理速度と効率性を向上させたモデルです。
ただしコスト面に難あり・・・

:::note warn
同じリクエストでもtoken使用量が多く、推論するので仕方ないかもしれませんが、4o-miniの3.5倍以上を消費してました。
- o1-mini：569
- 4o-mini：160

コストはそれなりにかかりますが、より正確な情報がほしい場合にはいいかもしれないです
:::

処理時間 　: 4/5点
レスポンス: 4/5点
処理時間は悪くないです。
レスポンスはシンプルですが、悪くないです
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/38045fed-5e70-2d63-72ca-6e852d27a8c3.png"/>


### Gemini-1.0-pro-latest
処理時間 　: 4.5/5点
レスポンス: 4/5点
処理時間も早いです。レスポンスも良い感じですね。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/07e6689b-3c03-ba79-4221-8f197452baed.png"/>


### Gemini-1.5-flash-latest

2024/05/14リリース
低遅延・低コストのモデルが売りなだけあってめちゃくちゃ早いです。

処理時間 　: 6/5点
レスポンス: 4/5点
まさかの1秒台はビビりました。
ChatGPT4o-mini超えです。
レスポンスも良い感じです。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/0c6d0106-7ac5-02e1-c7d3-04d00875a130.png
"/>

### Gemini-1.5-flash-8b-latest

2024/10/03リリース
1.5-flashより「50%値下げ」、「より早い」とのことです。
実際早いです。

処理時間 　: 6.5/5点
レスポンス: 4/5点
更に早くなってました。阿部寛さんのHPを超える日は来るのか。。。
レスポンスも良い感じです。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/f4c90547-e286-eada-ba99-19de126d8113.png"/>


### Gemini-1.5-pro-latest
処理時間 　: 4.5/5点
レスポンス: 4/5点
処理時間も早いです。レスポンスも良い感じですね。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/aad3946f-b8c5-3f18-2a60-ea51599d56aa.png"/>


### Gemini-Exp-1114
2024/11/15リリース
高度な推論力と複雑な問題解決に強いです。

処理時間 　: 2/5点
レスポンス: 5/5点
処理時間は遅いですが、レスポンスはかなり良い感じですね。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/0b06057b-d4a0-0f70-ee6f-3ea4ab55cab9.png"/>


### Claude-3-haiku
処理時間 　: 5/5点
レスポンス: 4/5点
処理時間は早く、レスポンスも良い感じですね。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/86950495-e3ad-d3eb-81c3-182b981c20f4.png"/>


### Claude-3-sonnet
処理時間 　: 3/5点
レスポンス: 3/5点
処理時間は若干遅めですね。レスポンスも口調がイマイチ定まってないですね。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/e48c44e3-02d3-a6f6-0587-4ff3d160811a.png"/>

### Claude-3-opus
処理時間 　: 1/5点
レスポンス: 4.5/5点
処理時間は一番遅いですね。Alexaだと余裕でタイムアウトします。
ただ、レスポンスで口調はいい感じですが、「ダーツ社」ではなく「Google社」なのでそのミスが残念。
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/79e36bc4-2830-bd26-7157-c2c1fc8de9b1.png"/>


### Claude-3.5-haiku
2024/10/22リリース
処理時間 　: 4/5点
レスポンス: 4/5点
処理時間は3-haikuより1秒近く遅くなりました。※結構残念
レスポンスは少し短くなり、個人的には改善されたかと思います。
何故か末尾に絵文字がありました
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/2c720684-0ba7-34c3-33f5-4cbc4ca84725.png"/>


### Claude-3.5-sonnet
処理時間 　: 3/5点
レスポンス: 4.5/5点
処理時間は3-sonnetと変わらないですね。レスポンスの口調はいい感じに改善されてます。(安定性が上がった？)
<img width="370" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569056/5c1bd113-4f95-7b38-4600-c450c271c4ba.png"/>


### まとめ
2024/11/20時点では個人的に「Gemini-1.5-flash-8b」がベストですね。
Alexa(対話)で使うなら回答精度も大事ですが、何より早さが重要かと思いますので。
※何より、制限があるにしろ「APIが無料で使用」できるのが素晴らしい

次点で「ChatGPT-4o-mini」で、その次が「Claude-3-haiku」という感じでしょうか。

## コード紹介

### UI部分

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


#### コード
<details><summary>クリックで↓に展開します</summary>

```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);
  }
}
```

</details>

### ロジック部分

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


#### コード
<details><summary>クリックで↓に展開します</summary>

```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;
  }
}
```

</details>