26
17

More than 1 year has passed since last update.

ChatGPTを使って「ChatGPTとチャットするアプリ」を作ってみた

Last updated at Posted at 2023-04-20

株式会社Neverのshoheiです。

株式会社Neverは「NEVER STOP CREATE 作りつづけること」をビジョンに掲げ、理想を実現するためにプロダクトを作り続ける組織です。モバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にお問合せください。
https://neverjp.com/contact/

概要

巷で話題のChatGPTを使ってアプリを作ってみたという企画です。

ChatGPTについて

ChatGPTの概要について聞いてみました。

ChatGPTへやりたいことや質問をすると、欲しい情報が手に入ったり、ヒントを教えてくれます。また、めんどくさい作業をお願いすると、それを代わりにやってくれる便利なツールです。

めんどくさい作業の例. 47都道府県の番号と名称データの列挙など

ChatGPTを触ってみた感想

エンジニアは新しい技術を習得する際や、課題調査をする際によくGoogle先生にお世話になります。Googleで検索してヒットした内容を自分で精査して欲しい情報に辿り着くというものです。

ChatGPTの登場で、その「ヒットした内容を自分で精査する」という事がショートカットされ、すぐに欲しい情報に辿り着けるようになりました。ただ、情報の信憑性については、ChatGPT自体の学習タイミングによりますので、古い情報が提供されることがあり、特にSDKのIFは古いものを教えてくれることがよくありました。

だとしても、知らないことを学ぶ取っ掛かりツールとしてはかなり強力で、知らない世界へ前線突破してくれるChatGPTは仕事のパートナーです。

  1. ChatGPTにやりたいことを伝える
  2. ChatGPTがサンプルコードと知見を教えてくれる(実際に動かすが、動かない)
  3. ChatGPTにサンプルコードが間違っていることを伝える
  4. ChatGPTが別の手段を教えてくれる(動く場合もあるし、動かない場合もある)
  5. ChatGPTが教えてくれた内容をヒントに、GoogleやGithubで正しい情報を取りにいく

このような手順で私は使っています。
この手順で、ChatGPTに実際にアプリ作ってもらうとどうなるのか気になり、

ChatGPTを使って「ChatGPTとチャットするアプリ」を作ってみました。

言語は Flutter(Dart)です。

完成したソースコードはこちら

ChatGPTのAPIを使う準備

APIキーの取得

ChatGPTのAPIを使うために、APIキーを取得します。
https://platform.openai.com/

右上のアイコンより、View API Keysを選択

左メニューより、USER -> API Keys -> + Create new secret key を選択

ここに表示されるAPIキーを使ってChatGPTとやり取ります。

APIの情報

APIの情報はここから確認できます。
https://platform.openai.com/docs/api-reference/chat

今回はChatGPTと対話をするアプリなのでhttps://api.openai.com/v1/chat/completions を使います。

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "Hello!"}]
  }'

OPENAI_API_KEYに先ほど取得したAPIキーをセットすると利用できます。

ChatGPTにアプリを実装してもらう

ChatGPTのAPIとやり取りする部分を実装

まずは、APIと直接やりとりする部分を実装してもらいましょう。
スクリーンショット 2023-04-20 10.10.13.png
ChatGPTのアウトプットがこちら

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> main() async {
  final apiKey = 'your_openai_api_key';
  final prompt = 'サンプルの質問';

  final response = await chatGPT(apiKey, prompt);
  print('GPT-4 Response: $response');
}

Future<String> chatGPT(String apiKey, String prompt) async {
  final url = 'https://api.openai.com/v1/engines/davinci-codex/completions';
  final headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $apiKey',
  };
  final body = json.encode({
    'prompt': prompt,
    'max_tokens': 50,
    'n': 1,
    'stop': null,
    'temperature': 0.8,
  });

  final response = await http.post(Uri.parse(url), headers: headers, body: body);
  final jsonResponse = json.decode(response.body);

  if (response.statusCode == 200) {
    return jsonResponse['choices'][0]['text'].trim();
  } else {
    throw Exception('Failed to get response from ChatGPT: ${response.body}');
  }
}

https://api.openai.com/v1/chat/completions のサンプルコードがほしかったのですが、違うAPIを教えてくれました。

聞き方が悪いのか、APIのURLも記載して質問しましたが、欲しい情報が取得できず。
スクリーンショット 2023-04-20 10.19.04.png
このAPIのbeta版が非推奨という扱いなのかわかりませんが、API部分は手動で修正しました。

修正後のソースコード

ファイル名を決めてもらう

ファイル名を聞きました。
スクリーンショット 2023-04-20 10.20.37.png

素晴らしい。chat_gpt_service.dartにします。

レスポンスのデータクラスを作ってもらう

freezedが好きなので、freezedのデータクラスを作ってもらいました。
スクリーンショット 2023-04-20 10.23.16.png

長くなるので割愛しますが、ちゃんとfreezedのデータクラスを作ってくれました🎉

APIキーを扱う術を教えてもらう

