Move implementation of all LLM tools to a separate file.

This commit is contained in:
Bartosz Taudul
2025-05-18 23:37:42 +02:00
parent 4af390719c
commit ebe50c9981
5 changed files with 336 additions and 289 deletions

View File

@@ -60,6 +60,7 @@ set(SERVER_FILES
TracyFilesystem.cpp
TracyImGui.cpp
TracyLlm.cpp
TracyLlmTools.cpp
TracyMicroArchitecture.cpp
TracyMouse.cpp
TracyProtoHistory.cpp

View File

@@ -1,13 +1,7 @@
#include <libbase64.h>
#include <curl/curl.h>
#include <ollama.hpp>
#include <stdint.h>
#include <stdlib.h>
#include <pugixml.hpp>
#include <ranges>
#include <tidy.h>
#include <tidybuffio.h>
#include <time.h>
#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<ollama::messages>();
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 = "<tool_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<std::string>& 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<const std::string&>();
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<const std::string&>() );
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<pugi::xml_document>();
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 );
}
}

View File

@@ -11,6 +11,7 @@
#include <vector>
#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<std::string>& 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<Ollama> 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<ollama::messages> m_chat;
unordered_flat_map<size_t, ChatCache> m_chatCache;
unordered_flat_map<std::string, std::string> m_webCache;
ImFont* m_font;
ImFont* m_smallFont;
ImFont* m_bigFont;
std::shared_ptr<EmbedData> m_systemPrompt;
TracyLlmTools m_tools;
};
}

View File

@@ -0,0 +1,286 @@
#include <curl/curl.h>
#include <json.hpp>
#include <libbase64.h>
#include <pugixml.hpp>
#include <tidy.h>
#include <tidybuffio.h>
#include <time.h>
#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<std::string>& 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<const std::string&>();
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<const std::string&>() );
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<pugi::xml_document>();
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 );
}
}

View File

@@ -0,0 +1,41 @@
#ifndef __TRACYLLMTOOLS_HPP__
#define __TRACYLLMTOOLS_HPP__
#include <string>
#include <vector>
#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<std::string>& 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<std::string, std::string> m_webCache;
int m_modelMaxContext = 0;
};
}
#endif