OpenAIのAssistant APIを使ってFlutter アプリを作ってみた

以前、gpt-3.5-turboを使って、Flutter アプリを作ったことがありました。

Open AIのAssistant APIを使うと、もっと簡単に実装できそうな気がしましたので、試してみました。

方針

前回は、AWSのLamdaにChaliceを使ってボットサーバを立てていました。アプリから直接OpenAIのAPIを叩いても良かったのですが、いくつかの利点からサーバを立てていました。主な理由は、pythonのopenaiライブラリが使えること、system message(Assistantにおけるinstructionにあたるもの)の変更が容易なことの2点です。

今回はサーバをなくして、Open AI上のAssistant APIを直接利用します。

また、前回の会話のスレッドはアプリ内でリスト管理していましたが、今回はThread内のmessage listに会話が蓄積していくことになります。

Assistantは、都度作成しても良かったのですが、コストを見ると以下のようになっていました。Assistantが作成されると日々コストがかかるようです。(チリも積もれば、、で結構かかりそうです。)

Assistants API

Assistants API and tools (retrieval, code interpreter) make it easy for developers to build AI assistants within their own applications. Each assistant incurs its own retrieval file storage fee based on the files passed to that assistant. The retrieval tool chunks and indexes your files content in our vector database.

Learn more

The tokens used for the Assistant API are billed at the chosen language model's per-token input / output rates and the assistant intelligently chooses which context from the thread to include when calling the model

Learn about Assistants API

ToolInput
Code interpreter$0.03 / session (free until 12/1/2023)
Retrieval$0.20 / GB / assistant / day (free until 12/13/2023)

アプリ終了時にAssistantを削除すれば良いのですが、ゴミのようなAssistantが溜まっていきそうです。そこでAssistantはあらかじめ作成しておき、アプリから既存のAssistantを利用する形としました。

Flutter Chat bot アプリ

Assistantの作成

手っ取り早く、PlaygroundからAssistantをCreateしました。

Instructionでbotへの指示を与えます。Nameの下のAssistant IDを使いますので、控えておきます。

サンプルコード

”とりあえず動く”レベルのコードですが、以下のようになります。

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


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Chatbot App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        fontFamily: 'NotoSansJP',
      ),
      home: ChatScreen(),
    );
  }
}

class ChatScreen extends StatefulWidget {
  @override
  State createState() => ChatScreenState();
}

class ChatScreenState extends State<ChatScreen> {
  final TextEditingController _controller = TextEditingController();
  final List<Map<String, dynamic>> _messages = [];

  String api_key= '<Your API key>';
  String assistant_id = '<Your Assitant ID>';
  String thread_id ='';
  String message_id='';
  String run_id='';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async{
      await _createThread().then((result) {
            setState(() {
                _messages.add({'text': 'こんにちは、お話ししましょう。', 'user': false});
            });
        
      });
    });
  }

  
  void _sendMessage() async {
    String userMessage = _controller.text;

    setState(() {
      _messages.add({'text': userMessage, 'user': true});
    });

    _controller.clear();

    message_id = await _createMessage(userMessage);
    run_id = await _createRun();
    await Future.delayed(Duration(seconds: 3)); // Assistantメッセージがセットされないことがあったので少しディレイを入れています。
    String botReply = await getAssistantReply(userMessage);
    
    setState(() {
      _messages.add({'text': botReply, 'user': false});
    });
  }


  Future<String> getAssistantReply(String message) async{

    final response = await http.get(
      Uri.parse('https://api.openai.com/v1/threads/$thread_id/messages?order=desc'), 
      headers: {
        'Content-Type': 'application/json',
        'OpenAI-Beta': 'assistants=v1',
        'Authorization': 'Bearer $api_key' , 
      },
    );

    if (response.statusCode == 200) {
      Map<String, dynamic> data = json.decode(utf8.decode(response.bodyBytes));
      return data['data'][0]['content'][0]['text']['value'];
    } else {
      throw Exception('Failed to load response');
    }

  }


  Future<String>  _createThread() async {

      final response = await http.post(
      Uri.parse('https://api.openai.com/v1/threads'), 
      headers: {
        'Content-Type': 'application/json',
        'OpenAI-Beta': 'assistants=v1',
        'Authorization': 'Bearer $api_key', 
        
      },
      body: null,
    );
    if (response.statusCode == 200) {
      Map<String, dynamic> data = json.decode(utf8.decode(response.bodyBytes));
      thread_id = data['id'];
      return thread_id;

    } else {
      throw Exception('Failed to load response');
    }
  }

  Future<String> _createMessage(String message) async {
      final response = await http.post(
      Uri.parse('https://api.openai.com/v1/threads/$thread_id/messages'), 
      headers: {
        'Content-Type': 'application/json',
        'OpenAI-Beta': 'assistants=v1',
        'Authorization': 'Bearer $api_key', 
      },
      body: json.encode({
        'role':'user',
        'content':message,
      }),
    );

    if (response.statusCode == 200) {
      Map<String, dynamic> data = json.decode(utf8.decode(response.bodyBytes));
      return data['id'];

    } else {
      throw Exception('Failed to load response');
    }
  }

  Future<String> _createRun()async{
      final response = await http.post(
      Uri.parse('https://api.openai.com/v1/threads/$thread_id/runs'), 
      headers: {
        'Content-Type': 'application/json',
        'OpenAI-Beta': 'assistants=v1',
        'Authorization': 'Bearer $api_key', 
        
      },
      body: json.encode({
        'assistant_id':assistant_id,
      }),
    );

    if (response.statusCode == 200) {
      Map<String, dynamic> data = json.decode(utf8.decode(response.bodyBytes));
  
      return data['id'];
    } else {
      throw Exception('Failed to load response');
    }

 }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Chatbot App'),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_messages[index]['text']),
                  leading: _messages[index]['user'] ? const Icon(Icons.face) : const Icon(Icons.android),
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: <Widget>[
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Enter your message...',
                    ),
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.send),
                  onPressed: _sendMessage,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Threadはアプリ起動時に作成したいのですが、非同期関数はinitState()で使えません。

そこで、WidgetsBinding.instance.addPostFrameCallbackを使っています。これは、UI構築後にコールバックされるので、非同期関数である_createThreadを呼び出すことができます。Threadが生成されたら、Botからの最初のメッセージを出力します。

あとは、通常のボットアプリと同様の構造です。

メッセージを入力して、Send Iconをクリックすると、 _sendMessage()を呼び出します。この中で、_createMessage, _createRun, _getAssistantReply と、3回Assistant APIにアクセスする必要があります。

pythonの場合はOpenAIのライブラリをインポートすれば良いのですが、Flutter用のライブラリはありません。公式ドキュメントのcurlを参考に、httpリクエストを記述しました。

何度かテストをしてみたのですが、_createRunをしてからAssistantのリプライがThreadのMessageに追加されるまでに、タイムラグがあるように思います。とりあえず、3秒ほどディレイを入れています。(3秒に根拠はありません。)

まとめ

Assistant APIを使ったアプリの試作をしてみました。確かに簡単に書けるのですが、1回のメッセージのやり取りに、3回もhttpリクエストが発生するというのはちょっと、、な感じです。Function Callingを使うともっとリクエストが増えますので、レスポンスが問題になりそうな気がしています。まだベータ版なので、改善されるかもしれません。