オープンソース上でAPIキーがバレるとまずいので、それを管理する方法を教えてもらいました。
スクリーンショット 2023-04-20 10.26.24.png
flutter_dotenvを用いて、環境変数にAPIキーをセットして用いる方法ですね(6.0.0は執筆時は無かったけど)

プロジェクトのルートに.envファイルを作成して、OPENAI_API_KEYの環境変数にAPIキーを定義します。

OPENAI_API_KEY=your_openai_api_key

キーの使い方まで教えてくれました。

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<ChatGPTResponse> chatGPT(String prompt) async {
  final apiKey = dotenv.env['OPENAI_API_KEY']; // 環境変数からAPIキーを取得
  // 以降のコードは変更なし
}

チャット画面を実装

APIとやり取りする部分ができたので、ユーザーが操作するUI部分を作ってもらいましょう。
スクリーンショット 2023-04-20 10.44.19.png

import 'package:flutter/material.dart';

class ChatScreen extends StatefulWidget {
  const ChatScreen({Key? key}) : super(key: key);

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

class _ChatScreenState extends State<ChatScreen> {
  final TextEditingController _textController = TextEditingController();
  final List<String> _messages = [];

  void _handleSubmitted(String text) {
    _textController.clear();
    setState(() {
      _messages.add(text);
    });
  }

  Widget _buildTextComposer() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Row(
        children: <Widget>[
          Flexible(
            child: TextField(
              controller: _textController,
              onSubmitted: _handleSubmitted,
              decoration: const InputDecoration.collapsed(
                  hintText: 'メッセージを入力してください'),
            ),
          ),
          IconButton(
            icon: const Icon(Icons.send),
            onPressed: () => _handleSubmitted(_textController.text),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('チャット')),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_messages[index]),
                );
              },
            ),
          ),
          Divider(height: 1.0),
          Container(
            decoration: BoxDecoration(color: Theme.of(context).cardColor),
            child: _buildTextComposer(),
          ),
        ],
      ),
    );
  }
}

チャット画面でchatGPT関数とやりとりする方法も教えてもらいましょう。
スクリーンショット 2023-04-20 10.46.24.png

void _handleSubmitted(String text) async {
  _textController.clear();
  setState(() {
    _messages.add('User: $text');
  });

  // ChatGPT関数を使用して応答を取得する
  try {
    final response = await chatGPT(text);
    final chatGptResponse = response.choices[0].text.trim();

    setState(() {
      _messages.add('ChatGPT: $chatGptResponse');
    });
  } catch (e) {
    print('Error: $e');
  }
}

エラーダイアログの実装

エラー時のUIについても教えてもらいました。
スクリーンショット 2023-04-20 10.47.25.png

void _handleSubmitted(String text) async {
  _textController.clear();
  setState(() {
    _messages.add('User: $text');
  });

  // ChatGPT関数を使用して応答を取得する
  try {
    final response = await chatGPT(text);
    final chatGptResponse = response.choices[0].text.trim();

    setState(() {
      _messages.add('ChatGPT: $chatGptResponse');
    });
  } catch (e) {
    print('Error: $e');
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('エラーが発生しました'),
          content: Text('APIからの応答が得られませんでした。詳細:$e'),
          actions: <Widget>[
            TextButton(
              child: const Text('閉じる'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

操作性を良くする

メッセージ送信後と画面タップでキーボードを非表示にしたかったので、その方法を教えてもらいました。
スクリーンショット 2023-04-20 10.50.54.png

import 'package:flutter/services.dart'; // 追加

class _ChatScreenState extends State<ChatScreen> {
  // 既存のコード...

  void _handleSubmitted(String text) async {
    // 既存のコード...
    FocusScope.of(context).unfocus(); // メッセージ送信後にキーボードを閉じる
  }

  // 既存のコード...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('チャット')),
      body: GestureDetector( // 追加
        onTap: () {
          FocusScope.of(context).unfocus(); // 入力フォーム以外をタップした時にキーボードを閉じる
        },
        child: SafeArea(
          child: Column(
            children: <Widget>[
              // 既存のコード...
            ],
          ),
        ),
      ),
    );
  }
}

見た目を良くする

メッセージのWidgetがしょぼかったので、文句を言いました。
スクリーンショット 2023-04-20 10.52.22.png

Widget _buildMessageItem(String message, bool isUserMessage) {
  return Container(
    padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
    child: Row(
      mainAxisAlignment:
          isUserMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Flexible(
          child: Container(
            padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(12.0),
              color: isUserMessage ? Colors.blue[100] : Colors.grey[200],
            ),
            child: Text(
              message,
              style: TextStyle(color: isUserMessage ? Colors.white : Colors.black),
            ),
          ),
        ),
      ],
    ),
  );
}
@override
Widget build(BuildContext context) {
  // 既存のコード...

  return Scaffold(
    appBar: AppBar(title: const Text('チャット')),
    body: GestureDetector(
      onTap: () {
        FocusScope.of(context).unfocus();
      },
      child: SafeArea(
        child: Column(
          children: <Widget>[
            Expanded(
              child: ListView.builder(
                itemCount: _messages.length,
                itemBuilder: (context, index) {
                  final message = _messages[index];
                  final isUserMessage = message.startsWith('User: ');
                  final displayMessage = isUserMessage
                      ? message.replaceFirst('User: ', '')
                      : message.replaceFirst('ChatGPT: ', '');
                  return _buildMessageItem(displayMessage, isUserMessage);
                },
              ),
            ),
            // 既存のコード...
          ],
        ),
      ),
    ),
  );
}

UXを良くする

メッセージ送信してからレスポンスが来るまで時間がかかるので、Waiting中を表示してもらうようにしました。
スクリーンショット 2023-04-20 10.57.41.png

FutureBuilder使うのは嫌だったので文句を言いました。
スクリーンショット 2023-04-20 10.58.14.png

class ChatScreen extends StatefulWidget {
  // 既存のコード...
}

class _ChatScreenState extends State<ChatScreen> {
  // 既存のコード...

  bool _isLoading = false;

  // 既存のコード...

  Future<void> _handleSubmitted(String text) async {
    // 既存のコード...

    setState(() {
      _isLoading = true;
    });

    try {
      final chatGptResponse = await chatGPT(text);
      setState(() {
        _messages.add('ChatGPT: ${chatGptResponse.text}');
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _isLoading = false;
      });
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Error'),
          content: Text('Failed to load response from ChatGPT'),
        ),
      );
    }
  }

  // 既存のコード...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 既存のコード...

      body: GestureDetector(
        onTap: () {
          FocusScope.of(context).unfocus();
        },
        child: SafeArea(
          child: Column(
            children: <Widget>[
              Expanded(
                child: ListView.builder(
                  itemCount: _messages.length,
                  itemBuilder: (context, index) {
                    // 既存のコード...
                  },
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Row(
                  children: <Widget>[
                    Expanded(
                      child: TextField(
                        controller: _textController,
                        onSubmitted: _handleSubmitted,
                        decoration: InputDecoration(hintText: 'Send a message'),
                      ),
                    ),
                    IconButton(
                      icon: Icon(Icons.send),
                      onPressed: () => _handleSubmitted(_textController.text),
                    ),
                  ],
                ),
              ),
              _isLoading ? Center(child: CircularProgressIndicator()) : Container(),
            ],
          ),
        ),
      ),
    );
  }
}

