[C++] 291 FLTK : ChatGPTアプリの製作 その20 grepによるjson内文字列検索

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

ChatGPT APIとのやりとりを過去ログとしてjsonファイル保管しています。

質問がなるべくダブらないよう、grepコマンドで事前にjsonファイルを検索できるようにしました。

動画ではまず”FLTK”で調べて21ファイルがヒットしたので”Java”でand検索し、1ファイルに絞り込んでいます。

grepFiles関数はChatGPTに書かせようとしたのですが、どう説明してもor検索のコードしか作れないので、こちらでand検索に書き換えました。とは言えdirent構造体を使ってくるあたりは中級以上のエキスパートかと思うほどの熟練度です。

次はJSONの内容をGUI右側に、質問と回答を左側に表示するようにします。

void searchCB(Fl_Widget*, void*){
    // 初期化
    hitShow -> value();
    window -> redraw();

    const char* grepStr = grepInput -> value();
    cout << "grepStr: " << grepStr << endl;

    stringstream iss(grepStr);
    vector<string> words;
    string word;
    while (getline(iss, word, ' ')) {
        words.push_back(word);
    }
    for (const auto& w : words) {
        cout << "word: " << w << endl;
    }

    string dirPath = "/ChatGPT";
    vector<string> paths = grepFiles(words, dirPath);
    for (const auto& p : paths) {
        cout << "path: " << p << endl;
    }

    int pathsNum = paths.size();
    cout << "HITファイル数: " << pathsNum << endl;

    string pathsNumStr;
    if (pathsNum < 10){
        pathsNumStr = to_string(pathsNum) + " / " + to_string(pathsNum);
    } else {
        pathsNumStr = to_string(pathsNum);
    }

    hitShow -> value(pathsNumStr.c_str());
}

vector<string> grepFiles(const vector<string>& words, const string& path) {
    vector<string> result;
    DIR* dir = opendir(path.c_str());

    if (!dir) {
        cerr << "Error: opendir() failed." << endl;
        return result;
    }

    struct dirent* entry;
    while ((entry = readdir(dir)) != nullptr) {
        if (entry->d_type == DT_REG) { // regular file
            string filePath = path + "/" + entry->d_name;
            string command = "grep -l ";

            int count = 0;
            for (const auto& word : words) {
                if (count == 0){
                    command += word + " \"" + filePath + "\"";
                } else {
                    command += "| grep " + word;
                }
                count++;
            }

            // cout << "command:\n" << command << endl;
            
            FILE* pipe = popen(command.c_str(), "r");
            if (!pipe) {
                cerr << "Error: popen() failed." << endl;
                continue;
            }
            char buffer[256];
            if (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
                result.emplace_back(filePath);
            }
            pclose(pipe);
        }
    }
    closedir(dir);
    return result;
}

[C++] 290 FLTK : ChatGPTアプリの製作 その19 マルチプロセス処理の試み fork関数

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

マルチスレッドの次はマルチプロセスです。

fork関数を使って別のプロセスIDを作成しましたが、プログラムはそこで停止しました。

CoreFoundationはfork()をサポートしていないそうです。POSIX標準なのでLinuxでのみ使用可能とのこと。

void processA() {
    std::cout << "ProcessA: PID = " << getpid() << std::endl;
    sendCB_A();
}

void processB() {
    std::cout << "ProcessB: PID = " << getpid() << std::endl;
    timerCallback(nullptr);
}

void sendCB(Fl_Widget*, void*) {
    sendBool = true;

    pid_t pidA, pidB;

    pidA = fork();
    if (pidA == 0) { // 子プロセスA
        processA();
        return;
    }
    else if (pidA > 0) { // 親プロセス
        std::cout << "Parent process: PID = " << getpid() << std::endl;

        pidB = fork();
        if (pidB == 0) { // 子プロセスB
            processB();
            return;
        }
        else if (pidB > 0) { // 親プロセス
            waitpid(pidA, NULL, 0);
            waitpid(pidB, NULL, 0);
        }
        else { // forkに失敗した場合
            std::cerr << "fork() failed." << std::endl;
            return;
        }
    }
    else { // forkに失敗した場合
        std::cerr << "fork() failed." << std::endl;
        return;
    }
}
Parent process: PID = 62590
ProcessA: PID = 62601
question1回目
ChatGPTとは
requestData: {"model":"gpt-3.5-turbo", "messages":[{"role":"system","content":""},{"role":"user","content":"ChatGPTとは"}], "temperature":0.0}
ProcessB: PID = 62602
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().

