From 94cf867d36fc3321a576600e099cf59959228436 Mon Sep 17 00:00:00 2001 From: scawful Date: Fri, 3 Oct 2025 23:55:08 -0400 Subject: [PATCH] feat: Add simple chat session for AI agent interaction and enhance message rendering --- CMakeLists.txt | 4 + src/app/gui/widgets/agent_chat_widget.cc | 114 ++++--------- src/app/gui/widgets/agent_chat_widget.h | 2 +- src/cli/handlers/agent.cc | 5 +- src/cli/handlers/agent/commands.h | 1 + src/cli/handlers/agent/general_commands.cc | 27 ++++ src/cli/service/agent/simple_chat_session.cc | 160 +++++++++++++++++++ src/cli/service/agent/simple_chat_session.h | 62 +++++++ 8 files changed, 294 insertions(+), 81 deletions(-) create mode 100644 src/cli/service/agent/simple_chat_session.cc create mode 100644 src/cli/service/agent/simple_chat_session.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 92b97b23..abfe4dd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -262,6 +262,10 @@ include(cmake/imgui.cmake) file(GLOB THEME_FILES "${CMAKE_SOURCE_DIR}/assets/themes/*.theme") file(COPY ${THEME_FILES} DESTINATION "${CMAKE_BINARY_DIR}/assets/themes/") +# Copy agent resource files to build directory (for AI features) +file(GLOB AGENT_FILES "${CMAKE_SOURCE_DIR}/assets/agent/*") +file(COPY ${AGENT_FILES} DESTINATION "${CMAKE_BINARY_DIR}/assets/agent/") + # IMPORTANT: Also ensure themes are included in macOS bundles # This is handled in src/CMakeLists.txt via YAZE_RESOURCE_FILES diff --git a/src/app/gui/widgets/agent_chat_widget.cc b/src/app/gui/widgets/agent_chat_widget.cc index b20f3b4e..e5dd2a1e 100644 --- a/src/app/gui/widgets/agent_chat_widget.cc +++ b/src/app/gui/widgets/agent_chat_widget.cc @@ -166,84 +166,46 @@ void AgentChatWidget::RenderMessageBubble(const cli::agent::ChatMessage& msg, in // Message content ImGui::Indent(20.0f); - // Check if message is JSON (tool result) - if (!is_user && msg.text.find('[') != std::string::npos && - msg.text.find('{') != std::string::npos) { - // Try to render as table - RenderTableFromJson(msg.text); + // Check if we have table data to render + if (!is_user && msg.table_data.has_value()) { + RenderTableData(msg.table_data.value()); + } else if (!is_user && msg.json_pretty.has_value()) { + ImGui::TextWrapped("%s", msg.json_pretty.value().c_str()); } else { // Regular text message - ImGui::TextWrapped("%s", msg.text.c_str()); + ImGui::TextWrapped("%s", msg.message.c_str()); } ImGui::Unindent(20.0f); } -void AgentChatWidget::RenderTableFromJson(const std::string& json_str) { -#ifdef YAZE_WITH_JSON - try { - auto json_data = nlohmann::json::parse(json_str); - - if (!json_data.is_array() || json_data.empty()) { - ImGui::TextWrapped("%s", json_str.c_str()); - return; - } - - // Extract column headers from first object - std::vector headers; - if (json_data[0].is_object()) { - for (auto& [key, value] : json_data[0].items()) { - headers.push_back(key); - } - } - - if (headers.empty()) { - ImGui::TextWrapped("%s", json_str.c_str()); - return; - } - - // Render table - if (ImGui::BeginTable("ToolResultTable", headers.size(), - ImGuiTableFlags_Borders | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_ScrollY)) { - // Headers - for (const auto& header : headers) { - ImGui::TableSetupColumn(header.c_str()); - } - ImGui::TableHeadersRow(); - - // Rows - for (const auto& row : json_data) { - if (!row.is_object()) continue; - - ImGui::TableNextRow(); - for (size_t col = 0; col < headers.size(); ++col) { - ImGui::TableSetColumnIndex(col); - - const auto& header = headers[col]; - if (row.contains(header)) { - std::string cell_value; - if (row[header].is_string()) { - cell_value = row[header].get(); - } else { - cell_value = row[header].dump(); - } - ImGui::TextWrapped("%s", cell_value.c_str()); - } - } - } - - ImGui::EndTable(); - } - - } catch (const nlohmann::json::exception& e) { - // Fallback to plain text if JSON parsing fails - ImGui::TextWrapped("%s", json_str.c_str()); +void AgentChatWidget::RenderTableData(const cli::agent::ChatMessage::TableData& table) { + if (table.headers.empty()) { + return; + } + + // Render table + if (ImGui::BeginTable("ToolResultTable", table.headers.size(), + ImGuiTableFlags_Borders | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_ScrollY)) { + // Headers + for (const auto& header : table.headers) { + ImGui::TableSetupColumn(header.c_str()); + } + ImGui::TableHeadersRow(); + + // Rows + for (const auto& row : table.rows) { + ImGui::TableNextRow(); + for (size_t col = 0; col < std::min(row.size(), table.headers.size()); ++col) { + ImGui::TableSetColumnIndex(col); + ImGui::TextWrapped("%s", row[col].c_str()); + } + } + + ImGui::EndTable(); } -#else - ImGui::TextWrapped("%s", json_str.c_str()); -#endif } void AgentChatWidget::RenderInputArea() { @@ -278,16 +240,10 @@ void AgentChatWidget::SendMessage(const std::string& message) { #ifdef Z3ED_AI_AVAILABLE if (!agent_service_) return; - // Process message through agent service - auto result = agent_service_->ProcessMessage(message); + // Send message through agent service + auto result = agent_service_->SendMessage(message); if (!result.ok()) { - // Add error message to history - cli::agent::ChatMessage error_msg; - error_msg.sender = cli::agent::ChatMessage::Sender::kAgent; - error_msg.text = absl::StrFormat("Error: %s", result.status().message()); - error_msg.timestamp = absl::Now(); - // Note: We'd need to expose AddMessage in the service to do this properly std::cerr << "Error processing message: " << result.status() << std::endl; } @@ -356,7 +312,7 @@ absl::Status AgentChatWidget::SaveHistory(const std::string& filepath) { nlohmann::json msg_json; msg_json["sender"] = (msg.sender == cli::agent::ChatMessage::Sender::kUser) ? "user" : "agent"; - msg_json["text"] = msg.text; + msg_json["message"] = msg.message; msg_json["timestamp"] = absl::FormatTime(msg.timestamp); j["messages"].push_back(msg_json); } diff --git a/src/app/gui/widgets/agent_chat_widget.h b/src/app/gui/widgets/agent_chat_widget.h index 304562e8..52d130b8 100644 --- a/src/app/gui/widgets/agent_chat_widget.h +++ b/src/app/gui/widgets/agent_chat_widget.h @@ -48,7 +48,7 @@ class AgentChatWidget { void RenderInputArea(); void RenderToolbar(); void RenderMessageBubble(const cli::agent::ChatMessage& msg, int index); - void RenderTableFromJson(const std::string& json_str); + void RenderTableData(const cli::agent::ChatMessage::TableData& table); void SendMessage(const std::string& message); void ScrollToBottom(); diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 064e9d8d..43bc4abe 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -12,7 +12,7 @@ namespace agent { namespace { constexpr absl::string_view kUsage = - "Usage: agent " + "Usage: agent " "[options]"; } // namespace @@ -80,6 +80,9 @@ absl::Status Agent::Run(const std::vector& arg_vec) { if (subcommand == "chat") { return agent::HandleChatCommand(rom_); } + if (subcommand == "simple-chat") { + return agent::HandleSimpleChatCommand(subcommand_args, rom_); + } return absl::InvalidArgumentError(std::string(agent::kUsage)); } diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index 58ce86da..5e93b54e 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -41,6 +41,7 @@ absl::Status HandleOverworldListWarpsCommand( const std::vector& arg_vec, Rom* rom_context = nullptr); absl::Status HandleChatCommand(Rom& rom); +absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, Rom& rom); absl::Status HandleTestConversationCommand( const std::vector& arg_vec); diff --git a/src/cli/handlers/agent/general_commands.cc b/src/cli/handlers/agent/general_commands.cc index 17e51269..c2cbe75f 100644 --- a/src/cli/handlers/agent/general_commands.cc +++ b/src/cli/handlers/agent/general_commands.cc @@ -26,6 +26,7 @@ #include "cli/service/ai/gemini_ai_service.h" #include "cli/service/ai/ollama_ai_service.h" #include "cli/service/ai/service_factory.h" +#include "cli/service/agent/simple_chat_session.h" #include "cli/service/planning/proposal_registry.h" #include "cli/service/planning/tile16_proposal_generator.h" #include "cli/service/resources/resource_catalog.h" @@ -571,6 +572,32 @@ absl::Status HandleChatCommand(Rom& rom) { return absl::OkStatus(); } +absl::Status HandleSimpleChatCommand(const std::vector& arg_vec, + Rom& rom) { + RETURN_IF_ERROR(EnsureRomLoaded(rom, "agent simple-chat")); + + // Parse flags + std::optional batch_file; + for (size_t i = 0; i < arg_vec.size(); ++i) { + const std::string& arg = arg_vec[i]; + if (absl::StartsWith(arg, "--file=")) { + batch_file = arg.substr(7); + } else if (arg == "--file" && i + 1 < arg_vec.size()) { + batch_file = arg_vec[i + 1]; + ++i; + } + } + + SimpleChatSession session; + session.SetRomContext(&rom); + + if (batch_file.has_value()) { + return session.RunBatch(*batch_file); + } else { + return session.RunInteractive(); + } +} + absl::Status HandleAcceptCommand(const std::vector& arg_vec, Rom& rom) { std::optional proposal_id; diff --git a/src/cli/service/agent/simple_chat_session.cc b/src/cli/service/agent/simple_chat_session.cc new file mode 100644 index 00000000..f2167f8f --- /dev/null +++ b/src/cli/service/agent/simple_chat_session.cc @@ -0,0 +1,160 @@ +#include "cli/service/agent/simple_chat_session.h" + +#include +#include +#include + +#include "absl/strings/str_format.h" +#include "absl/time/time.h" + +namespace yaze { +namespace cli { +namespace agent { + +SimpleChatSession::SimpleChatSession() = default; + +void SimpleChatSession::SetRomContext(Rom* rom) { + agent_service_.SetRomContext(rom); +} + +void SimpleChatSession::PrintTable(const ChatMessage::TableData& table) { + if (table.headers.empty()) return; + + // Calculate column widths + std::vector col_widths(table.headers.size(), 0); + for (size_t i = 0; i < table.headers.size(); ++i) { + col_widths[i] = table.headers[i].length(); + } + + for (const auto& row : table.rows) { + for (size_t i = 0; i < std::min(row.size(), col_widths.size()); ++i) { + col_widths[i] = std::max(col_widths[i], row[i].length()); + } + } + + // Print header + std::cout << " "; + for (size_t i = 0; i < table.headers.size(); ++i) { + std::cout << std::left << std::setw(col_widths[i] + 2) << table.headers[i]; + } + std::cout << "\n "; + for (size_t i = 0; i < table.headers.size(); ++i) { + std::cout << std::string(col_widths[i] + 2, '-'); + } + std::cout << "\n"; + + // Print rows + for (const auto& row : table.rows) { + std::cout << " "; + for (size_t i = 0; i < std::min(row.size(), table.headers.size()); ++i) { + std::cout << std::left << std::setw(col_widths[i] + 2) << row[i]; + } + std::cout << "\n"; + } +} + +void SimpleChatSession::PrintMessage(const ChatMessage& msg, bool show_timestamp) { + const char* sender = (msg.sender == ChatMessage::Sender::kUser) ? "You" : "Agent"; + + if (show_timestamp) { + std::string timestamp = absl::FormatTime("%H:%M:%S", msg.timestamp, absl::LocalTimeZone()); + std::cout << "[" << timestamp << "] "; + } + + std::cout << sender << ": "; + + if (msg.table_data.has_value()) { + std::cout << "\n"; + PrintTable(msg.table_data.value()); + } else if (msg.json_pretty.has_value()) { + std::cout << "\n" << msg.json_pretty.value() << "\n"; + } else { + std::cout << msg.message << "\n"; + } +} + +absl::Status SimpleChatSession::SendAndWaitForResponse( + const std::string& message, std::string* response_out) { + auto result = agent_service_.SendMessage(message); + if (!result.ok()) { + return result.status(); + } + + const auto& response_msg = result.value(); + + if (response_out != nullptr) { + *response_out = response_msg.message; + } + + return absl::OkStatus(); +} + +absl::Status SimpleChatSession::RunInteractive() { + std::cout << "Z3ED Agent Chat (Simple Mode)\n"; + std::cout << "Type 'quit' or 'exit' to end the session.\n"; + std::cout << "Type 'reset' to clear conversation history.\n"; + std::cout << "----------------------------------------\n\n"; + + std::string input; + while (true) { + std::cout << "You: "; + std::getline(std::cin, input); + + if (input.empty()) continue; + if (input == "quit" || input == "exit") break; + + if (input == "reset") { + Reset(); + std::cout << "Conversation history cleared.\n\n"; + continue; + } + + auto result = agent_service_.SendMessage(input); + if (!result.ok()) { + std::cerr << "Error: " << result.status().message() << "\n\n"; + continue; + } + + PrintMessage(result.value(), false); + std::cout << "\n"; + } + + return absl::OkStatus(); +} + +absl::Status SimpleChatSession::RunBatch(const std::string& input_file) { + std::ifstream file(input_file); + if (!file.is_open()) { + return absl::NotFoundError( + absl::StrFormat("Could not open file: %s", input_file)); + } + + std::cout << "Running batch session from: " << input_file << "\n"; + std::cout << "----------------------------------------\n\n"; + + std::string line; + int line_num = 0; + while (std::getline(file, line)) { + ++line_num; + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') continue; + + std::cout << "Input [" << line_num << "]: " << line << "\n"; + + auto result = agent_service_.SendMessage(line); + if (!result.ok()) { + std::cerr << "Error: " << result.status().message() << "\n\n"; + continue; + } + + PrintMessage(result.value(), false); + std::cout << "\n"; + } + + return absl::OkStatus(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/service/agent/simple_chat_session.h b/src/cli/service/agent/simple_chat_session.h new file mode 100644 index 00000000..c58bf691 --- /dev/null +++ b/src/cli/service/agent/simple_chat_session.h @@ -0,0 +1,62 @@ +#ifndef YAZE_SRC_CLI_SERVICE_AGENT_SIMPLE_CHAT_SESSION_H_ +#define YAZE_SRC_CLI_SERVICE_AGENT_SIMPLE_CHAT_SESSION_H_ + +#include +#include + +#include "absl/status/status.h" +#include "cli/service/agent/conversational_agent_service.h" + +namespace yaze { + +class Rom; + +namespace cli { +namespace agent { + +/** + * @class SimpleChatSession + * @brief Simple text-based chat session for AI agent interaction + * + * Provides a basic REPL-style interface without FTXUI dependencies, + * suitable for automated testing and AI agent interactions. + */ +class SimpleChatSession { + public: + SimpleChatSession(); + + // Set ROM context for tool execution + void SetRomContext(Rom* rom); + + // Send a single message and get response (blocking) + absl::Status SendAndWaitForResponse(const std::string& message, + std::string* response_out = nullptr); + + // Run interactive REPL mode (reads from stdin) + absl::Status RunInteractive(); + + // Run batch mode from file (one message per line) + absl::Status RunBatch(const std::string& input_file); + + // Get full conversation history + const std::vector& GetHistory() const { + return agent_service_.GetHistory(); + } + + // Clear conversation history + void Reset() { + agent_service_.ResetConversation(); + } + + private: + void PrintMessage(const ChatMessage& msg, bool show_timestamp = false); + void PrintTable(const ChatMessage::TableData& table); + + ConversationalAgentService agent_service_; +}; + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_SRC_CLI_SERVICE_AGENT_SIMPLE_CHAT_SESSION_H_