不具合の修正

日本語だと文字化けしたので、不具合を修正してもらいました。
スクリーンショット 2023-04-20 10.55.10.png

import 'dart:convert'; // 追加

// 既存のコード...

Future<ChatGPTResponse> chatGPT(String message) async {
  // 既存のコード...

  final response = await http.post(
    // 既存のコード...
  );

  if (response.statusCode == 200) {
    return ChatGPTResponse.fromJson(json.decode(utf8.decode(response.bodyBytes))); // 更新
  } else {
    throw Exception('Failed to load response from ChatGPT');
  }
}

完成

ChatGPTのAPIを使ってチャットできるアプリが完成しました🎉

一部、APIのところでChatGPTでは実装が厳しかった部分があり修正をしましたが、それ以外はChatGPTが書いてくれたコードで作りたいアプリが作れました👏

タイトルなし.gif

完成したソースコードはこちら

この方法で「AIを使ってXXXするアプリ」の量産ができるかも!?ただし、API利用には上限があるためコスパ(採算)と相談ですね

考察とまとめ

「すごい👏」の一言です。誤りはあるものの、ここまで実装してくれることに驚いています。

ChatGPTの活用方法に関して、冒頭でもお伝えしたとおり、書いてくれたソースコードはそのままでは使えないので、ChatGPTには「叩き台を作ってもらう」感覚で利用するのが良いと感じました。ご自身が触れてこなかった技術を調べる取っ掛かりとしては強力で、ChatGPTに教えてもらった内容をヒントに開発をスムーズに進めることができます。

また、ソースコードをアウトプットしてもらう際は、プロジェクトによってはソフトウェア設計が異なるため、ChatGPTにはどういう設計なのか伝える必要があります。ネット内に情報がある設計が学習されていれば望み通りの回答をしてくれるかもしれませんが、社内ドメイン知識で固められている設計では思ったような回答を得られず難しいだろうなと感じました。

そして今回は、ChatGPTに対して「日本語でやりたいことに対して少し条件をつけて質問する」ようにしました。質問する側として、ここは改善の余地があり、以下の対応をすればより精度の高い回答が得られるのではないかと思っています。

  • プロンプトを組んで背景や条件を明記して質問する
  • 英語で質問する

プロンプトについては、こちらのYouTubeチャンネルの方がかなり詳しく解説されておりますのでご参考ください。
https://www.youtube.com/@ShunsukeHayashi/videos

英語で質問する件については、日本語より英語で質問した方が精度が上がると聞きましたが、肌感そこまで感じなかったです。やはり質問の内容が大事(当たり前)かなと思います。

私個人として、ChatGPTを触り始めて間もないですが、開発業務につながる面白い使い方が見つかりましたらまた共有したいと思います。

それでは!

26
17
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
26
17