[C++] 289 FLTK : ChatGPTアプリの製作 その18 マルチスレッド処理の試み std::thread

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

std::threadもうまくいきませんでした。アプリが落ちることはなく、送受信終了時にようやくタイムが表示されます。

void sendCB(Fl_Widget*, void*) {
    sendBool = true;

    std::thread sendMain([](){
        sendCB_A();
    });

    std::thread stopwatch([](){
        timerCallback(nullptr);
    });

    sendMain.join();
    stopwatch.join();

}

[C++] 288 FLTK : ChatGPTアプリの製作 その17 マルチスレッド処理の試み Cpp-Taskflowライブラリ

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

Cpp-Taskflowというライブラリを試してみましたが、これもダメでした。

#include "taskflow/taskflow.hpp"

void sendCB_A(){
    (送受信に関するコード)
}

void sendCB_B(){
    // ストップウォッチ開始
    resetBtn -> do_callback();
    startBtn -> do_callback();

    sendBool = true; // timerCallback関数を停止させるため
}

void sendCB_C(){
    // ストップウォッチ停止
    stopBtn -> do_callback();
}

void sendCB_D(){
    timerCallback(nullptr);
}


void sendCB(Fl_Widget*, void*) {
    tf::Executor executor;
    tf::Taskflow taskflow;

    auto [A, B, C, D] = taskflow.emplace(
        [] () { sendCB_A(); },
        [] () { sendCB_B(); },
        [] () { sendCB_C(); },
        [] () { sendCB_D(); }
    );

    A.succeed(B); // ストップウォッチ開始後、送受信開始
    D.succeed(B); // ストップウォッチ開始後、timerCallback関数開始
    C.succeed(A); // 送受信終了後、ストップウォッチ停止
 
    executor.run(taskflow).wait();
}
ストップウォッチは動作しているがGUIへ反映せず

[C++] 287 FLTK : ChatGPTアプリの製作 その16 マルチスレッド処理の試み Fl::add_timeout関数

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

FLTKのコールバック関数とストップウォッチ表示のマルチスレッド処理について試した内容をまとめておきます。

・Fl::add_timeout関数
指定された時間間隔でコールバック関数を呼び出します。ストップウォッチだけなら動作するが、送受信が加わるとウォッチの数値は動かず、送受信終了時に表示される。

int main(int argc, char **argv) {

   	<中略>

   	window->end();  
   	window->show(argc, argv);

    Fl::add_timeout(0.01, timerCallback);

   	return Fl::run();
}
void timerCallback(void*) {
    // 経過時間を取得して、Fl_Outputに表示する
    double elapsedTime = stopwatch.elapsedTime();
    stringstream ss;
    // ss << std::fixed << elapsedTime;
    ss << std::fixed << std::setprecision(1) << elapsedTime;
    // cout << "経過時間: " << ss.str() << endl;
    timeShow->value(ss.str().c_str());

    window -> redraw();

    // タイマーを再起動する
    Fl::repeat_timeout(0.01, timerCallback);
}
void sendCB2(Fl_Widget*, void*) {
    // ストップウォッチ開始
    resetBtn -> do_callback();
    startBtn -> do_callback();
}
シングルスレッドの場合(マルチスレッドでもこのようにしたい)

[C++] 286 FLTK : ChatGPTアプリの製作 その15 経過時間の測定

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

ChatGPTアプリの送受信に掛かる時間を測定して表示するようにしました。

これまでFLTKアプリで時間経過の概念を扱ったことがなかったので結構難航しました。

ストップウォッチの値が増えていく様子を表示したかったのですが、並列処理が出来なくて断念しました。

ウィンドウ範囲外の見えないボタンとしてStartボタン、Stopボタン、Resetボタンを配置し、送信ボタンのコールバック関数(sendCB関数)の中でdo_callback関数を使ってボタンを押すというややこしい仕様になっています。

