feat: Enhance chat command with multiple output formats and improve help documentation

- Updated the chat command to support new output formats: text, markdown, json, and compact.
- Modified the agent configuration to include output format settings.
- Enhanced the command line interface to handle new format options and provide detailed usage instructions.
- Improved the message printing logic in SimpleChatSession to format output based on the selected format.
- Added JSON and Markdown formatting for session metrics and messages.
- Updated help documentation to reflect changes in command usage and available options.
This commit is contained in:
scawful
2025-10-04 13:33:19 -04:00
parent 0db71a71fe
commit 6990e565b8
8 changed files with 736 additions and 75 deletions

View File

@@ -30,6 +30,8 @@ struct ParsedGlobals {
std::vector<char*> positional;
bool show_help = false;
bool show_version = false;
bool list_commands = false;
std::optional<std::string> help_category;
std::optional<std::string> error;
};
@@ -54,8 +56,22 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) {
continue;
}
if (absl::StartsWith(token, "--help=")) {
std::string category(token.substr(7));
if (!category.empty()) {
result.help_category = category;
} else {
result.show_help = true;
}
continue;
}
if (token == "--help" || token == "-h") {
result.show_help = true;
if (i + 1 < argc && argv[i + 1][0] != '-') {
result.help_category = std::string(argv[++i]);
} else {
result.show_help = true;
}
continue;
}
@@ -69,6 +85,23 @@ ParsedGlobals ParseGlobalFlags(int argc, char* argv[]) {
continue;
}
if (token == "--list-commands" || token == "--list") {
result.list_commands = true;
continue;
}
if (absl::StartsWith(token, "--quiet=")) {
std::string value(token.substr(8));
bool enable = value.empty() || value == "1" || value == "true";
absl::SetFlag(&FLAGS_quiet, enable);
continue;
}
if (token == "--quiet" || token == "-q") {
absl::SetFlag(&FLAGS_quiet, true);
continue;
}
if (absl::StartsWith(token, "--rom=")) {
absl::SetFlag(&FLAGS_rom, std::string(token.substr(6)));
continue;
@@ -200,6 +233,16 @@ int main(int argc, char* argv[]) {
yaze::cli::ModernCLI cli;
if (globals.help_category.has_value()) {
cli.PrintCategoryHelp(*globals.help_category);
return EXIT_SUCCESS;
}
if (globals.list_commands) {
cli.PrintCommandSummary();
return EXIT_SUCCESS;
}
if (globals.show_help) {
cli.PrintTopLevelHelp();
return EXIT_SUCCESS;

View File

@@ -73,7 +73,7 @@ constexpr absl::string_view kUsage =
" --ai_model=<name> Model name (e.g., qwen2.5-coder:7b for Ollama)\n"
" --ollama_host=<url> Ollama server URL (default: http://localhost:11434)\n"
" --gemini_api_key=<key> Gemini API key (or set GEMINI_API_KEY env var)\n"
" --format=<type> Output format: json | table | yaml\n"
" --format=<type> Output format: text | markdown | json | compact\n"
"\n"
"For more details, see: docs/simple_chat_input_methods.md";

View File

@@ -12,13 +12,14 @@
#include "absl/flags/declare.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/status/status.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/str_join.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/strings/string_view.h"
#include "app/core/project.h"
#include "app/zelda3/dungeon/room.h"
@@ -533,6 +534,7 @@ absl::Status HandleSimpleChatCommand(const std::vector<std::string>& arg_vec,
std::optional<std::string> batch_file;
std::optional<std::string> single_message;
bool verbose = false;
std::optional<std::string> format_option;
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& arg = arg_vec[i];
@@ -540,6 +542,16 @@ absl::Status HandleSimpleChatCommand(const std::vector<std::string>& arg_vec,
batch_file = arg.substr(7);
} else if (arg == "--file" && i + 1 < arg_vec.size()) {
batch_file = arg_vec[++i];
} else if (absl::StartsWith(arg, "--format=")) {
format_option = arg.substr(9);
} else if (arg == "--format" && i + 1 < arg_vec.size()) {
format_option = arg_vec[++i];
} else if (arg == "--json") {
format_option = "json";
} else if (arg == "--markdown" || arg == "--md") {
format_option = "markdown";
} else if (arg == "--compact" || arg == "--raw") {
format_option = "compact";
} else if (arg == "--verbose" || arg == "-v") {
verbose = true;
} else if (!absl::StartsWith(arg, "--") && !single_message.has_value()) {
@@ -549,6 +561,23 @@ absl::Status HandleSimpleChatCommand(const std::vector<std::string>& arg_vec,
agent::AgentConfig config;
config.verbose = verbose;
if (format_option.has_value()) {
std::string normalized = absl::AsciiStrToLower(*format_option);
if (normalized == "json") {
config.output_format = AgentOutputFormat::kJson;
} else if (normalized == "markdown" || normalized == "md") {
config.output_format = AgentOutputFormat::kMarkdown;
} else if (normalized == "compact" || normalized == "raw") {
config.output_format = AgentOutputFormat::kCompact;
} else if (normalized == "text" || normalized == "friendly" ||
normalized == "pretty") {
config.output_format = AgentOutputFormat::kFriendly;
} else {
return absl::InvalidArgumentError(
absl::StrCat("Unsupported chat format: ", *format_option,
". Supported formats: text, markdown, json, compact"));
}
}
SimpleChatSession session;
session.SetConfig(config);

View File

@@ -1,9 +1,12 @@
#include "cli/modern_cli.h"
#include <iostream>
#include <optional>
#include "absl/flags/flag.h"
#include "absl/flags/declare.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
@@ -13,6 +16,7 @@
#include "cli/z3ed_ascii_logo.h"
ABSL_DECLARE_FLAG(std::string, rom);
ABSL_DECLARE_FLAG(bool, quiet);
namespace yaze {
namespace cli {
@@ -114,6 +118,48 @@ void ModernCLI::SetupCommands() {
}
};
commands_["chat"] = {
.name = "chat",
.description =
"Unified chat entrypoint with text, markdown, or JSON output",
.usage =
"z3ed chat [--mode=simple|gui|test] [--format=text|markdown|json|compact]"
" [--prompt \"<message>\"] [--file=<questions.txt>] [--quiet]",
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
return HandleChatEntryCommand(args);
}
};
commands_["proposal"] = {
.name = "proposal",
.description = "Review and manage AI-generated change proposals",
.usage =
"z3ed proposal <run|list|diff|show|accept|commit|revert> [options]",
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
return HandleProposalCommand(args);
}
};
commands_["widget"] = {
.name = "widget",
.description = "Discover GUI widgets exposed through automation APIs",
.usage = "z3ed widget discover [--window=<name>] [--type=<widget>]",
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
return HandleWidgetCommand(args);
}
};
commands_["widget discover"] = {
.name = "widget discover",
.description = "Inspect UI widgets using the automation service",
.usage =
"z3ed widget discover [--window=<name>] [--type=<widget>]"
" [--format=text|json]",
.handler = [this](const std::vector<std::string>& args) -> absl::Status {
return HandleWidgetCommand(args);
}
};
commands_["project build"] = {
.name = "project build",
.description = "Build the project and create a new ROM file",
@@ -305,11 +351,25 @@ void ModernCLI::ShowHelp() {
std::cout << "\033[1m\033[36mCOMMANDS:\033[0m" << std::endl;
std::cout << std::endl;
std::cout << " \033[1m🤖 AI Agent\033[0m" << std::endl;
std::cout << " agent simple-chat Natural language ROM queries" << std::endl;
std::cout << " agent test-conversation Interactive testing mode" << std::endl;
std::cout << " \033[90m→ z3ed help agent\033[0m" << std::endl;
std::cout << " \033[1m🤖 AI Agent\033[0m" << std::endl;
std::cout << " chat Unified chat entrypoint (text/json/markdown)" << std::endl;
std::cout << " agent simple-chat Natural language ROM queries" << std::endl;
std::cout << " agent test-conversation Interactive testing mode" << std::endl;
std::cout << " \033[90m→ z3ed help chat, z3ed help agent\033[0m" << std::endl;
std::cout << std::endl;
std::cout << " \033[1m🧠 Proposals\033[0m" << std::endl;
std::cout << " proposal run Execute AI-driven sandbox plan" << std::endl;
std::cout << " proposal list Show pending proposals" << std::endl;
std::cout << " proposal diff Review latest sandbox diff" << std::endl;
std::cout << " proposal accept Apply sandbox changes" << std::endl;
std::cout << " \033[90m→ z3ed help proposal\033[0m" << std::endl;
std::cout << std::endl;
std::cout << " \033[1m🪟 GUI Automation\033[0m" << std::endl;
std::cout << " widget discover Inspect GUI widgets via automation" << std::endl;
std::cout << " \033[90m→ z3ed help widget\033[0m" << std::endl;
std::cout << std::endl;
std::cout << " \033[1m🔧 ROM Patching\033[0m" << std::endl;
std::cout << " patch apply-asar Apply Asar 65816 assembly patch" << std::endl;
@@ -350,7 +410,7 @@ void ModernCLI::ShowHelp() {
std::cout << "\033[1m\033[36mQUICK START:\033[0m" << std::endl;
std::cout << " z3ed --tui" << std::endl;
std::cout << " z3ed agent simple-chat \"What is room 5?\" --rom=zelda3.sfc" << std::endl;
std::cout << " z3ed chat \"What is room 5?\" --rom=zelda3.sfc --format=markdown" << std::endl;
std::cout << " z3ed patch apply-asar patch.asm --rom=zelda3.sfc" << std::endl;
std::cout << std::endl;
@@ -361,6 +421,14 @@ void ModernCLI::PrintTopLevelHelp() const {
const_cast<ModernCLI*>(this)->ShowHelp();
}
void ModernCLI::PrintCategoryHelp(const std::string& category) const {
const_cast<ModernCLI*>(this)->ShowCategoryHelp(category);
}
void ModernCLI::PrintCommandSummary() const {
const_cast<ModernCLI*>(this)->ShowCommandSummary();
}
void ModernCLI::ShowCategoryHelp(const std::string& category) {
std::cout << GetColoredLogo() << std::endl;
std::cout << std::endl;
@@ -395,6 +463,54 @@ void ModernCLI::ShowCategoryHelp(const std::string& category) {
std::cout << " • Use --ai_provider=gemini or --ai_provider=ollama" << std::endl;
std::cout << std::endl;
} else if (category == "chat") {
std::cout << "\033[1m\033[36m💬 CHAT ENTRYPOINT\033[0m" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mDESCRIPTION:\033[0m" << std::endl;
std::cout << " Launch the embedded agent in text, markdown, or JSON-friendly modes." << std::endl;
std::cout << std::endl;
std::cout << "\033[1mMODES:\033[0m" << std::endl;
std::cout << " chat --mode=simple Quick REPL with --format=text|markdown|json|compact" << std::endl;
std::cout << " chat --mode=batch --file=F Run prompts from file (one per line)" << std::endl;
std::cout << " chat --mode=gui Launch the full FTXUI conversation experience" << std::endl;
std::cout << " chat --mode=test Execute scripted agent conversation for QA" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mOPTIONS:\033[0m" << std::endl;
std::cout << " --format=text|markdown|json|compact Control response formatting" << std::endl;
std::cout << " --prompt \"<msg>\" Send a single message and exit" << std::endl;
std::cout << " --file questions.txt Batch mode input" << std::endl;
std::cout << " --quiet Suppress extra banners" << std::endl;
std::cout << std::endl;
} else if (category == "proposal") {
std::cout << "\033[1m\033[36m🧠 PROPOSAL WORKFLOWS\033[0m" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mCOMMANDS:\033[0m" << std::endl;
std::cout << " proposal run --prompt \"<desc>\" Plan and execute changes in sandbox" << std::endl;
std::cout << " proposal list Show pending proposals" << std::endl;
std::cout << " proposal diff [--proposal-id=X] Inspect latest diff/log" << std::endl;
std::cout << " proposal accept --proposal-id=X Apply sandbox changes to main ROM" << std::endl;
std::cout << " proposal commit | proposal revert Persist or undo sandbox changes" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mTIPS:\033[0m" << std::endl;
std::cout << " • Run `z3ed proposal list` frequently to monitor progress" << std::endl;
std::cout << " • Use `--prompt` to describe tasks in natural language" << std::endl;
std::cout << " • Sandbox artifacts live alongside proposal logs" << std::endl;
std::cout << std::endl;
} else if (category == "widget") {
std::cout << "\033[1m\033[36m🪟 GUI WIDGET DISCOVERY\033[0m" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mCOMMANDS:\033[0m" << std::endl;
std::cout << " widget discover [--window=<name>] [--type=<widget>]" << std::endl;
std::cout << " Enumerate UI widgets available through automation hooks" << std::endl;
std::cout << " Options: --format=table|json, --limit <n>, --include-invisible, --include-disabled" << std::endl;
std::cout << std::endl;
std::cout << "\033[1mTIPS:\033[0m" << std::endl;
std::cout << " • Requires the YAZE GUI to be running locally" << std::endl;
std::cout << " • Combine with `z3ed proposal run` for automated UI tests" << std::endl;
std::cout << std::endl;
} else if (category == "patch") {
std::cout << "\033[1m\033[36m🔧 ROM PATCHING COMMANDS\033[0m" << std::endl;
std::cout << std::endl;
@@ -520,12 +636,44 @@ void ModernCLI::ShowCategoryHelp(const std::string& category) {
} else {
std::cout << "\033[1m\033[31mUnknown category: " << category << "\033[0m" << std::endl;
std::cout << std::endl;
std::cout << "Available categories: agent, patch, rom, overworld, dungeon, gfx, palette" << std::endl;
std::cout << "Available categories: agent, chat, proposal, widget, patch, rom, overworld, dungeon, gfx, palette" << std::endl;
std::cout << std::endl;
std::cout << "Use 'z3ed --help' to see all commands." << std::endl;
}
}
void ModernCLI::ShowCommandSummary() const {
std::cout << GetColoredLogo() << std::endl;
std::cout << std::endl;
std::cout << "\033[1m\033[36mCOMMANDS OVERVIEW\033[0m" << std::endl;
std::cout << std::endl;
for (const auto& [key, info] : commands_) {
std::string headline = info.description;
const size_t newline_pos = headline.find('\n');
if (newline_pos != std::string::npos) {
headline = headline.substr(0, newline_pos);
}
if (headline.empty()) {
headline = info.usage;
}
if (headline.size() > 80) {
headline = absl::StrCat(headline.substr(0, 77), "");
}
std::string label = info.name;
if (label.size() > 24) {
label = absl::StrCat(label.substr(0, 23), "");
}
std::cout << " "
<< absl::StrFormat("%-24s%s", label, headline) << std::endl;
}
std::cout << std::endl;
std::cout << "Use \033[90mz3ed help <topic>\033[0m for detailed information." << std::endl;
}
absl::Status ModernCLI::Run(int argc, char* argv[]) {
if (argc < 2) {
ShowHelp();
@@ -737,8 +885,143 @@ absl::Status ModernCLI::HandleOverworldSetTileCommand(const std::vector<std::str
}
absl::Status ModernCLI::HandleSpriteCreateCommand(const std::vector<std::string>& args) {
SpriteCreate handler;
return handler.Run(args);
SpriteCreate handler;
return handler.Run(args);
}
absl::Status ModernCLI::HandleChatEntryCommand(
const std::vector<std::string>& args) {
std::string mode = "simple";
std::optional<std::string> prompt;
std::vector<std::string> forwarded;
for (size_t i = 0; i < args.size(); ++i) {
const std::string& token = args[i];
if (absl::StartsWith(token, "--mode=")) {
mode = absl::AsciiStrToLower(token.substr(7));
continue;
}
if (token == "--mode" && i + 1 < args.size()) {
mode = absl::AsciiStrToLower(args[i + 1]);
++i;
continue;
}
if (absl::StartsWith(token, "--prompt=")) {
prompt = token.substr(9);
continue;
}
if (token == "--prompt" && i + 1 < args.size()) {
prompt = args[i + 1];
++i;
continue;
}
if (token == "--quiet" || token == "-q") {
absl::SetFlag(&FLAGS_quiet, true);
continue;
}
if (!absl::StartsWith(token, "--") && !prompt.has_value()) {
prompt = token;
continue;
}
forwarded.push_back(token);
}
const std::string normalized_mode = absl::AsciiStrToLower(mode);
auto has_batch_file = [&forwarded]() {
for (const auto& token : forwarded) {
if (absl::StartsWith(token, "--file") || token == "--file") {
return true;
}
}
return false;
};
std::vector<std::string> agent_args;
if (normalized_mode == "gui" || normalized_mode == "visual" ||
normalized_mode == "tui") {
if (prompt.has_value()) {
return absl::InvalidArgumentError(
"GUI chat mode launches the interactive TUI and does not accept a --prompt value.");
}
agent_args.push_back("chat");
} else if (normalized_mode == "test" || normalized_mode == "qa") {
if (prompt.has_value()) {
return absl::InvalidArgumentError(
"Test conversation mode does not accept an inline prompt.");
}
agent_args.push_back("test-conversation");
} else {
if (normalized_mode == "batch" && !has_batch_file()) {
return absl::InvalidArgumentError(
"Batch chat mode requires a --file=<path> option.");
}
agent_args.push_back("simple-chat");
if (prompt.has_value()) {
agent_args.push_back(*prompt);
}
}
agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end());
return HandleAgentCommand(agent_args);
}
absl::Status ModernCLI::HandleProposalCommand(
const std::vector<std::string>& args) {
if (args.empty()) {
ShowCategoryHelp("proposal");
return absl::OkStatus();
}
std::string subcommand = absl::AsciiStrToLower(args[0]);
std::vector<std::string> forwarded(args.begin() + 1, args.end());
std::vector<std::string> agent_args;
if (subcommand == "run" || subcommand == "plan") {
agent_args.push_back(subcommand);
} else if (subcommand == "list") {
agent_args.push_back("list");
} else if (subcommand == "diff" || subcommand == "show") {
agent_args.push_back("diff");
} else if (subcommand == "accept") {
agent_args.push_back("accept");
} else if (subcommand == "commit") {
agent_args.push_back("commit");
} else if (subcommand == "revert" || subcommand == "reject") {
agent_args.push_back("revert");
} else if (subcommand == "test") {
agent_args.push_back("test");
} else {
return absl::InvalidArgumentError(
absl::StrCat("Unknown proposal command: ", subcommand,
". Valid actions: run, plan, list, diff, show, accept, commit, revert."));
}
agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end());
return HandleAgentCommand(agent_args);
}
absl::Status ModernCLI::HandleWidgetCommand(
const std::vector<std::string>& args) {
if (args.empty()) {
ShowCategoryHelp("widget");
return absl::OkStatus();
}
std::vector<std::string> forwarded(args.begin(), args.end());
std::string subcommand = absl::AsciiStrToLower(forwarded[0]);
std::vector<std::string> agent_args;
if (subcommand == "discover") {
agent_args.push_back("gui");
agent_args.insert(agent_args.end(), forwarded.begin(), forwarded.end());
} else {
return absl::InvalidArgumentError(
absl::StrCat("Unknown widget command: ", forwarded[0],
". Try 'z3ed widget discover'."));
}
return HandleAgentCommand(agent_args);
}
} // namespace cli

View File

@@ -25,6 +25,8 @@ class ModernCLI {
absl::Status Run(int argc, char* argv[]);
CommandHandler* GetCommandHandler(const std::string& name);
void PrintTopLevelHelp() const;
void PrintCategoryHelp(const std::string& category) const;
void PrintCommandSummary() const;
std::map<std::string, CommandInfo> commands_;
@@ -32,6 +34,7 @@ class ModernCLI {
void SetupCommands();
void ShowHelp();
void ShowCategoryHelp(const std::string& category);
void ShowCommandSummary() const;
// Command Handlers
absl::Status HandleAsarPatchCommand(const std::vector<std::string>& args);
@@ -58,6 +61,9 @@ class ModernCLI {
absl::Status HandleOverworldListWarpsCommand(const std::vector<std::string>& args);
absl::Status HandleOverworldSetTileCommand(const std::vector<std::string>& args);
absl::Status HandleSpriteCreateCommand(const std::vector<std::string>& args);
absl::Status HandleChatEntryCommand(const std::vector<std::string>& args);
absl::Status HandleProposalCommand(const std::vector<std::string>& args);
absl::Status HandleWidgetCommand(const std::vector<std::string>& args);
};
} // namespace cli

View File

@@ -41,6 +41,13 @@ struct ChatMessage {
std::optional<SessionMetrics> metrics;
};
enum class AgentOutputFormat {
kFriendly,
kCompact,
kMarkdown,
kJson
};
struct AgentConfig {
int max_tool_iterations = 4; // Maximum number of tool calling iterations
int max_retry_attempts = 3; // Maximum retries on errors
@@ -48,6 +55,7 @@ struct AgentConfig {
bool show_reasoning = true; // Show LLM reasoning in output
size_t max_history_messages = 50; // Maximum stored history messages per session
bool trim_history = true; // Whether to trim history beyond the limit
AgentOutputFormat output_format = AgentOutputFormat::kFriendly;
};
class ConversationalAgentService {

View File

@@ -1,8 +1,10 @@
#include "cli/service/agent/simple_chat_session.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <iostream>
#include <sstream>
#ifdef _WIN32
#include <io.h>
@@ -12,7 +14,9 @@
#include <unistd.h>
#endif
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/time/time.h"
#include "cli/util/terminal_colors.h"
@@ -26,6 +30,170 @@ void SimpleChatSession::SetRomContext(Rom* rom) {
agent_service_.SetRomContext(rom);
}
namespace {
std::string EscapeJsonString(absl::string_view input) {
std::string output;
output.reserve(input.size());
for (char ch : input) {
switch (ch) {
case '\\':
output.append("\\\\");
break;
case '"':
output.append("\\\"");
break;
case '\b':
output.append("\\b");
break;
case '\f':
output.append("\\f");
break;
case '\n':
output.append("\\n");
break;
case '\r':
output.append("\\r");
break;
case '\t':
output.append("\\t");
break;
default: {
unsigned char code = static_cast<unsigned char>(ch);
if (code < 0x20) {
absl::StrAppendFormat(&output, "\\u%04x",
static_cast<unsigned int>(code));
} else {
output.push_back(ch);
}
break;
}
}
}
return output;
}
std::string QuoteJson(absl::string_view value) {
return absl::StrCat("\"", EscapeJsonString(value), "\"");
}
std::string TableToJson(const ChatMessage::TableData& table) {
std::vector<std::string> header_entries;
header_entries.reserve(table.headers.size());
for (const auto& header : table.headers) {
header_entries.push_back(QuoteJson(header));
}
std::vector<std::string> row_entries;
row_entries.reserve(table.rows.size());
for (const auto& row : table.rows) {
std::vector<std::string> cell_entries;
cell_entries.reserve(row.size());
for (const auto& cell : row) {
cell_entries.push_back(QuoteJson(cell));
}
row_entries.push_back(
absl::StrCat("[", absl::StrJoin(cell_entries, ","), "]"));
}
return absl::StrCat("{\"headers\":[", absl::StrJoin(header_entries, ","),
"],\"rows\":[", absl::StrJoin(row_entries, ","),
"]}");
}
std::string MetricsToJson(const ChatMessage::SessionMetrics& metrics) {
return absl::StrCat(
"{\"turn_index\":", metrics.turn_index, ","
"\"total_user_messages\":", metrics.total_user_messages, ","
"\"total_agent_messages\":", metrics.total_agent_messages, ","
"\"total_tool_calls\":", metrics.total_tool_calls, ","
"\"total_commands\":", metrics.total_commands, ","
"\"total_proposals\":", metrics.total_proposals, ","
"\"total_elapsed_seconds\":", metrics.total_elapsed_seconds, ","
"\"average_latency_seconds\":", metrics.average_latency_seconds, "}");
}
std::string MessageToJson(const ChatMessage& msg, bool show_timestamp) {
std::string json = "{";
absl::StrAppend(&json, "\"sender\":\"",
msg.sender == ChatMessage::Sender::kUser ? "user"
: "agent",
"\"");
absl::StrAppend(&json, ",\"message\":", QuoteJson(msg.message));
if (msg.json_pretty.has_value()) {
absl::StrAppend(&json, ",\"structured\":",
QuoteJson(msg.json_pretty.value()));
}
if (msg.table_data.has_value()) {
absl::StrAppend(&json, ",\"table\":", TableToJson(*msg.table_data));
}
if (msg.metrics.has_value()) {
absl::StrAppend(&json, ",\"metrics\":",
MetricsToJson(*msg.metrics));
}
if (show_timestamp) {
std::string timestamp =
absl::FormatTime("%Y-%m-%dT%H:%M:%S%z", msg.timestamp,
absl::LocalTimeZone());
absl::StrAppend(&json, ",\"timestamp\":", QuoteJson(timestamp));
}
absl::StrAppend(&json, "}");
return json;
}
void PrintMarkdownTable(const ChatMessage::TableData& table) {
if (table.headers.empty()) {
return;
}
std::cout << "\n|";
for (const auto& header : table.headers) {
std::cout << " " << header << " |";
}
std::cout << "\n|";
for (size_t i = 0; i < table.headers.size(); ++i) {
std::cout << " --- |";
}
std::cout << "\n";
for (const auto& row : table.rows) {
std::cout << "|";
for (size_t i = 0; i < table.headers.size(); ++i) {
if (i < row.size()) {
std::cout << " " << row[i];
}
std::cout << " |";
}
std::cout << "\n";
}
}
void PrintMarkdownMetrics(const ChatMessage::SessionMetrics& metrics) {
std::cout << "\n> _Turn " << metrics.turn_index
<< ": users=" << metrics.total_user_messages
<< ", agents=" << metrics.total_agent_messages
<< ", tool-calls=" << metrics.total_tool_calls
<< ", commands=" << metrics.total_commands
<< ", proposals=" << metrics.total_proposals
<< ", elapsed="
<< absl::StrFormat("%.2fs avg %.2fs",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)
<< "_\n";
}
std::string SessionMetricsToJson(const ChatMessage::SessionMetrics& metrics) {
return MetricsToJson(metrics);
}
} // namespace
void SimpleChatSession::PrintTable(const ChatMessage::TableData& table) {
if (table.headers.empty()) return;
@@ -62,37 +230,77 @@ void SimpleChatSession::PrintTable(const ChatMessage::TableData& table) {
}
}
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";
}
void SimpleChatSession::PrintMessage(const ChatMessage& msg,
bool show_timestamp) {
switch (config_.output_format) {
case AgentOutputFormat::kFriendly: {
const char* sender =
(msg.sender == ChatMessage::Sender::kUser) ? "You" : "Agent";
if (msg.metrics.has_value()) {
const auto& metrics = msg.metrics.value();
std::cout << " 📊 Turn " << metrics.turn_index
<< " summary — users: " << metrics.total_user_messages
<< ", agents: " << metrics.total_agent_messages
<< ", tools: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs", metrics.total_elapsed_seconds,
metrics.average_latency_seconds)
<< "\n";
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";
}
if (msg.metrics.has_value()) {
const auto& metrics = msg.metrics.value();
std::cout << " 📊 Turn " << metrics.turn_index
<< " summary — users: " << metrics.total_user_messages
<< ", agents: " << metrics.total_agent_messages
<< ", tools: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)
<< "\n";
}
break;
}
case AgentOutputFormat::kCompact: {
if (msg.json_pretty.has_value()) {
std::cout << msg.json_pretty.value() << "\n";
} else if (msg.table_data.has_value()) {
PrintTable(msg.table_data.value());
} else {
std::cout << msg.message << "\n";
}
break;
}
case AgentOutputFormat::kMarkdown: {
std::cout << (msg.sender == ChatMessage::Sender::kUser ? "**You:** "
: "**Agent:** ");
if (msg.table_data.has_value()) {
PrintMarkdownTable(msg.table_data.value());
} else if (msg.json_pretty.has_value()) {
std::cout << "\n```json\n" << msg.json_pretty.value()
<< "\n```\n";
} else {
std::cout << msg.message << "\n";
}
if (msg.metrics.has_value()) {
PrintMarkdownMetrics(*msg.metrics);
}
break;
}
case AgentOutputFormat::kJson: {
std::cout << MessageToJson(msg, show_timestamp) << std::endl;
break;
}
}
}
@@ -116,7 +324,7 @@ absl::Status SimpleChatSession::RunInteractive() {
// Check if stdin is a TTY (interactive) or a pipe/file
bool is_interactive = isatty(fileno(stdin));
if (is_interactive) {
if (is_interactive && config_.output_format == AgentOutputFormat::kFriendly) {
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";
@@ -125,14 +333,14 @@ absl::Status SimpleChatSession::RunInteractive() {
std::string input;
while (true) {
if (is_interactive) {
if (is_interactive && config_.output_format != AgentOutputFormat::kJson) {
std::cout << "You: ";
std::cout.flush(); // Ensure prompt is displayed before reading
}
if (!std::getline(std::cin, input)) {
// EOF reached (piped input exhausted or Ctrl+D)
if (is_interactive) {
if (is_interactive && config_.output_format != AgentOutputFormat::kJson) {
std::cout << "\n";
}
break;
@@ -143,31 +351,68 @@ absl::Status SimpleChatSession::RunInteractive() {
if (input == "reset") {
Reset();
std::cout << "Conversation history cleared.\n\n";
if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << "{\"event\":\"history_cleared\"}" << std::endl;
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "> Conversation history cleared.\n\n";
} else {
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";
if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << absl::StrCat(
"{\"event\":\"error\",\"message\":",
QuoteJson(result.status().message()), "}")
<< std::endl;
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "> **Error:** " << result.status().message() << "\n\n";
} else if (config_.output_format == AgentOutputFormat::kCompact) {
std::cout << "error: " << result.status().message() << "\n";
} else {
std::cerr << "Error: " << result.status().message() << "\n\n";
}
continue;
}
PrintMessage(result.value(), false);
std::cout << "\n";
if (config_.output_format != AgentOutputFormat::kJson) {
std::cout << "\n";
}
}
const auto metrics = agent_service_.GetMetrics();
std::cout << "Session totals — turns: " << metrics.turn_index
<< ", user messages: " << metrics.total_user_messages
<< ", agent messages: " << metrics.total_agent_messages
<< ", tool calls: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs\n\n",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds);
if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << absl::StrCat("{\"event\":\"session_summary\",\"metrics\":",
SessionMetricsToJson(metrics), "}")
<< std::endl;
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "\n> **Session totals** ";
std::cout << "turns=" << metrics.turn_index << ", users="
<< metrics.total_user_messages << ", agents="
<< metrics.total_agent_messages << ", tools="
<< metrics.total_tool_calls << ", commands="
<< metrics.total_commands << ", proposals="
<< metrics.total_proposals << ", elapsed="
<< absl::StrFormat("%.2fs avg %.2fs",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)
<< "\n\n";
} else {
std::cout << "Session totals — turns: " << metrics.turn_index
<< ", user messages: " << metrics.total_user_messages
<< ", agent messages: " << metrics.total_agent_messages
<< ", tool calls: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs\n\n",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds);
}
return absl::OkStatus();
}
@@ -179,8 +424,12 @@ absl::Status SimpleChatSession::RunBatch(const std::string& input_file) {
absl::StrFormat("Could not open file: %s", input_file));
}
std::cout << "Running batch session from: " << input_file << "\n";
std::cout << "----------------------------------------\n\n";
if (config_.output_format == AgentOutputFormat::kFriendly) {
std::cout << "Running batch session from: " << input_file << "\n";
std::cout << "----------------------------------------\n\n";
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "### Batch session: " << input_file << "\n\n";
}
std::string line;
int line_num = 0;
@@ -190,29 +439,70 @@ absl::Status SimpleChatSession::RunBatch(const std::string& input_file) {
// Skip empty lines and comments
if (line.empty() || line[0] == '#') continue;
std::cout << "Input [" << line_num << "]: " << line << "\n";
if (config_.output_format == AgentOutputFormat::kFriendly) {
std::cout << "Input [" << line_num << "]: " << line << "\n";
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "- **Input " << line_num << "**: " << line << "\n";
} else if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << absl::StrCat(
"{\"event\":\"batch_input\",\"index\":",
line_num, ",\"prompt\":", QuoteJson(line), "}")
<< std::endl;
}
auto result = agent_service_.SendMessage(line);
if (!result.ok()) {
std::cerr << "Error: " << result.status().message() << "\n\n";
if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << absl::StrCat(
"{\"event\":\"error\",\"index\":", line_num,
",\"message\":",
QuoteJson(result.status().message()), "}")
<< std::endl;
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << " - ⚠️ " << result.status().message() << "\n";
} else if (config_.output_format == AgentOutputFormat::kCompact) {
std::cout << "error@" << line_num << ": "
<< result.status().message() << "\n";
} else {
std::cerr << "Error: " << result.status().message() << "\n\n";
}
continue;
}
PrintMessage(result.value(), false);
std::cout << "\n";
if (config_.output_format != AgentOutputFormat::kJson) {
std::cout << "\n";
}
}
const auto metrics = agent_service_.GetMetrics();
std::cout << "Batch session totals — turns: " << metrics.turn_index
<< ", user messages: " << metrics.total_user_messages
<< ", agent messages: " << metrics.total_agent_messages
<< ", tool calls: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs\n\n",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds);
if (config_.output_format == AgentOutputFormat::kJson) {
std::cout << absl::StrCat("{\"event\":\"session_summary\",\"metrics\":",
SessionMetricsToJson(metrics), "}")
<< std::endl;
} else if (config_.output_format == AgentOutputFormat::kMarkdown) {
std::cout << "\n> **Batch totals** turns=" << metrics.turn_index
<< ", users=" << metrics.total_user_messages << ", agents="
<< metrics.total_agent_messages << ", tools="
<< metrics.total_tool_calls << ", commands="
<< metrics.total_commands << ", proposals="
<< metrics.total_proposals << ", elapsed="
<< absl::StrFormat("%.2fs avg %.2fs",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds)
<< "\n\n";
} else {
std::cout << "Batch session totals — turns: " << metrics.turn_index
<< ", user messages: " << metrics.total_user_messages
<< ", agent messages: " << metrics.total_agent_messages
<< ", tool calls: " << metrics.total_tool_calls
<< ", commands: " << metrics.total_commands
<< ", proposals: " << metrics.total_proposals
<< ", elapsed: "
<< absl::StrFormat("%.2fs avg %.2fs\n\n",
metrics.total_elapsed_seconds,
metrics.average_latency_seconds);
}
return absl::OkStatus();
}

View File

@@ -36,6 +36,7 @@ class SimpleChatSession {
// Set agent configuration
void SetConfig(const AgentConfig& config) {
config_ = config;
agent_service_.SetConfig(config);
}
@@ -65,6 +66,7 @@ class SimpleChatSession {
void PrintTable(const ChatMessage::TableData& table);
ConversationalAgentService agent_service_;
AgentConfig config_;
};
} // namespace agent