[C++] 366 ChatAIアプリの製作 その47 “role:systemの扱い”公式版 claude-3 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

claude-3ではgptのようにrole:systemは設定できず、systemという独立したパラメータとして扱います。つまりmodelやtemperatureと同じです。

以前の記事で紹介したようにuserのcontentの先頭に追加しても効果は同じだと思いますが、一応公式の方法に従います。

if (urls == ""){
    // claude-3 画像なし
    requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":\"" + question + "\"}], \"system\":\"" + systemStr + "\",\"temperature\":0.0, \"max_tokens\":4096}";
} else {
    // claude-3 画像あり
    string requestData_1 = "{\"model\":\"" + model + "\", \"system\":\"" + systemStr + "\", \"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"" + question + "\"},";

<以下略>

公式サイト

[C++] 365 ChatAIアプリの製作 その46 画像エンコードデータ送信 claude-3 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

claude-3でも画像のエンコードデータを送れるようにしました。JSONの形式がgpt-4と異なるため、かなり手を入れました。

画像のあるなしに関係なくclaude-3の方がレスポンスに時間が掛かります。

if (model.find("gpt") != string::npos){
    // gpt-4
    requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + systemStr + "\"},{\"role\":\"user\",\"content\":\"" + question + "\"}], \"temperature\":0.0}";
} else {
    if (urls == ""){
        // claude-3 画像なし
        requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":\"" + systemStr + question + "\"}], \"temperature\":0.0, \"max_tokens\":4096}";
    } else {
        // claude-3 画像あり
        string requestData_1 = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"" + systemStr + question + "\"},";
        vector<string> urlList = splitString(urls, '\n');
        cout << "urlListの要素数: " << to_string(urlList.size()) << endl;

        std::stringstream imageStream;
        for (size_t i = 0; i < urlList.size(); ++i) {
            // ローカル画像はBase64形式にエンコードする
            if (urlList[i].find("https:") != string::npos){
                cout << "claude3への画像URL送信には未対応" << endl;
                return "";
            } else {
                string base64_image = file_to_base64(urlList[i]);
                string file_extension = get_file_extension(urlList[i]);
                string url = "\"media_type\":\"image/" + file_extension + "\",\"type\":\"base64\", \"data\": \"" + base64_image + "\"";
                imageStream << "{\"type\": \"image\", \"source\": {" << url << "}}";
            }
            
            if (i < urlList.size() - 1) {
                imageStream << ",";
            }
        }

        std::string requestData_2 = imageStream.str() + "]}], \"max_tokens\": 4096,\"temperature\": 0.0}";
        cout << "requestData_2: \n" << requestData_2.c_str() << endl;

        requestData = requestData_1 + requestData_2;
    }
}

※ file_to_base64関数他については前回の記事参照

[C++] 364 ChatAIアプリの製作 その45 画像URL or エンコードデータ送信 gpt-4-vision 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

これまでは自分のブログの非公開記事に画像を貼り付けて、そのURLを送信していましたが、ローカル画像をBase64形式にエンコードして送れるようにもしました。

string requestData_1 = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + systemStr + "\"},{\"role\":\"user\",\"content\":[{\"type\": \"text\", \"text\":\"" + question + "\"},";

cout << "requestData_1: \n" << requestData_1.c_str() << endl;

vector<string> urlList = splitString(urls, '\n');
cout << "urlListの要素数: " << to_string(urlList.size()) << endl;

std::stringstream imageStream;
for (size_t i = 0; i < urlList.size(); ++i) {
    // URLの場合はそのまま使用し、ローカルの場合はBase64形式にエンコードする
    if (urlList[i].find("https:") != string::npos){
        imageStream << "{\"type\": \"image_url\", \"image_url\": {\"url\":\"" << urlList[i] << "\"}}";
    } else {
        string base64_image = file_to_base64(urlList[i]);
        string file_extension = get_file_extension(urlList[i]);
        string url = "data:image/" + file_extension + ";base64," + base64_image;
        imageStream << "{\"type\": \"image_url\", \"image_url\": {\"url\":\"" << url << "\"}}";
    }
    
    if (i < urlList.size() - 1) {
        imageStream << ",";
    }
}

std::string requestData_2 = imageStream.str() + "]}], \"max_tokens\": 4096,\"temperature\": 0.0}";
cout << "requestData_2: \n" << requestData_2.c_str() << endl;

requestData = requestData_1 + requestData_2;
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/archive/iterators/ostream_iterator.hpp>
#include <fstream>
#include <sstream>
#include <string>

std::string file_to_base64(const std::string& file_path) {
    using namespace boost::archive::iterators;
    using Base64EncodeIterator = base64_from_binary<transform_width<std::string::const_iterator, 6, 8>>;
    
    std::ifstream file(file_path, std::ios::binary);
    std::string file_contents((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    
    std::stringstream os;
    std::copy(Base64EncodeIterator(file_contents.begin()), Base64EncodeIterator(file_contents.end()), ostream_iterator<char>(os));
    size_t num = (3 - file_contents.length() % 3) % 3;
    for (size_t i = 0; i < num; i++) {
        os.put('=');
    }
    return os.str();
}

std::string get_file_extension(const std::string& file_path) {
    size_t dot_pos = file_path.rfind('.');
    if (dot_pos == std::string::npos) return "";
    return file_path.substr(dot_pos + 1);
}

[C++] 363 ChatAIアプリの製作 その44 role:systemの扱い claude-3 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

claude-3ではgpt-4のようにrole:systemの設定ができないため、最初のプロンプトでuserとして指示します。

if (model.find("gpt") != string::npos){
   requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"system\",\"content\":\"" + systemStr + "\"},{\"role\":\"user\",\"content\":\"" + question + "\"}], \"temperature\":0.0}";
} else {
   requestData = "{\"model\":\"" + model + "\", \"messages\":[{\"role\":\"user\",\"content\":\"" + systemStr + question + "\"}], \"temperature\":0.0, \"max_tokens\":1024}";
}

[C++] 362 ChatAIアプリの製作 その43 Claude3 APIへの対応 旧ChatGPTアプリ

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0]
対応LLM:gpt-4, gpt-4-vision, claude-3

ネット観察しているとClaude3 Opusの評判がやたらいいので、ChatGPTアプリに導入しました。今日からChatAIアプリに改名します。

GPT-4と同様にlibcurlライブラリを使って通信します。認証(Authorization)のところで少し手間取りました。

これからじっくり比較評価していきます。

    string url;
    if (model.find("gpt") != string::npos){
        url = "https://api.openai.com/v1/chat/completions";
    }else{
        url = "https://api.anthropic.com/v1/messages";
    }

    const char* apiKey;
    const char* apiKey2;
    apiKey = getenv("CHATGPT_API_KEY");
    apiKey2 = getenv("CLAUDE_API_KEY");

    // appファイルは環境変数を取得できないため直打ち
    if (apiKey == NULL) {
        apiKey = "xxx";
    }

    if (apiKey2 == NULL) {
        apiKey2 = "xxx";
    }

    string authHeader;
    if (model.find("gpt") != string::npos){
        authHeader = "Authorization: Bearer " + string(apiKey);
    } else {
        authHeader = "x-api-key: " + string(apiKey2);
    }
    
    curl_slist* headers = {};
    headers = curl_slist_append(headers, authHeader.c_str());
    headers = curl_slist_append(headers, "Content-Type: application/json");
    if (model.find("claude") != string::npos){
        headers = curl_slist_append(headers, "anthropic-version: 2023-06-01");
    }

[C++] 361 BBS閲覧アプリの製作 その34 FLTK内部ソースコード混在によるビルドエラー 原因判明 #define命令

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0, FLTK 1.3.9]

前回記事のビルドエラーについて原因が判明しました。

Fl_WebView.cppで定義していたプリプロセッサの#define命令が悪さをしていました。

#define命令 FL_INTERNALSによりmac.Hのif文で誤った分岐に誘導されていました。なぜ急にプリプロセッサが機能し始めたのか、については不明です。

とりあえずプリプロセッサは無効にしました。libfltk_webview.aファイルに静的ライブラリ化しなくても、これまで通りビルドできます。

// #define FL_INTERNALS // mac.Hのif文で誤った分岐に誘導していたため無効化

#include "Fl_WebView.H"
#include "webview.h"
#include <FL/Fl.H>
#include <FL/platform.H>
#include <stdexcept>

extern "C" void my_close_win(void *win);
extern "C" void make_delegate(void *child, void *parent);

void close_cb(Fl_Widget *w, void *win) {
  my_close_win(win);
  w->hide();
}

void Fl_WebView::navigate(const char *addr) { webview_navigate(wv, addr); }

Fl_WebView::Fl_WebView(int x, int y, int w, int h, const char *title)
    : Fl_Double_Window(x, y, w, h, title) {
  Fl_Double_Window::end();
}

void Fl_WebView::draw() {
  if (wv)
    webview_set_size(wv, w(), h(), 0);
}

void Fl_WebView::init() {
    if (!shown())
        throw std::runtime_error("The window needs to be shown.");
    
    auto handle = fl_xid(this);
    wv = webview_create(false, (void *)handle);
    make_delegate((void *)webview_get_window(wv), (void *)handle);
    // Fl::add_idle(webview_run, wv); // この行をコメントアウトする
    this->top_window()->callback(close_cb, (void *)webview_get_window(wv));
}
#if !(defined(FL_LIBRARY) || defined(FL_INTERNALS)) // this part is used when compiling an application program
#  include <FL/Fl_Widget.H>

typedef struct flCocoaRegion* Fl_Region;
typedef struct CGContext* CGContextRef;
typedef struct OpaquePMPrintSettings*   PMPrintSettings;
typedef struct OpaquePMPageFormat*      PMPageFormat;
typedef struct OpaquePMPrintSession*    PMPrintSession;
typedef struct CGImage* CGImageRef;
typedef struct __CFData* CFMutableDataRef; // used in Fl_Copy_Surface.H
typedef CGContextRef Fl_Offscreen;

#else // this part must be compiled when building the FLTK libraries

※ mac.Hのif文でelseの方に誘導されていた

[C++] 360 BBS閲覧アプリの製作 その33 FLTK内部ソースコード混在によるビルドエラー

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0, FLTK 1.4.0ベータ版]