ストップウォッチのGUI数値が送受信中に変化しなくてよいのであれば、もっと単純な方法で実装できていました。ソースコードの構成が複雑になるだけなので、明日以降簡潔な方法に変更します

こういったところはSDLに分があるのかもしれません。

void sendCB(Fl_Widget*, void*) {
    // ストップウォッチ開始
    resetBtn -> do_callback();
    startBtn -> do_callback();

    <中略>

    // ストップウォッチ停止
    stopBtn -> do_callback();
}
#include <FLstd.h>
#include <cppstd.h>
#include "Stopwatch.h"

// 経過時間を表示するFl_Output
extern Fl_Window* window;
extern Fl_Output* timeShow;

// ストップウォッチのインスタンスを作成する
Stopwatch stopwatch;

// スタートボタンが押された時のコールバック関数
void startCallback(Fl_Widget* widget, void*) {
    stopwatch.start();
}

// ストップボタンが押された時のコールバック関数
void stopCallback(Fl_Widget* widget, void*) {
    stopwatch.stop();
}

// リセットボタンが押された時のコールバック関数
void resetCallback(Fl_Widget* widget, void*) {
    stopwatch.reset();
    timeShow->value("");
}

// タイマーが起動した時のコールバック関数
void timerCallback(void*) {
    // 経過時間を取得して、Fl_Outputに表示する
    double elapsedTime = stopwatch.elapsedTime();
    stringstream ss;
    // ss << std::fixed << elapsedTime;
    ss << std::fixed << std::setprecision(1) << elapsedTime;
    // cout << "経過時間: " << ss.str() << endl;
    timeShow->value(ss.str().c_str());

    window -> redraw();

    // タイマーを再起動する
    Fl::repeat_timeout(0.01, timerCallback);
}

[C++] 285 FLTK : ChatGPTアプリの製作 その14 コードの取り出し

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

ChatGPTアプリ製作開始からだらだらと1週間掛けて、ようやく基本的な機能を一通り実装しました。

回答からコードを取り出し、何番目のコードでも選択してクリップボードにコピーできます。

次は特定分野のドキュメントを読み込ませたアシスタントBotを作成したいです。都度ドキュメントを送ると課金がかさみレスポンスも悪くなるので、一回読み込ませて単発の質問をいくつか投げかけるという仕様が妥当なところかと思います。

void advanceCB(Fl_Widget*, void*){
    if (codeNumCur == codeNum){
        codeNumCur = 0;
    }

    // 初期化
    codeNumShow -> value("");

    Fl_Text_Buffer* bufferReset = new Fl_Text_Buffer();
    bufferReset -> text("");
    codeDisplay->buffer(bufferReset);

    window -> redraw();

    // code取り出し&表示
    codes = getCode(jsonResponse);
    cout << "getCode検出コード数: " << codes.size() << endl;

    // codeNumCur加算
    codeNumCur++;

    if (codes.size() > 0){
        bufferCode = new Fl_Text_Buffer();
        bufferCode -> text(codes[codeNumCur-1].c_str());
        codeDisplay -> buffer(bufferCode);
        codeDisplay -> wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 5);
    }

    // コード数表示
    codeNum = codes.size();
    if (codeNum > 0){
        codeNumStr =  to_string(codeNumCur) + " / " + to_string(codeNum);
        cout << "codeNumStr: " << codeNumStr << endl;
        codeNumShow -> value(codeNumStr.c_str());
    }
}

[C++] 284 FLTK : ChatGPTアプリの製作 その13 パースエラーの捕捉 try-catch文

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

jsonデータのパースエラーを捕捉するtry-catch文をメモ書きしておきます。

    try{
        resJson = json::parse(res);
    }
    catch (const json::parse_error& e) {
        std::cerr << "Parse error:\n" << e.what() << std::endl;

        Fl_Text_Buffer* bufferError = new Fl_Text_Buffer();
        bufferError -> text(e.what());
        noticeDisplay->buffer(bufferError);
        noticeDisplay -> wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 5);

        return;
    }
    catch (const char* error) {
        cerr << "Error: " << error << endl;

        Fl_Text_Buffer* bufferError = new Fl_Text_Buffer();
        bufferError -> text(error);
        noticeDisplay->buffer(bufferError);
        noticeDisplay -> wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 5);

        return;
    }
    catch (...){
        cerr << "その他のError" << endl;

        const char* error = "その他のError";
        Fl_Text_Buffer* bufferError = new Fl_Text_Buffer();
        bufferError -> text(error);
        noticeDisplay->buffer(bufferError);
        noticeDisplay -> wrap_mode(Fl_Text_Display::WRAP_AT_BOUNDS, 5);

        return;
    }

