Take the thinking and tool use context into account in LLM chat rendering.

This commit is contained in:
Bartosz Taudul
2025-12-29 20:41:36 +01:00
parent ac5dabbb94
commit c996c67390
3 changed files with 81 additions and 97 deletions

View File

@@ -430,14 +430,11 @@ void TracyLlm::Draw()
if( !line.contains( "role" ) ) break;
const auto& roleStr = line["role"].get_ref<const std::string&>();
if( roleStr == "system" ) continue;
const auto& contentNode = line["content"];
if( !contentNode.is_string() ) continue;
const auto& content = contentNode.get_ref<const std::string&>();
TracyLlmChat::TurnRole role = TracyLlmChat::TurnRole::None;
if( roleStr == "user" ) role = TracyLlmChat::TurnRole::User;
else if( roleStr == "error" ) role = TracyLlmChat::TurnRole::Error;
else if( roleStr == "assistant" ) role = TracyLlmChat::TurnRole::Assistant;
else if( roleStr == "assistant" || roleStr == "tool" ) role = TracyLlmChat::TurnRole::Assistant;
else assert( false );
if( role == TracyLlmChat::TurnRole::User )
@@ -446,7 +443,7 @@ void TracyLlm::Draw()
}
ImGui::PushID( turnIdx++ );
if( !m_chatUi->Turn( role, content ) )
if( !m_chatUi->Turn( role, line ) )
{
if( role == TracyLlmChat::TurnRole::Assistant )
{

View File

@@ -72,7 +72,7 @@ void TracyLlmChat::End()
}
}
bool TracyLlmChat::Turn( TurnRole role, const std::string& content )
bool TracyLlmChat::Turn( TurnRole role, const nlohmann::json& json )
{
bool keep = true;
const auto& roleData = roles[(int)role];
@@ -166,120 +166,107 @@ bool TracyLlmChat::Turn( TurnRole role, const std::string& content )
}
else if( role != TurnRole::Assistant )
{
auto pos = content.find( "<SYSTEM_PROMPT>" );
if( pos == std::string::npos )
if( json.contains( "content" ) )
{
auto& content = json["content"].get_ref<const std::string&>();
m_markdown.Print( content.c_str(), content.size() );
}
else
{
auto str = content;
while( ( pos = str.find( "<SYSTEM_PROMPT>" ) ) != std::string::npos )
{
auto pos2 = str.find( "</SYSTEM_PROMPT>" );
str = str.substr( 0, pos ) + str.substr( pos2 + sizeof( "</SYSTEM_PROMPT>" ) - 1 );
}
m_markdown.Print( str.c_str(), str.size() );
}
}
else if( content.starts_with( "<tool_output>\n" ) )
{
ThinkScope();
if( m_thinkOpen )
{
ImGui::PushStyleColor( ImGuiCol_Text, ImVec4( 0.5f, 0.5f, 0.5f, 1.f ) );
if( content == ForgetMsg )
{
ImGui::TextUnformatted( ICON_FA_RECYCLE " Tool response removed to save context space" );
m_subIdx++;
}
else
{
ImGui::PushID( m_subIdx++ );
if( ImGui::TreeNode( ICON_FA_REPLY " Tool response..." ) )
{
ImGui::PushFont( g_fonts.mono, FontNormal );
ImGui::TextWrapped( "%s", content.c_str() + sizeof( "<tool_output>\n" ) - 1 );
ImGui::PopFont();
ImGui::TreePop();
}
ImGui::PopID();
}
ImGui::PopStyleColor();
}
else
{
m_subIdx++;
}
}
else
{
size_t pos = 0;
size_t end = content.size();
while( pos < end )
if( json.contains( "reasoning_content" ) )
{
auto posThink = content.find( "<think>", pos );
auto posTool = content.find( "<tool>", pos );
auto minPos = std::min( posThink, posTool );
if( pos != minPos )
ThinkScope();
if( m_thinkOpen )
{
NormalScope();
m_markdown.Print( content.c_str() + pos, std::min( end, minPos ) - pos );
auto& reasoning = json["reasoning_content"].get_ref<const std::string&>();
PrintThink( reasoning.c_str(), reasoning.size() );
}
pos = minPos;
if( pos == std::string::npos ) break;
if( minPos == posThink )
}
if( json.contains( "tool_calls" ) )
{
ThinkScope();
if( m_thinkOpen )
{
pos += sizeof( "<think>" ) - 1;
while( content[pos] == '\n' || content[pos] == ' ' ) pos++;
auto endThink = content.find( "</think>", pos );
if( endThink != pos )
auto calls = json["tool_calls"].dump( 2 );
PrintToolCall( calls.c_str(), calls.size() );
}
}
if( json.contains( "content" ) )
{
auto& content = json["content"].get_ref<const std::string&>();
if( json["role"].get_ref<const std::string&>() == "tool" )
{
ThinkScope();
if( m_thinkOpen )
{
ThinkScope();
if( m_thinkOpen ) PrintThink( content.c_str() + pos, std::min( end, endThink ) - pos );
ImGui::PushStyleColor( ImGuiCol_Text, ImVec4( 0.5f, 0.5f, 0.5f, 1.f ) );
if( content == ForgetMsg )
{
ImGui::TextUnformatted( ICON_FA_RECYCLE " Tool response removed to save context space" );
m_subIdx++;
}
else
{
auto& name = json["name"].get_ref<const std::string&>();
auto& id = json["tool_call_id"].get_ref<const std::string&>();
ImGui::PushID( m_subIdx++ );
char buf[1024];
snprintf( buf, sizeof( buf ), ICON_FA_REPLY " Tool response (%s/%s)...", name.c_str(), id.substr( 0, 8 ).c_str() );
if( ImGui::TreeNode( buf ) )
{
std::string parsed;
try
{
parsed = nlohmann::json::parse( content.c_str() ).dump( 2 );
}
catch( nlohmann::json::exception& )
{
parsed = content;
}
ImGui::PushFont( g_fonts.mono, FontNormal );
ImGui::TextWrapped( "%s", parsed.c_str() );
ImGui::PopFont();
ImGui::TreePop();
}
ImGui::PopID();
}
ImGui::PopStyleColor();
}
if( endThink == std::string::npos ) break;
pos = endThink;
do
else
{
pos += sizeof( "</think>" ) - 1;
while( content[pos] == '\n' || content[pos] == ' ' ) pos++;
m_subIdx++;
}
while( strncmp( content.c_str() + pos, "</think>", sizeof( "</think>" ) - 1 ) == 0 );
}
else
{
assert( minPos == posTool );
pos += sizeof( "<tool>" ) - 1;
while( content[pos] == '\n' || content[pos] == ' ' ) pos++;
auto endTool = content.find( "</tool>", pos );
ThinkScope();
if( m_thinkOpen ) PrintToolCall( content.c_str() + pos, std::min( end, endTool ) - pos );
if( endTool == std::string::npos ) break;
pos = endTool + sizeof( "</tool>" ) - 1;
while( content[pos] == '\n' || content[pos] == ' ' ) pos++;
if( !content.empty() )
{
NormalScope();
m_markdown.Print( content.c_str(), content.size() );
}
}
}
}
ImGui::PopStyleColor();
if( ImGui::IsMouseClicked( ImGuiMouseButton_Right ) &&
ImGui::IsWindowHovered() &&
ImGui::IsMouseHoveringRect( posStart, ImGui::GetCursorScreenPos() + ImVec2( ImGui::GetContentRegionAvail().x, 0 ) ) )
if( json.contains( "content" ) )
{
ImGui::OpenPopup( "ContextMenu" );
}
if( ImGui::BeginPopup( "ContextMenu" ) )
{
if( ImGui::Selectable( ICON_FA_CLIPBOARD " Copy" ) )
if( ImGui::IsMouseClicked( ImGuiMouseButton_Right ) &&
ImGui::IsWindowHovered() &&
ImGui::IsMouseHoveringRect( posStart, ImGui::GetCursorScreenPos() + ImVec2( ImGui::GetContentRegionAvail().x, 0 ) ) )
{
ImGui::SetClipboardText( content.c_str() );
ImGui::CloseCurrentPopup();
ImGui::OpenPopup( "ContextMenu" );
}
if( ImGui::BeginPopup( "ContextMenu" ) )
{
if( ImGui::Selectable( ICON_FA_CLIPBOARD " Copy" ) )
{
ImGui::SetClipboardText( json["content"].get_ref<const std::string&>().c_str() );
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::EndPopup();
}
return keep;

View File

@@ -1,7 +1,7 @@
#ifndef __TRACYLLMCHAT_HPP__
#define __TRACYLLMCHAT_HPP__
#include <string>
#include <nlohmann/json.hpp>
#include "TracyMarkdown.hpp"
@@ -31,7 +31,7 @@ public:
void Begin();
void End();
bool Turn( TurnRole role, const std::string& content );
bool Turn( TurnRole role, const nlohmann::json& json );
private:
void NormalScope();