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

@@ -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