[C++] 283 FLTK : ChatGPTアプリの製作 その12 前後の質問表示 

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

一連の質問についてボタンで前後移動できるようにしました。

質問回数表示で数値がダブって描画されるというトラブルが発生しましたが、window -> redraw();で解決しました。

あとはGUI左下の回答内コード表示が出来れば完成です。

アプリが落ちないよう例外処理を随時追加していきます。

void backCB(Fl_Widget*, void*){
    questionNumShow -> value("");
    window -> redraw();

    // jsonDataからuserとassistantのcontentを取り出し表示
    if (arrowCount == 0){
        currentCount = countQ - 2;

        if(currentCount < 0){
            currentCount = 0;
        }

        arrowCount++;
    } else {
        if (currentCount > 0){
            currentCount--;
        }
    }

    cout << "currentCount: " << currentCount << endl;

    // jsonData確認
    string jsonDataString = jsonData.dump(2);
    cout << "jsonDataString:\n" << jsonDataString << endl;

    auto messages = jsonData["messages"];

    int i = 0;
    for (auto message : messages) {
        // roleがuserの場合はcontentを出力
        if (message["role"] == "user") {
            // cout << "user: " << message["content"] << endl;
            if (i == currentCount){
                cout << "user: " << message["content"] << endl;
                input -> value(message["content"].get<string>().c_str());
            }
            i++;
        }
    }

    i = 0;
    for (auto message : messages) {
        // roleがassistantの場合はcontentを出力
        if (message["role"] == "assistant") {
            // cout << "assistant: " << message["content"] << endl;
            if (i == currentCount){
                cout << "assistant: " << message["content"] << endl;

                Fl_Text_Buffer* buffer = new Fl_Text_Buffer();
                buffer -> text(message["content"].get<string>().c_str());
                output -> buffer(buffer);
            }
            i++;
        }
    }

    // 質問回数表示
    string countStr =  to_string(currentCount + 1) + " / " + to_string(countQ);
    cout << "countStr: " << countStr << endl;
    questionNumShow -> value(countStr.c_str());
}

[C++] 282 FLTK : ChatGPTアプリの製作 その11 エスケープシーケンス処理

[M1 Mac, Monterey 12.6.3, clang 13.0.0, FLTK 1.3.8, ChatGPT Plus, NO IDE]

string型をjson型データに変換する際、エスケープシーケンスを適切に前処理しないと変換に失敗します。

そこで前処理するescapeSequence関数を作成しました。内容についてはさらに詰める必要がありますが、今のところ私が使う範囲では正常に動作します。

C++で文字列を扱うと色々と泥沼にハマります。

ようやくOpenAIのアカウント管理ページに課金が反映されました。6日間それなりに使っても0.5ドル(70円)でした。お試し期間としてもらっている18ドルを使い切るのはまだまだ先のようです。値下げ前だったら10倍の700円ということを考えると激安もいいところです。

今日の夕方から急にAPIが重くなったりサーバ内部エラーになるのですが、トラフィックがきつくなってきたのでしょうか。

void escapeSequence(string& str){
    size_t pos = 0;
    while ((pos = str.find("\\", pos)) != string::npos) {
        str.replace(pos, 1, "\\\\");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find('"', pos)) != string::npos) {
        str.replace(pos, 1, "\\\"");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find("'", pos)) != string::npos) {
        str.replace(pos, 1, "\'");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find('\n', pos)) != string::npos) {
        str.replace(pos, 1, "\\n");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find('\t', pos)) != string::npos) {
        str.replace(pos, 1, "\\t");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find('\b', pos)) != string::npos) {
        str.replace(pos, 1, "\\b");
        pos += 2;
    }

    pos = 0;
    while ((pos = str.find('\f', pos)) != string::npos) {
        str.replace(pos, 1, "\\f");
        pos += 2;
    }
}
ChatGPT課金額推移