BBS閲覧アプリが急にビルドできなくなりました。

FLTKライブラリ作成用のFl_WebView.cppをプロジェクトに混在させていた問題が何らかのきっかけで顕在化したようです。これまではマグレでたまたまビルドできていました。

In file included from src/Fl_WebView.cpp:6:
In file included from /usr/local/include/FL/platform.H:26:
In file included from /usr/local/include/FL/x.H:32:
/usr/local/include/FL/mac.H:118:12: fatal error: '../src/Fl_Font.H' file not found
#  include "../src/Fl_Font.H"
           ^~~~~~~~~~~~~~~~~~
1 error generated.
make: *** [Makefile:36: obj/Fl_WebView.o] Error 1

GitHubにあるFL_WebViewプロジェクトからビルドし、生成したlibfltk_webview.aファイルを使うようにすると正常化しました。ビルドの際、GitHubのFLTK 1.4.0 最新ベータ版を使っているため、このアプリのビルドにも同バージョンを使用しました。

FLTK最新ベータ版 libファイル:fltk-build/libにあるものを使用
FLTK最新ベータ版 ヘッダファイル:fltk-src/FLにあるものを使用
libfltk_webview.aファイル:libファイルと同じディレクトリへコピーする

GUI右側中央のFl_Boxにスレッドタイトルが表示できていない不具合についてはこれから対処します。

