feat: Add resource search and dungeon room description commands

- Implemented `resource-search` command to allow fuzzy searching of resource labels.
- Added `dungeon-describe-room` command to summarize metadata for a specified dungeon room.
- Enhanced `agent` command handler to support new commands and updated usage documentation.
- Introduced read-only accessors for room metadata in the Room class.
- Updated AI service to recognize and handle new commands for resource searching and room description.
- Improved metrics tracking for user interactions, including command execution and response times.
- Enhanced TUI to display command metrics and session summaries.
This commit is contained in:
scawful
2025-10-04 12:00:51 -04:00
parent acada1bec5
commit 4b61b213c0
15 changed files with 844 additions and 68 deletions

View File

@@ -14,7 +14,9 @@
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "app/rom.h"
#include "cli/service/agent/proposal_executor.h"
#include "cli/service/ai/service_factory.h"
@@ -132,6 +134,20 @@ std::optional<ChatMessage::TableData> BuildTableData(const nlohmann::json& data)
return std::nullopt;
}
bool IsExecutableCommand(absl::string_view command) {
return !command.empty() && command.front() != '#';
}
int CountExecutableCommands(const std::vector<std::string>& commands) {
int count = 0;
for (const auto& command : commands) {
if (IsExecutableCommand(command)) {
++count;
}
}
return count;
}
ChatMessage CreateMessage(ChatMessage::Sender sender, const std::string& content) {
ChatMessage message;
message.sender = sender;
@@ -175,6 +191,38 @@ void ConversationalAgentService::SetRomContext(Rom* rom) {
void ConversationalAgentService::ResetConversation() {
history_.clear();
metrics_ = InternalMetrics{};
}
void ConversationalAgentService::TrimHistoryIfNeeded() {
if (!config_.trim_history || config_.max_history_messages == 0) {
return;
}
while (history_.size() > config_.max_history_messages) {
history_.erase(history_.begin());
}
}
ChatMessage::SessionMetrics ConversationalAgentService::BuildMetricsSnapshot() const {
ChatMessage::SessionMetrics snapshot;
snapshot.turn_index = metrics_.turns_completed;
snapshot.total_user_messages = metrics_.user_messages;
snapshot.total_agent_messages = metrics_.agent_messages;
snapshot.total_tool_calls = metrics_.tool_calls;
snapshot.total_commands = metrics_.commands_generated;
snapshot.total_proposals = metrics_.proposals_created;
snapshot.total_elapsed_seconds = absl::ToDoubleSeconds(metrics_.total_latency);
snapshot.average_latency_seconds =
metrics_.turns_completed > 0
? snapshot.total_elapsed_seconds /
static_cast<double>(metrics_.turns_completed)
: 0.0;
return snapshot;
}
ChatMessage::SessionMetrics ConversationalAgentService::GetMetrics() const {
return BuildMetricsSnapshot();
}
absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
@@ -186,10 +234,13 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
if (!message.empty()) {
history_.push_back(CreateMessage(ChatMessage::Sender::kUser, message));
TrimHistoryIfNeeded();
++metrics_.user_messages;
}
const int max_iterations = config_.max_tool_iterations;
bool waiting_for_text_response = false;
absl::Time turn_start = absl::Now();
if (config_.verbose) {
util::PrintInfo(absl::StrCat("Starting agent loop (max ", max_iterations, " iterations)"));
@@ -269,6 +320,7 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
const std::string& tool_output = tool_result_or.value();
if (!tool_output.empty()) {
util::PrintSuccess("Tool executed successfully");
++metrics_.tool_calls;
if (config_.verbose) {
std::cout << util::colors::kDim << "Tool output (truncated):"
@@ -358,6 +410,8 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
response_text.append("Reasoning: ");
response_text.append(agent_response.reasoning);
}
const int executable_commands =
CountExecutableCommands(agent_response.commands);
if (!agent_response.commands.empty()) {
if (!response_text.empty()) {
response_text.append("\n\n");
@@ -365,6 +419,7 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
response_text.append("Commands:\n");
response_text.append(absl::StrJoin(agent_response.commands, "\n"));
}
metrics_.commands_generated += executable_commands;
if (proposal_result.has_value()) {
const auto& metadata = proposal_result->metadata;
@@ -381,6 +436,7 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
proposal_result->executed_commands == 1 ? "" : "s",
metadata.id, metadata.sandbox_rom_path.string(),
proposal_result->proposal_json_path.string()));
++metrics_.proposals_created;
} else if (attempted_proposal && !proposal_status.ok()) {
if (!response_text.empty()) {
response_text.append("\n\n");
@@ -392,7 +448,12 @@ absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
ChatMessage chat_response =
CreateMessage(ChatMessage::Sender::kAgent, response_text);
++metrics_.agent_messages;
++metrics_.turns_completed;
metrics_.total_latency += absl::Now() - turn_start;
chat_response.metrics = BuildMetricsSnapshot();
history_.push_back(chat_response);
TrimHistoryIfNeeded();
return chat_response;
}

View File

@@ -6,6 +6,7 @@
#include <vector>
#include "absl/status/statusor.h"
#include "absl/time/time.h"
#include "cli/service/ai/ai_service.h"
#include "cli/service/agent/tool_dispatcher.h"
@@ -27,6 +28,17 @@ struct ChatMessage {
absl::Time timestamp;
std::optional<std::string> json_pretty;
std::optional<TableData> table_data;
struct SessionMetrics {
int turn_index = 0;
int total_user_messages = 0;
int total_agent_messages = 0;
int total_tool_calls = 0;
int total_commands = 0;
int total_proposals = 0;
double total_elapsed_seconds = 0.0;
double average_latency_seconds = 0.0;
};
std::optional<SessionMetrics> metrics;
};
struct AgentConfig {
@@ -34,6 +46,8 @@ struct AgentConfig {
int max_retry_attempts = 3; // Maximum retries on errors
bool verbose = false; // Enable verbose diagnostic output
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
};
class ConversationalAgentService {
@@ -57,12 +71,28 @@ class ConversationalAgentService {
void SetConfig(const AgentConfig& config) { config_ = config; }
const AgentConfig& GetConfig() const { return config_; }
ChatMessage::SessionMetrics GetMetrics() const;
private:
struct InternalMetrics {
int user_messages = 0;
int agent_messages = 0;
int tool_calls = 0;
int commands_generated = 0;
int proposals_created = 0;
int turns_completed = 0;
absl::Duration total_latency = absl::ZeroDuration();
};
void TrimHistoryIfNeeded();
ChatMessage::SessionMetrics BuildMetricsSnapshot() const;
std::vector<ChatMessage> history_;
std::unique_ptr<AIService> ai_service_;
ToolDispatcher tool_dispatcher_;
Rom* rom_context_ = nullptr;
AgentConfig config_;
InternalMetrics metrics_;
};
} // namespace agent

View File

@@ -80,6 +80,20 @@ void SimpleChatSession::PrintMessage(const ChatMessage& msg, bool show_timestamp
} 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";
}
}
absl::Status SimpleChatSession::SendAndWaitForResponse(
@@ -142,6 +156,18 @@ absl::Status SimpleChatSession::RunInteractive() {
PrintMessage(result.value(), false);
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);
return absl::OkStatus();
}
@@ -175,6 +201,18 @@ absl::Status SimpleChatSession::RunBatch(const std::string& input_file) {
PrintMessage(result.value(), false);
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);
return absl::OkStatus();
}

View File

@@ -36,8 +36,12 @@ absl::StatusOr<std::string> ToolDispatcher::Dispatch(
absl::Status status;
if (tool_call.tool_name == "resource-list") {
status = HandleResourceListCommand(args, rom_context_);
} else if (tool_call.tool_name == "resource-search") {
status = HandleResourceSearchCommand(args, rom_context_);
} else if (tool_call.tool_name == "dungeon-list-sprites") {
status = HandleDungeonListSpritesCommand(args, rom_context_);
} else if (tool_call.tool_name == "dungeon-describe-room") {
status = HandleDungeonDescribeRoomCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-find-tile") {
status = HandleOverworldFindTileCommand(args, rom_context_);
} else if (tool_call.tool_name == "overworld-describe-map") {