diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json index 2607e11e7..ab9352a70 100644 --- a/bin/assets/plugins/aiassistant.json +++ b/bin/assets/plugins/aiassistant.json @@ -38,7 +38,8 @@ "display_name": "Claude 3.5 Haiku", "max_output_tokens": 8192, "max_tokens": 200000, - "name": "claude-3-5-haiku-latest" + "name": "claude-3-5-haiku-latest", + "cheapest": true }, { "cache_configuration": null, @@ -79,7 +80,8 @@ "display_name": "DeepSeek Chat", "max_output_tokens": 8192, "max_tokens": 64000, - "name": "deepseek-chat" + "name": "deepseek-chat", + "cheapest": true }, { "display_name": "DeepSeek Reasoner", @@ -121,7 +123,8 @@ { "display_name": "Gemini 2.0 Flash Lite", "max_tokens": 1000000, - "name": "gemini-2.0-flash-lite-preview" + "name": "gemini-2.0-flash-lite-preview", + "cheapest": true } ] }, @@ -148,7 +151,8 @@ { "display_name": "mistral-small-latest", "max_tokens": 32000, - "name": "mistral-small-latest" + "name": "mistral-small-latest", + "cheapest": true }, { "display_name": "open-mistral-nemo", @@ -190,7 +194,8 @@ }, { "max_tokens": 128000, - "name": "gpt-4o-mini" + "name": "gpt-4o-mini", + "cheapest": true }, { "max_tokens": 200000, @@ -216,7 +221,8 @@ "display_name": "xAI", "models": [ { - "name": "grok-2-latest" + "name": "grok-2-latest", + "cheapest": true }, { "name": "grok-3-latest" diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index 60a0b8259..9bc353285 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -1109,9 +1109,12 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // Close the connection if ( !mConnection->isKeepAlive() ) { mConnection->disconnect(); - HttpConnection* connection = mConnection; - eeSAFE_DELETE( connection ); - mConnection = NULL; + + if ( mConnection ) { + HttpConnection* connection = mConnection; + eeSAFE_DELETE( connection ); + mConnection = NULL; + } } } @@ -1177,9 +1180,11 @@ void Http::AsyncRequest::run() { } // The Async Request destroys the socket used to create the request - HttpConnection* connection = mHttp->mConnection; - eeSAFE_DELETE( connection ); - mHttp->mConnection = NULL; + if ( mHttp->mConnection ) { + HttpConnection* connection = mHttp->mConnection; + eeSAFE_DELETE( connection ); + mHttp->mConnection = NULL; + } mRunning = false; } diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp index a9004c0b3..739e51934 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp @@ -57,15 +57,23 @@ static std::map parseLLMProviders( const nlohmann::jso if ( modelJson.contains( "display_name" ) ) { model.displayName = modelJson["display_name"].get(); } + if ( modelJson.contains( "max_tokens" ) ) { model.maxTokens = modelJson["max_tokens"].get(); } + if ( modelJson.contains( "max_output_tokens" ) ) { model.maxOutputTokens = modelJson["max_output_tokens"].get(); } + if ( modelJson.contains( "default_temperature" ) ) { model.defaultTemperature = modelJson["default_temperature"].get(); } + + if ( modelJson.contains( "cheapest" ) ) { + model.cheapest = modelJson.value( "cheapest", false ); + } + if ( modelJson.contains( "cache_configuration" ) && !modelJson["cache_configuration"].is_null() ) { const auto& cacheJson = modelJson["cache_configuration"]; diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 3c5a17e28..0f836f9bc 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -1,6 +1,7 @@ #include "aiassistantplugin.hpp" #include "chatui.hpp" +#include #include #include #include @@ -14,6 +15,7 @@ #include +using namespace EE::System; using namespace EE::Window; namespace ecode { @@ -398,22 +400,36 @@ Uint32 LLMChatUI::onMessage( const NodeMessage* msg ) { return 0; } -nlohmann::json LLMChatUI::serializeChat() { +nlohmann::json LLMChatUI::serializeChat( const LLMModel& model ) { nlohmann::json j = { - { "model", mCurModel.name }, { "stream", true }, { "messages", chatToJson() } }; - if ( mCurModel.maxOutputTokens ) - j["max_tokens"] = *mCurModel.maxOutputTokens; + { "model", model.name }, { "stream", true }, { "messages", chatToJson() } }; + if ( model.maxOutputTokens ) + j["max_tokens"] = *model.maxOutputTokens; return j; } nlohmann::json LLMChatUI::serialize() { nlohmann::json j; j["uuid"] = mUUID.toString(); - j["chat"] = serializeChat(); + j["chat"] = serializeChat( mCurModel ); return j; } -void unserialize( const nlohmann::json& /*payload*/ ) {} +void LLMChatUI::unserialize( const nlohmann::json& /*payload*/ ) {} + +void LLMChatUI::saveChat() { + auto plugin = getPlugin(); + if ( plugin == nullptr ) + return; + + std::string pluginsStatePath( plugin->getManager()->getPluginsPath() + "plugins_state" + + FileSystem::getOSSlash() + "aiassistant" + + FileSystem::getOSSlash() ); + if ( !FileSystem::fileExists( pluginsStatePath ) ) + FileSystem::makeDir( pluginsStatePath, true ); + std::string path( pluginsStatePath + mSummary + ".json" ); + FileSystem::fileWrite( path, serialize().dump( 2 ) ); +} std::string LLMChatUI::prepareApiUrl( const std::string& apiKey ) { const auto& provider = mProviders[mCurModel.provider]; @@ -441,9 +457,11 @@ void LLMChatUI::doRequest() { UIWidget* chat = addChatUI( LLMChat::Role::Assistant ); toggleEnableChats( false ); + auto model = mCurModel; auto* editor = chat->findByClass( "data_ui" ); + std::string apiUrl( prepareApiUrl( apiKeyStr ) ); mRequest = std::make_unique( - prepareApiUrl( apiKeyStr ), apiKeyStr, serializeChat().dump(), mCurModel.provider ); + apiUrl, apiKeyStr, serializeChat( model ).dump(), model.provider ); mRequest->streamedResponseCb = [this, editor]( const std::string& chunk ) { auto conversation = chunk; editor->runOnMainThread( [this, conversation = std::move( conversation ), editor] { @@ -452,11 +470,14 @@ void LLMChatUI::doRequest() { resizeToFit( editor ); } ); }; - mRequest->doneCb = [this, editor]( const LLMChatCompletionRequest&, Http::Response& response ) { + mRequest->doneCb = [this, editor, apiUrl = std::move( apiUrl ), + apiKeyStr = std::move( apiKeyStr ), model = std::move( model )]( + const LLMChatCompletionRequest&, Http::Response& response ) { auto status = response.getStatus(); auto statusDesc = response.getStatusDescription(); - runOnMainThread( [this, editor, status, statusDesc] { + runOnMainThread( [this, editor, status, statusDesc, apiUrl = std::move( apiUrl ), + apiKeyStr = std::move( apiKeyStr ), model = std::move( model )] { if ( status != Http::Response::Ok ) { auto resp = nlohmann::json::parse( mRequest->getStream(), nullptr, false ); if ( resp.contains( "error" ) && resp["error"].contains( "message" ) ) { @@ -477,6 +498,38 @@ void LLMChatUI::doRequest() { if ( editor->hasFocus() ) mChatInput->setFocus(); + + if ( !mSummaryRequest && mSummary.empty() && status == Http::Response::Ok ) { + static const std::string SummaryPrompt = + "Generate a concise 3-7 word title for this conversation, omitting " + "punctuation. Go " + "straight to the title, without any preamble and prefix like `Here's a concise " + "suggestion:...` or `Title:`. Ignore this message for the summary generation."; + + auto jchat = serializeChat( getCheapestModelFromCurrentProvider() ); + + jchat["messages"].push_back( + { { "role", LLMChat::roleToString( LLMChat::Role::User ) }, + { "content", SummaryPrompt } } ); + + auto chatstr = jchat.dump(); + + mSummaryRequest = std::make_unique( + apiUrl, apiKeyStr, chatstr, model.provider ); + + mSummaryRequest->doneCb = [this]( const LLMChatCompletionRequest& req, + Http::Response& response ) { + auto status = response.getStatus(); + if ( status == Http::Response::Ok ) { + mSummary = req.getResponse(); + saveChat(); + } + runOnMainThread( [this] { mSummaryRequest.reset(); } ); + }; + mSummaryRequest->requestAsync(); + } else { + saveChat(); + } } ); }; mRequest->requestAsync(); @@ -612,4 +665,14 @@ AIAssistantPlugin* LLMChatUI::getPlugin() { return nullptr; } +const LLMModel& LLMChatUI::getCheapestModelFromCurrentProvider() const { + auto providerIt = mProviders.find( mCurModel.provider ); + if ( providerIt != mProviders.end() ) { + for ( const auto& model : providerIt->second.models ) + if ( model.cheapest ) + return model; + } + return mCurModel; +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/aiassistant/chatui.hpp b/src/tools/ecode/plugins/aiassistant/chatui.hpp index d493b6084..4a6ad0749 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.hpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.hpp @@ -53,6 +53,7 @@ class LLMChatUI : public UILinearLayout { protected: UUID mUUID; + std::string mSummary; PluginManager* mManager{ nullptr }; UIWidget* mChatsList{ nullptr }; UICodeEditor* mChatInput{ nullptr }; @@ -62,6 +63,7 @@ class LLMChatUI : public UILinearLayout { UIScrollView* mChatScrollView{ nullptr }; UIDropDownList* mModelDDL{ nullptr }; std::unique_ptr mRequest; + std::unique_ptr mSummaryRequest; LLMProviders mProviders; LLMModel mCurModel; std::unordered_map mModelsMap; @@ -72,7 +74,7 @@ class LLMChatUI : public UILinearLayout { void showMsg( String msg ); - nlohmann::json serializeChat(); + nlohmann::json serializeChat( const LLMModel& model ); nlohmann::json chatToJson(); @@ -101,6 +103,10 @@ class LLMChatUI : public UILinearLayout { void setProviders( LLMProviders&& providers ); virtual Uint32 onMessage( const NodeMessage* ); + + const LLMModel& getCheapestModelFromCurrentProvider() const; + + void saveChat(); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp index db5ca79be..7d62e91e4 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.cpp @@ -103,7 +103,7 @@ void LLMChatCompletionRequest::cancel() { mCancel = true; } -const std::string& LLMChatCompletionRequest::getStream() { +const std::string& LLMChatCompletionRequest::getStream() const { return mStream.getStream(); } diff --git a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp index 4cc25e12b..a148ea4c0 100644 --- a/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp +++ b/src/tools/ecode/plugins/aiassistant/llmchatcompletionrequest.hpp @@ -32,7 +32,9 @@ class LLMChatCompletionRequest { void cancel(); - const std::string& getStream(); + const std::string& getStream() const; + + const std::string& getResponse() const { return mResponse; } protected: URI mUrl; diff --git a/src/tools/ecode/plugins/aiassistant/protocol.hpp b/src/tools/ecode/plugins/aiassistant/protocol.hpp index ca7b087b1..8b07313eb 100644 --- a/src/tools/ecode/plugins/aiassistant/protocol.hpp +++ b/src/tools/ecode/plugins/aiassistant/protocol.hpp @@ -22,6 +22,7 @@ struct LLMModel { std::optional defaultTemperature; std::optional cacheConfiguration; bool isEphemeral{ false }; + bool cheapest{ false }; }; struct LLMProvider {