GitHubリンク

[C++] 359 Makefileでワイルドカード使用 FLTKアプリ

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0, FLTK 1.3.9]

Homebrewのライブラリをアップデートする度にMakefileにあるバージョン番号を更新しなくて済むようワイルドカードに置き換えました。

さらにライブラリへのリンクを意味する-lを正常に動作させるため、-Lオプションに/opt/homebrew/libを追加しました。

これまでは/opt/homebrew/Cellarにある各ライブラリのlibパスをわざわざ記入していました。/opt/homebrew/libにライブラリファイルがまとめられているとは知らなかったです。

今回の修正で大分洗練されたMakefileになったように思います。

# コンパイラ
COMPILER = clang++
DEBUG = -g

# ビルドオプション
CPPFLAGS = $(shell fltk-config --use-gl --use-images --cxxflags ) -std=c++11
CPPFLAGS2 = $(shell pkg-config --cflags --libs opencv4 )
LDFLAGS = $(shell fltk-config --use-gl --use-images --ldflags ) -lc++

# includeパス(-I)
INCLUDE = -I./include -I/opt/homebrew/Cellar/libpng/*/include -I/opt/homebrew/Cellar/fltk/*/include \
-I/opt/homebrew/Cellar/opencv/*/include -I/opt/homebrew/Cellar/opencv/*/include/opencv4 \
-I/Library/Frameworks/Python.framework/Versions/3.10/include/python3.10 \
-I/Volumes/DATA_m1/code/cpp/mylib/include

# リンク(-l)
LINK = -lz -lpng -lopencv_core -lopencv_imgcodecs -lopencv_highgui -lopencv_imgproc -lpython3.10

# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib -L/opt/homebrew/lib \
-L/Library/Frameworks/Python.framework/Versions/3.10/lib \
/Volumes/DATA_m1/code/cpp/mylib/lib/Split.a

# ソースファイル
SRCDIR = ./src
SRCS = $(shell find $(SRCDIR) -type f)

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS)))

# 実行ファイル
TARGETDIR = ./bin
TARGET = ImageInspector
	
# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(COMPILER) $(CPPFLAGS) $(CPPFLAGS2) $(INCLUDE) $(DEBUG) -o $@ -c $<

# アプリファイル作成関連
POSTBUILD  = fltk-config --post

