From ebe50c9981148aebc0331dfb4a36acae103ef4f1 Mon Sep 17 00:00:00 2001 From: Bartosz Taudul Date: Sun, 18 May 2025 23:37:42 +0200 Subject: [PATCH] Move implementation of all LLM tools to a separate file. --- profiler/CMakeLists.txt | 1 + profiler/src/profiler/TracyLlm.cpp | 277 +---------------------- profiler/src/profiler/TracyLlm.hpp | 20 +- profiler/src/profiler/TracyLlmTools.cpp | 286 ++++++++++++++++++++++++ profiler/src/profiler/TracyLlmTools.hpp | 41 ++++ 5 files changed, 336 insertions(+), 289 deletions(-) create mode 100644 profiler/src/profiler/TracyLlmTools.cpp create mode 100644 profiler/src/profiler/TracyLlmTools.hpp diff --git a/profiler/CMakeLists.txt b/profiler/CMakeLists.txt index c8c21624..251c9539 100644 --- a/profiler/CMakeLists.txt +++ b/profiler/CMakeLists.txt @@ -60,6 +60,7 @@ set(SERVER_FILES TracyFilesystem.cpp TracyImGui.cpp TracyLlm.cpp + TracyLlmTools.cpp TracyMicroArchitecture.cpp TracyMouse.cpp TracyProtoHistory.cpp diff --git a/profiler/src/profiler/TracyLlm.cpp b/profiler/src/profiler/TracyLlm.cpp index 9ce50ab7..00b404f2 100644 --- a/profiler/src/profiler/TracyLlm.cpp +++ b/profiler/src/profiler/TracyLlm.cpp @@ -1,13 +1,7 @@ -#include -#include #include #include #include -#include #include -#include -#include -#include #include "TracyConfig.hpp" #include "TracyImGui.hpp" @@ -172,6 +166,7 @@ void TracyLlm::Draw() { m_modelIdx = i; s_config.llmModel = model.name; + m_tools.SetModelMaxContext( model.ctxSize ); } if( m_modelIdx == i ) ImGui::SetItemDefaultFocus(); ImGui::SameLine(); @@ -207,7 +202,7 @@ void TracyLlm::Draw() ImGui::SetNextItemWidth( 40 * scale ); if( ImGui::InputFloat( "##temperature", &m_temperature, 0, 0, "%.2f" ) ) m_temperature = std::clamp( m_temperature, 0.f, 2.f ); - ImGui::Checkbox( ICON_FA_GLOBE " Internet access", &m_netAccess ); + ImGui::Checkbox( ICON_FA_GLOBE " Internet access", &m_tools.m_netAccess ); ImGui::TreePop(); } @@ -583,12 +578,13 @@ void TracyLlm::UpdateModels() { m_modelIdx = std::distance( m_models.begin(), it ); } + if( !m_models.empty() ) m_tools.SetModelMaxContext( m_models[m_modelIdx].ctxSize ); } void TracyLlm::ResetChat() { auto systemPrompt = std::string( m_systemPrompt->data(), m_systemPrompt->size() ); - systemPrompt += "The current time is: " + GetCurrentTime() + "\n"; + systemPrompt += "The current time is: " + m_tools.GetCurrentTime() + "\n"; m_chat = std::make_unique(); m_chat->emplace_back( ollama::message( "system", systemPrompt ) ); @@ -687,7 +683,7 @@ bool TracyLlm::OnResponse( const ollama::response& response ) auto tool = lines[0]; lines.erase( lines.begin() ); lock.unlock(); - const auto reply = HandleToolCalls( tool, lines ); + const auto reply = m_tools.HandleToolCalls( tool, lines ); const auto output = "\n" + reply.reply; lock.lock(); if( reply.image.empty() ) @@ -822,267 +818,4 @@ void TracyLlm::CleanContext( LineContext& ctx) } } -static std::string UrlEncode( const std::string& str ) -{ - std::string out; - out.reserve( str.size() * 3 ); - - constexpr char hex[] = "0123456789ABCDEF"; - - for( char c : str ) - { - if( ( c >= 'a' && c <= 'z' ) || - ( c >= 'A' && c <= 'Z' ) || - ( c >= '0' && c <= '9' ) || - c == '-' || c == '.' || c == '_' || c == '~' ) - { - out += c; - } - else - { - out += '%'; - out += hex[(unsigned char)c >> 4]; - out += hex[(unsigned char)c & 0x0F]; - } - } - return out; -} - -TracyLlm::ToolReply TracyLlm::HandleToolCalls( const std::string& name, const std::vector& args ) -{ - if( name == "fetch_web_page" ) - { - if( args.empty() ) return { .reply = "Missing URL argument" }; - return { .reply = FetchWebPage( args[0] ) }; - } - if( name == "search_wikipedia" ) - { - if( args.empty() ) return { .reply = "Missing search term argument" }; - if( args.size() < 2 ) return { .reply = "Missing language argument" }; - return SearchWikipedia( args[0], args[1] ); - } - if( name == "get_wikipedia" ) - { - if( args.empty() ) return { .reply = "Missing page name argument" }; - if( args.size() < 2 ) return { .reply = "Missing language argument" }; - return { .reply = GetWikipedia( args[0], args[1] ) }; - } - if( name == "search_web" ) - { - if( args.empty() ) return { .reply = "Missing search term argument" }; - return { .reply = SearchWeb( args[0] ) }; - } - return { .reply = "Unknown tool call: " + name }; -} - -std::string TracyLlm::GetCurrentTime() -{ - auto t = time( nullptr ); - auto tm = localtime( &t ); - - char buffer[64]; - std::strftime( buffer, sizeof( buffer ), "%Y-%m-%d %H:%M:%S", tm ); - - return buffer; -} - -static size_t WriteFn( void* _data, size_t size, size_t num, void* ptr ) -{ - const auto data = (unsigned char*)_data; - const auto sz = size*num; - auto& v = *(std::string*)ptr; - v.append( (const char*)data, sz ); - return sz; -} - -std::string TracyLlm::FetchWebPage( const std::string& url ) -{ - auto it = m_webCache.find( url ); - if( it != m_webCache.end() ) return it->second; - - static bool initialized = false; - if( !initialized ) - { - initialized = true; - curl_global_init( CURL_GLOBAL_ALL ); - atexit( curl_global_cleanup ); - } - - auto curl = curl_easy_init(); - if( !curl ) return "Error: Failed to initialize cURL"; - - std::string buf; - - curl_easy_setopt( curl, CURLOPT_URL, url.c_str() ); - curl_easy_setopt( curl, CURLOPT_CA_CACHE_TIMEOUT, 604800L ); - curl_easy_setopt( curl, CURLOPT_FOLLOWLOCATION, 1L ); - curl_easy_setopt( curl, CURLOPT_TIMEOUT, 10 ); - curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, WriteFn ); - curl_easy_setopt( curl, CURLOPT_WRITEDATA, &buf ); - curl_easy_setopt( curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" ); - - auto res = curl_easy_perform( curl ); - - std::string response; - if( res != CURLE_OK ) - { - response = "Error: " + std::string( curl_easy_strerror( res ) ); - } - else - { - response = std::move( buf ); - } - m_webCache.emplace( url, response ); - - curl_easy_cleanup( curl ); - return response; -} - -TracyLlm::ToolReply TracyLlm::SearchWikipedia( std::string query, const std::string& lang ) -{ - if( !m_netAccess ) return { .reply = "Internet access is disabled by the user." }; - - std::ranges::replace( query, ' ', '+' ); - const auto response = FetchWebPage( "https://" + lang + ".wikipedia.org/w/rest.php/v1/search/page?q=" + UrlEncode( query ) + "&limit=1" ); - - auto json = nlohmann::json::parse( response ); - if( !json.contains( "pages" ) ) return { .reply = "No results found" }; - - auto& page = json["pages"]; - if( page.size() == 0 ) return { .reply = "No results found" }; - - auto& page0 = page[0]; - if( !page0.contains( "key" ) ) return { .reply = "No results found" }; - - const auto key = page0["key"].get_ref(); - - auto summary = FetchWebPage( "https://" + lang + ".wikipedia.org/api/rest_v1/page/summary/" + key ); - auto summaryJson = nlohmann::json::parse( summary ); - - if( !summaryJson.contains( "title" ) ) return { .reply = "No results found" }; - - nlohmann::json output; - output["key"] = key; - output["title"] = summaryJson["title"]; - if( summaryJson.contains( "description" ) ) output["description"] = summaryJson["description"]; - output["extract"] = summaryJson["extract"]; - - std::string image; - if( summaryJson.contains( "thumbnail" ) ) - { - auto& thumb = summaryJson["thumbnail"]; - if( thumb.contains( "source" ) ) - { - auto imgData = FetchWebPage( thumb["source"].get_ref() ); - if( !imgData.empty() && imgData[0] != '<' && strncmp( imgData.c_str(), "Error:", 6 ) != 0 ) - { - size_t b64sz = ( ( 4 * imgData.size() / 3 ) + 3 ) & ~3; - char* b64 = new char[b64sz+1]; - b64[b64sz] = 0; - size_t outSz; - base64_encode( (const char*)imgData.data(), imgData.size(), b64, &outSz, 0 ); - image = std::string( b64, outSz ); - delete[] b64; - } - } - } - - const auto reply = output.dump( 2 ); - return { .reply = reply, .image = image }; -} - -std::string TracyLlm::GetWikipedia( std::string page, const std::string& lang ) -{ - if( !m_netAccess ) return "Internet access is disabled by the user."; - - std::ranges::replace( page, ' ', '_' ); - auto res = FetchWebPage( "https://" + lang + ".wikipedia.org/w/rest.php/v1/page/" + page ); - - // Limit the size of the response to avoid exceeding the context size - // Assume average token size is 4 bytes. Make space for 3 articles to be retrieved. - const auto ctxSize = std::min( m_models[m_modelIdx].ctxSize, s_config.llmContext ); - const auto maxSize = ( ctxSize * 4 ) / 3; - - if( res.size() > maxSize ) res = res.substr( 0, maxSize ); - return res; -} - -static std::string RemoveNewline( std::string str ) -{ - std::erase( str, '\r' ); - std::ranges::replace( str, '\n', ' ' ); - return str; -} - -std::string TracyLlm::SearchWeb( std::string query ) -{ - if( !m_netAccess ) return "Internet access is disabled by the user."; - - std::ranges::replace( query, ' ', '+' ); - const auto response = FetchWebPage( "https://lite.duckduckgo.com/lite?q=" + UrlEncode( query ) ); - - TidyBuffer err = {}; - tidyBufInit( &err ); - - TidyDoc td = tidyCreate(); - tidyOptSetBool( td, TidyXhtmlOut, yes ); - tidyOptSetBool( td, TidyLowerLiterals, yes ); - tidyOptSetBool( td, TidyMark, no ); - tidyOptSetBool( td, TidyHideComments, yes ); - tidySetErrorBuffer( td, &err ); - - if( tidyParseString( td, response.c_str() ) == 2 ) - { - auto out = std::string( (const char*)err.bp ); - tidyBufFree( &err ); - tidyRelease( td ); - return out; - } - - TidyBuffer buf = {}; - tidyBufInit( &buf ); - tidyCleanAndRepair( td ); - tidySaveBuffer( td, &buf ); - - auto tidy = std::string( (const char*)buf.bp ); - - tidyBufFree( &buf ); - tidyBufFree( &err ); - tidyRelease( td ); - - auto doc = std::make_unique(); - if( !doc->load_string( tidy.c_str() ) ) - { - return "Error: Failed to parse HTML"; - } - - const auto titles = doc->select_nodes( "//a[@class='result-link']" ); - const auto snippets = doc->select_nodes( "//td[@class='result-snippet']" ); - const auto urls = doc->select_nodes( "//span[@class='link-text']" ); - - const auto sz = titles.size(); - if( sz != snippets.size() || sz != urls.size() ) - { - return "Error: Failed to parse HTML"; - } - - nlohmann::json json; - - for( size_t i = 0; i < sz; i++ ) - { - auto title = titles[i].node(); - auto snippet = snippets[i].node(); - auto url = urls[i].node(); - - nlohmann::json result; - result["title"] = RemoveNewline( title.text().as_string() ); - result["snippet"] = RemoveNewline( snippet.text().as_string() ); - result["url"] = RemoveNewline( url.text().as_string() ); - - json[i] = result; - } - - return json.dump( 2 ); -} - } diff --git a/profiler/src/profiler/TracyLlm.hpp b/profiler/src/profiler/TracyLlm.hpp index 4c90aa57..c6851c0c 100644 --- a/profiler/src/profiler/TracyLlm.hpp +++ b/profiler/src/profiler/TracyLlm.hpp @@ -11,6 +11,7 @@ #include #include "TracyEmbed.hpp" +#include "TracyLlmTools.hpp" #include "tracy_robin_hood.h" struct ImFont; @@ -52,12 +53,6 @@ class TracyLlm bool codeBlock; }; - struct ToolReply - { - std::string reply; - std::string image; - }; - public: struct LlmModel { @@ -96,14 +91,6 @@ private: void PrintLine( LineContext& ctx, const std::string& str, int num ); void CleanContext( LineContext& ctx); - ToolReply HandleToolCalls( const std::string& name, const std::vector& args ); - - std::string GetCurrentTime(); - std::string FetchWebPage( const std::string& url ); - ToolReply SearchWikipedia( std::string query, const std::string& lang ); - std::string GetWikipedia( std::string page, const std::string& lang ); - std::string SearchWeb( std::string query ); - std::unique_ptr m_ollama; mutable std::mutex m_modelsLock; @@ -125,19 +112,18 @@ private: int m_usedCtx = 0; float m_temperature = 1.0f; bool m_setTemperature = false; - bool m_netAccess = true; char* m_input; std::unique_ptr m_chat; unordered_flat_map m_chatCache; - unordered_flat_map m_webCache; - ImFont* m_font; ImFont* m_smallFont; ImFont* m_bigFont; std::shared_ptr m_systemPrompt; + + TracyLlmTools m_tools; }; } diff --git a/profiler/src/profiler/TracyLlmTools.cpp b/profiler/src/profiler/TracyLlmTools.cpp new file mode 100644 index 00000000..dfcc67b0 --- /dev/null +++ b/profiler/src/profiler/TracyLlmTools.cpp @@ -0,0 +1,286 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "TracyConfig.hpp" +#include "TracyLlmTools.hpp" + +extern tracy::Config s_config; + +namespace tracy +{ + +void TracyLlmTools::SetModelMaxContext( int modelMaxContext ) +{ + m_modelMaxContext = modelMaxContext; +} + +static std::string UrlEncode( const std::string& str ) +{ + std::string out; + out.reserve( str.size() * 3 ); + + constexpr char hex[] = "0123456789ABCDEF"; + + for( char c : str ) + { + if( ( c >= 'a' && c <= 'z' ) || + ( c >= 'A' && c <= 'Z' ) || + ( c >= '0' && c <= '9' ) || + c == '-' || c == '.' || c == '_' || c == '~' ) + { + out += c; + } + else + { + out += '%'; + out += hex[(unsigned char)c >> 4]; + out += hex[(unsigned char)c & 0x0F]; + } + } + return out; +} + +TracyLlmTools::ToolReply TracyLlmTools::HandleToolCalls( const std::string& name, const std::vector& args ) +{ + if( name == "fetch_web_page" ) + { + if( args.empty() ) return { .reply = "Missing URL argument" }; + return { .reply = FetchWebPage( args[0] ) }; + } + if( name == "search_wikipedia" ) + { + if( args.empty() ) return { .reply = "Missing search term argument" }; + if( args.size() < 2 ) return { .reply = "Missing language argument" }; + return SearchWikipedia( args[0], args[1] ); + } + if( name == "get_wikipedia" ) + { + if( args.empty() ) return { .reply = "Missing page name argument" }; + if( args.size() < 2 ) return { .reply = "Missing language argument" }; + return { .reply = GetWikipedia( args[0], args[1] ) }; + } + if( name == "search_web" ) + { + if( args.empty() ) return { .reply = "Missing search term argument" }; + return { .reply = SearchWeb( args[0] ) }; + } + return { .reply = "Unknown tool call: " + name }; +} + +std::string TracyLlmTools::GetCurrentTime() +{ + auto t = time( nullptr ); + auto tm = localtime( &t ); + + char buffer[64]; + strftime( buffer, sizeof( buffer ), "%Y-%m-%d %H:%M:%S", tm ); + + return buffer; +} + +static size_t WriteFn( void* _data, size_t size, size_t num, void* ptr ) +{ + const auto data = (unsigned char*)_data; + const auto sz = size*num; + auto& v = *(std::string*)ptr; + v.append( (const char*)data, sz ); + return sz; +} + +std::string TracyLlmTools::FetchWebPage( const std::string& url ) +{ + auto it = m_webCache.find( url ); + if( it != m_webCache.end() ) return it->second; + + static bool initialized = false; + if( !initialized ) + { + initialized = true; + curl_global_init( CURL_GLOBAL_ALL ); + atexit( curl_global_cleanup ); + } + + auto curl = curl_easy_init(); + if( !curl ) return "Error: Failed to initialize cURL"; + + std::string buf; + + curl_easy_setopt( curl, CURLOPT_URL, url.c_str() ); + curl_easy_setopt( curl, CURLOPT_CA_CACHE_TIMEOUT, 604800L ); + curl_easy_setopt( curl, CURLOPT_FOLLOWLOCATION, 1L ); + curl_easy_setopt( curl, CURLOPT_TIMEOUT, 10 ); + curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, WriteFn ); + curl_easy_setopt( curl, CURLOPT_WRITEDATA, &buf ); + curl_easy_setopt( curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" ); + + auto res = curl_easy_perform( curl ); + + std::string response; + if( res != CURLE_OK ) + { + response = "Error: " + std::string( curl_easy_strerror( res ) ); + } + else + { + response = std::move( buf ); + } + m_webCache.emplace( url, response ); + + curl_easy_cleanup( curl ); + return response; +} + +TracyLlmTools::ToolReply TracyLlmTools::SearchWikipedia( std::string query, const std::string& lang ) +{ + if( !m_netAccess ) return { .reply = "Internet access is disabled by the user." }; + + std::ranges::replace( query, ' ', '+' ); + const auto response = FetchWebPage( "https://" + lang + ".wikipedia.org/w/rest.php/v1/search/page?q=" + UrlEncode( query ) + "&limit=1" ); + + auto json = nlohmann::json::parse( response ); + if( !json.contains( "pages" ) ) return { .reply = "No results found" }; + + auto& page = json["pages"]; + if( page.size() == 0 ) return { .reply = "No results found" }; + + auto& page0 = page[0]; + if( !page0.contains( "key" ) ) return { .reply = "No results found" }; + + const auto key = page0["key"].get_ref(); + + auto summary = FetchWebPage( "https://" + lang + ".wikipedia.org/api/rest_v1/page/summary/" + key ); + auto summaryJson = nlohmann::json::parse( summary ); + + if( !summaryJson.contains( "title" ) ) return { .reply = "No results found" }; + + nlohmann::json output; + output["key"] = key; + output["title"] = summaryJson["title"]; + if( summaryJson.contains( "description" ) ) output["description"] = summaryJson["description"]; + output["extract"] = summaryJson["extract"]; + + std::string image; + if( summaryJson.contains( "thumbnail" ) ) + { + auto& thumb = summaryJson["thumbnail"]; + if( thumb.contains( "source" ) ) + { + auto imgData = FetchWebPage( thumb["source"].get_ref() ); + if( !imgData.empty() && imgData[0] != '<' && strncmp( imgData.c_str(), "Error:", 6 ) != 0 ) + { + size_t b64sz = ( ( 4 * imgData.size() / 3 ) + 3 ) & ~3; + char* b64 = new char[b64sz+1]; + b64[b64sz] = 0; + size_t outSz; + base64_encode( (const char*)imgData.data(), imgData.size(), b64, &outSz, 0 ); + image = std::string( b64, outSz ); + delete[] b64; + } + } + } + + const auto reply = output.dump( 2 ); + return { .reply = reply, .image = image }; +} + +std::string TracyLlmTools::GetWikipedia( std::string page, const std::string& lang ) +{ + if( !m_netAccess ) return "Internet access is disabled by the user."; + + std::ranges::replace( page, ' ', '_' ); + auto res = FetchWebPage( "https://" + lang + ".wikipedia.org/w/rest.php/v1/page/" + page ); + + // Limit the size of the response to avoid exceeding the context size + // Assume average token size is 4 bytes. Make space for 3 articles to be retrieved. + assert( m_modelMaxContext != 0 ); + const auto ctxSize = std::min( m_modelMaxContext, s_config.llmContext ); + const auto maxSize = ( ctxSize * 4 ) / 3; + + if( res.size() > maxSize ) res = res.substr( 0, maxSize ); + return res; +} + +static std::string RemoveNewline( std::string str ) +{ + std::erase( str, '\r' ); + std::ranges::replace( str, '\n', ' ' ); + return str; +} + +std::string TracyLlmTools::SearchWeb( std::string query ) +{ + if( !m_netAccess ) return "Internet access is disabled by the user."; + + std::ranges::replace( query, ' ', '+' ); + const auto response = FetchWebPage( "https://lite.duckduckgo.com/lite?q=" + UrlEncode( query ) ); + + TidyBuffer err = {}; + tidyBufInit( &err ); + + TidyDoc td = tidyCreate(); + tidyOptSetBool( td, TidyXhtmlOut, yes ); + tidyOptSetBool( td, TidyLowerLiterals, yes ); + tidyOptSetBool( td, TidyMark, no ); + tidyOptSetBool( td, TidyHideComments, yes ); + tidySetErrorBuffer( td, &err ); + + if( tidyParseString( td, response.c_str() ) == 2 ) + { + auto out = std::string( (const char*)err.bp ); + tidyBufFree( &err ); + tidyRelease( td ); + return out; + } + + TidyBuffer buf = {}; + tidyBufInit( &buf ); + tidyCleanAndRepair( td ); + tidySaveBuffer( td, &buf ); + + auto tidy = std::string( (const char*)buf.bp ); + + tidyBufFree( &buf ); + tidyBufFree( &err ); + tidyRelease( td ); + + auto doc = std::make_unique(); + if( !doc->load_string( tidy.c_str() ) ) + { + return "Error: Failed to parse HTML"; + } + + const auto titles = doc->select_nodes( "//a[@class='result-link']" ); + const auto snippets = doc->select_nodes( "//td[@class='result-snippet']" ); + const auto urls = doc->select_nodes( "//span[@class='link-text']" ); + + const auto sz = titles.size(); + if( sz != snippets.size() || sz != urls.size() ) + { + return "Error: Failed to parse HTML"; + } + + nlohmann::json json; + + for( size_t i = 0; i < sz; i++ ) + { + auto title = titles[i].node(); + auto snippet = snippets[i].node(); + auto url = urls[i].node(); + + nlohmann::json result; + result["title"] = RemoveNewline( title.text().as_string() ); + result["snippet"] = RemoveNewline( snippet.text().as_string() ); + result["url"] = RemoveNewline( url.text().as_string() ); + + json[i] = result; + } + + return json.dump( 2 ); +} + +} diff --git a/profiler/src/profiler/TracyLlmTools.hpp b/profiler/src/profiler/TracyLlmTools.hpp new file mode 100644 index 00000000..b95297fc --- /dev/null +++ b/profiler/src/profiler/TracyLlmTools.hpp @@ -0,0 +1,41 @@ +#ifndef __TRACYLLMTOOLS_HPP__ +#define __TRACYLLMTOOLS_HPP__ + +#include +#include + +#include "tracy_robin_hood.h" + +namespace tracy +{ + +class TracyLlmTools +{ +public: + struct ToolReply + { + std::string reply; + std::string image; + }; + + void SetModelMaxContext( int modelMaxContext ); + + ToolReply HandleToolCalls( const std::string& name, const std::vector& args ); + std::string GetCurrentTime(); + + bool m_netAccess = true; + +private: + std::string FetchWebPage( const std::string& url ); + ToolReply SearchWikipedia( std::string query, const std::string& lang ); + std::string GetWikipedia( std::string page, const std::string& lang ); + std::string SearchWeb( std::string query ); + + unordered_flat_map m_webCache; + + int m_modelMaxContext = 0; +}; + +} + +#endif