# oファイルから実行ファイルとappファイル作成
$(TARGET):$(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LINK) $(LIBRARY) $(LDFLAGS)
	cp $(TARGETDIR)/$(TARGET) $(TARGET)
	$(POSTBUILD) $(TARGET)
	mkdir $(TARGET).app/Contents/Resources
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	plutil -insert 'CFBundleIconFile' -string $(TARGET).icns $(TARGET).app/Contents/Info.plist
	rm -f $(TARGET)

# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)

# 全ファイル削除
.PHONY:clean
clean:
	rm -rf $(OBJS) $(TARGETDIR)/$(TARGET) $(TARGET).app

[C++] 358 FLTKアプリ ビルド時のトラブル png.h

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0, FLTK 1.3.9]

アイコン画像作成アプリを久しぶりにビルドすると、png関連のシンボルが見つからずエラーを吐くようになりました。

これまでlibpngのpng.hへアクセスしていたのが、FL/images/png.hを読み込むようになってしまったために起こったトラブルでした。

ヘッダファイルのincludeを絶対パスにすると直りました。

Homebrewのライブラリをアップデートするとバージョン番号が変わるのでMakefileを更新する必要がありますが、それとは関係なく起こったレアなトラブルだったため原因究明に時間が掛かりました。

ld: Undefined symbols:
  _fltk_png_create_info_struct, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_create_read_struct, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_destroy_read_struct, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_get_channels, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_get_image_height, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_get_image_width, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_get_x_pixels_per_inch, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_get_y_pixels_per_inch, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_init_io, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_read_png, referenced from:
      getInfoPNG(char const*) in processImage.o
  _fltk_png_set_sig_bytes, referenced from:
      getInfoPNG(char const*) in processImage.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [Makefile:53: ImageInspector] Error 1
#ifndef PROCESSIMAGE_H
#define PROCESSIMAGE_H

#include <array>
// FL/imagesのpng.hを避けるため絶対パスに変更
#include "/opt/homebrew/Cellar/libpng/1.6.42/include/libpng16/png.h"

std::vector<std::string> split(std::string, char);
std::string join(const std::vector<std::string>& , const char*);

int checkImage(const char*);
std::array<int,5> getInfoPNG(const char*);

#define SIGNATURE_NUM 8

#endif

[C++] 357 ChatGPTアプリの製作 その42 “libcurlによるHTTP通信”の中断 curl_easy_cleanup 

[Mac M2 Pro 12CPU, Sonoma 14.3.1, clang++ 15.0.0, FLTK 1.3.8]

中断ボタンでlibcurlによるHTTP通信をabortしようとしましたが、かなり手こずりました。

C++では難しいためSwiftへの移植も考えたものの、相当な時間を掛けることになるのでもう一粘りしたところ、あっさり解決しました。

CURL* curl = curl_easy_init();のcurlをグローバル変数にして、中断コールバック関数内でcurl_easy_cleanup(curl);とすれば中断できるようになりました。

シングルスレッドであるFLTKにて送信ボタン押下状態で中断ボタンを押せるようにするにはstd::threadを用い送信コールバック関数(HTTP通信)を別スレッド化する必要があります。

最初は別スレッド化したHTTP通信のプロセスID(PID)をpgrepコマンドで探し、killコマンドで中断しようとしました。しかしstd::threadでは同じプロセスIDをメインスレッドと共用しているため、その方法は使えませんでした。

昨年2023/3/2にChatGPTアプリ開発に着手してから、ずっと抱えていた懸案をようやく解決できました。

libcurlでHTTP通信が容易に中断できない問題はStackOverFlowサイトでも暗礁に乗り上げていたので意外な結末でした。ChatGPTに聞いてもサンプルコードが少ないためか、珍しく迷回答頻発でした。

#include <thread>
#include <future>
#include <curl/curl.h>

extern CURL* curl;

void sendCBWrapper(Fl_Widget* w, void* v) {
    // promiseとfutureを作成
    std::promise<void> p;
    std::future<void> f = p.get_future();
    
    // sendCBを新しいスレッドで実行し、promiseを渡す
    std::thread([w, v, p = std::move(p)]() mutable {
        sendCB(w, v, std::move(p));
    }).detach();
    
    // 別のスレッドでfutureの結果を待機
    std::thread([f = std::move(f)]() mutable {
        f.wait(); // sendCBの終了を待機
        std::system("afplay /System/Library/Sounds/Submarine.aiff"); // システム音を鳴らす
        cout << "sendCB終了" << endl;
    }).detach();
}

void abortCB(Fl_Widget*, void*){
    if (curl){
        curl_easy_cleanup(curl);
        statusBox -> changeColor(YELLOW);
    } else {
        cout << "送信していません" << endl;
    }
}