feat: Implement message handling commands for agent tool

- Added functionality for listing, reading, and searching messages in the ROM.
- Introduced new commands: `message-list`, `message-read`, and `message-search` with appropriate parameters and descriptions.
- Enhanced the CLI to support these commands, including JSON and text output formats.
- Updated system prompts and function schemas to reflect the new message handling capabilities.
This commit is contained in:
scawful
2025-10-04 20:53:13 -04:00
parent b3fee1b62e
commit f38946118c
11 changed files with 830 additions and 75 deletions

View File

@@ -46,6 +46,15 @@ absl::Status HandleOverworldDescribeMapCommand(
absl::Status HandleOverworldListWarpsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleChatCommand(Rom& rom);
absl::Status HandleSimpleChatCommand(const std::vector<std::string>&, Rom* rom, bool quiet);
absl::Status HandleTestConversationCommand(

View File

@@ -22,6 +22,7 @@
#include "app/rom.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/overworld/overworld.h"
#include "cli/handlers/message.h"
#include "cli/handlers/overworld_inspect.h"
#include "cli/service/resources/resource_context_builder.h"
#include "util/macro.h"
@@ -1179,6 +1180,21 @@ absl::Status HandleOverworldListWarpsCommand(
return absl::OkStatus();
}
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageListCommand(arg_vec, rom_context);
}
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageReadCommand(arg_vec, rom_context);
}
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
return yaze::cli::message::HandleMessageSearchCommand(arg_vec, rom_context);
}
} // namespace agent
} // namespace cli
} // namespace yaze

416
src/cli/handlers/message.cc Normal file
View File

@@ -0,0 +1,416 @@
#include "cli/handlers/message.h"
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include "absl/flags/declare.h"
#include "absl/flags/flag.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_format.h"
#include "app/editor/message/message_data.h"
#include "app/rom.h"
#include "util/macro.h"
ABSL_DECLARE_FLAG(std::string, rom);
namespace yaze {
namespace cli {
namespace message {
namespace {
absl::StatusOr<Rom> LoadRomFromFlag() {
std::string rom_path = absl::GetFlag(FLAGS_rom);
if (rom_path.empty()) {
return absl::FailedPreconditionError(
"No ROM loaded. Use --rom=<path> to specify ROM file.");
}
Rom rom;
auto status = rom.LoadFromFile(rom_path);
if (!status.ok()) {
return absl::FailedPreconditionError(absl::StrFormat(
"Failed to load ROM from '%s': %s", rom_path, status.message()));
}
return rom;
}
std::vector<editor::MessageData> LoadMessages(Rom* rom) {
return editor::ReadAllTextData(rom->data(), editor::kTextData);
}
} // namespace
absl::Status HandleMessageListCommand(const std::vector<std::string>& arg_vec,
Rom* rom_context) {
std::string format = "json";
int start_id = 0;
int end_id = -1; // -1 means all
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = absl::AsciiStrToLower(arg_vec[++i]);
} else if (absl::StartsWith(token, "--format=")) {
format = absl::AsciiStrToLower(token.substr(9));
} else if (token == "--range") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--range requires a value (start-end).");
}
std::string range = arg_vec[++i];
size_t dash_pos = range.find('-');
if (dash_pos == std::string::npos) {
return absl::InvalidArgumentError("--range format must be start-end (e.g. 0-100)");
}
if (!absl::SimpleAtoi(range.substr(0, dash_pos), &start_id) ||
!absl::SimpleAtoi(range.substr(dash_pos + 1), &end_id)) {
return absl::InvalidArgumentError("Invalid range format");
}
} else if (absl::StartsWith(token, "--range=")) {
std::string range = token.substr(8);
size_t dash_pos = range.find('-');
if (dash_pos == std::string::npos) {
return absl::InvalidArgumentError("--range format must be start-end (e.g. 0-100)");
}
if (!absl::SimpleAtoi(range.substr(0, dash_pos), &start_id) ||
!absl::SimpleAtoi(range.substr(dash_pos + 1), &end_id)) {
return absl::InvalidArgumentError("Invalid range format");
}
}
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
auto messages = LoadMessages(rom);
if (end_id < 0) {
end_id = static_cast<int>(messages.size()) - 1;
}
start_id = std::max(0, std::min(start_id, static_cast<int>(messages.size()) - 1));
end_id = std::max(start_id, std::min(end_id, static_cast<int>(messages.size()) - 1));
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"total_messages\": %zu,\n", messages.size());
std::cout << absl::StrFormat(" \"range\": [%d, %d],\n", start_id, end_id);
std::cout << " \"messages\": [\n";
bool first = true;
for (int i = start_id; i <= end_id; ++i) {
const auto& msg = messages[i];
if (!first) std::cout << ",\n";
std::cout << " {\n";
std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID);
std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address);
// Escape quotes in the text
std::string escaped_text = msg.ContentsParsed;
size_t pos = 0;
while ((pos = escaped_text.find('"', pos)) != std::string::npos) {
escaped_text.insert(pos, "\\");
pos += 2;
}
std::cout << absl::StrFormat(" \"text\": \"%s\"\n", escaped_text);
std::cout << " }";
first = false;
}
std::cout << "\n ]\n";
std::cout << "}\n";
} else {
std::cout << absl::StrFormat("📝 Messages %d-%d (Total: %zu)\n",
start_id, end_id, messages.size());
std::cout << std::string(60, '=') << "\n";
for (int i = start_id; i <= end_id; ++i) {
const auto& msg = messages[i];
std::cout << absl::StrFormat("[%03d] @ 0x%06X\n", msg.ID, msg.Address);
std::cout << " " << msg.ContentsParsed << "\n";
std::cout << std::string(60, '-') << "\n";
}
}
return absl::OkStatus();
}
absl::Status HandleMessageReadCommand(const std::vector<std::string>& arg_vec,
Rom* rom_context) {
int message_id = -1;
std::string format = "json";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--id") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--id requires a value.");
}
if (!absl::SimpleAtoi(arg_vec[++i], &message_id)) {
return absl::InvalidArgumentError("Invalid message ID format.");
}
} else if (absl::StartsWith(token, "--id=")) {
if (!absl::SimpleAtoi(token.substr(5), &message_id)) {
return absl::InvalidArgumentError("Invalid message ID format.");
}
} else if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = absl::AsciiStrToLower(arg_vec[++i]);
} else if (absl::StartsWith(token, "--format=")) {
format = absl::AsciiStrToLower(token.substr(9));
}
}
if (message_id < 0) {
return absl::InvalidArgumentError(
"Usage: message-read --id <message_id> [--format <json|text>]");
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
auto messages = LoadMessages(rom);
if (message_id >= static_cast<int>(messages.size())) {
return absl::NotFoundError(
absl::StrFormat("Message ID %d not found (max: %d)",
message_id, messages.size() - 1));
}
const auto& msg = messages[message_id];
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID);
std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address);
// Escape quotes
std::string escaped_text = msg.ContentsParsed;
size_t pos = 0;
while ((pos = escaped_text.find('"', pos)) != std::string::npos) {
escaped_text.insert(pos, "\\");
pos += 2;
}
std::cout << absl::StrFormat(" \"text\": \"%s\",\n", escaped_text);
std::cout << absl::StrFormat(" \"length\": %zu\n", msg.Data.size());
std::cout << "}\n";
} else {
std::cout << absl::StrFormat("📝 Message #%d\n", msg.ID);
std::cout << absl::StrFormat("Address: 0x%06X\n", msg.Address);
std::cout << absl::StrFormat("Length: %zu bytes\n", msg.Data.size());
std::cout << std::string(60, '-') << "\n";
std::cout << msg.ContentsParsed << "\n";
}
return absl::OkStatus();
}
absl::Status HandleMessageSearchCommand(const std::vector<std::string>& arg_vec,
Rom* rom_context) {
std::string query;
std::string format = "json";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--query") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--query requires a value.");
}
query = arg_vec[++i];
} else if (absl::StartsWith(token, "--query=")) {
query = token.substr(8);
} else if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = absl::AsciiStrToLower(arg_vec[++i]);
} else if (absl::StartsWith(token, "--format=")) {
format = absl::AsciiStrToLower(token.substr(9));
}
}
if (query.empty()) {
return absl::InvalidArgumentError(
"Usage: message-search --query <text> [--format <json|text>]");
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
auto messages = LoadMessages(rom);
std::string lowered_query = absl::AsciiStrToLower(query);
std::vector<int> matches;
for (const auto& msg : messages) {
std::string lowered_text = absl::AsciiStrToLower(msg.ContentsParsed);
if (lowered_text.find(lowered_query) != std::string::npos) {
matches.push_back(msg.ID);
}
}
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"query\": \"%s\",\n", query);
std::cout << absl::StrFormat(" \"match_count\": %zu,\n", matches.size());
std::cout << " \"matches\": [\n";
for (size_t i = 0; i < matches.size(); ++i) {
const auto& msg = messages[matches[i]];
if (i > 0) std::cout << ",\n";
std::string escaped_text = msg.ContentsParsed;
size_t pos = 0;
while ((pos = escaped_text.find('"', pos)) != std::string::npos) {
escaped_text.insert(pos, "\\");
pos += 2;
}
std::cout << " {\n";
std::cout << absl::StrFormat(" \"id\": %d,\n", msg.ID);
std::cout << absl::StrFormat(" \"address\": \"0x%06X\",\n", msg.Address);
std::cout << absl::StrFormat(" \"text\": \"%s\"\n", escaped_text);
std::cout << " }";
}
std::cout << "\n ]\n";
std::cout << "}\n";
} else {
std::cout << absl::StrFormat("🔍 Search: \"%s\" → %zu match(es)\n",
query, matches.size());
std::cout << std::string(60, '=') << "\n";
for (int match_id : matches) {
const auto& msg = messages[match_id];
std::cout << absl::StrFormat("[%03d] @ 0x%06X\n", msg.ID, msg.Address);
std::cout << " " << msg.ContentsParsed << "\n";
std::cout << std::string(60, '-') << "\n";
}
}
return absl::OkStatus();
}
absl::Status HandleMessageStatsCommand(const std::vector<std::string>& arg_vec,
Rom* rom_context) {
std::string format = "json";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = absl::AsciiStrToLower(arg_vec[++i]);
} else if (absl::StartsWith(token, "--format=")) {
format = absl::AsciiStrToLower(token.substr(9));
}
}
if (format != "json" && format != "text") {
return absl::InvalidArgumentError("--format must be either json or text");
}
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
auto messages = LoadMessages(rom);
size_t total_bytes = 0;
size_t max_length = 0;
size_t min_length = SIZE_MAX;
for (const auto& msg : messages) {
size_t len = msg.Data.size();
total_bytes += len;
max_length = std::max(max_length, len);
min_length = std::min(min_length, len);
}
double avg_length = messages.empty() ? 0.0 :
static_cast<double>(total_bytes) / messages.size();
if (format == "json") {
std::cout << "{\n";
std::cout << absl::StrFormat(" \"total_messages\": %zu,\n", messages.size());
std::cout << absl::StrFormat(" \"total_bytes\": %zu,\n", total_bytes);
std::cout << absl::StrFormat(" \"average_length\": %.2f,\n", avg_length);
std::cout << absl::StrFormat(" \"min_length\": %zu,\n", min_length);
std::cout << absl::StrFormat(" \"max_length\": %zu\n", max_length);
std::cout << "}\n";
} else {
std::cout << "📊 Message Statistics\n";
std::cout << std::string(40, '=') << "\n";
std::cout << absl::StrFormat("Total Messages: %zu\n", messages.size());
std::cout << absl::StrFormat("Total Bytes: %zu\n", total_bytes);
std::cout << absl::StrFormat("Average Length: %.2f bytes\n", avg_length);
std::cout << absl::StrFormat("Min Length: %zu bytes\n", min_length);
std::cout << absl::StrFormat("Max Length: %zu bytes\n", max_length);
}
return absl::OkStatus();
}
} // namespace message
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,57 @@
#ifndef YAZE_CLI_HANDLERS_MESSAGE_H_
#define YAZE_CLI_HANDLERS_MESSAGE_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
class Rom;
namespace cli {
namespace message {
// Message inspection handlers for agent tool calls
/**
* @brief List all messages in the ROM
* @param arg_vec Command arguments: [--format <json|text>] [--range <start-end>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageListCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Read a specific message by ID
* @param arg_vec Command arguments: --id <message_id> [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageReadCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Search for messages containing specific text
* @param arg_vec Command arguments: --query <text> [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageSearchCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
/**
* @brief Get message statistics and overview
* @param arg_vec Command arguments: [--format <json|text>]
* @param rom_context Optional ROM context to avoid reloading
*/
absl::Status HandleMessageStatsCommand(
const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
} // namespace message
} // namespace cli
} // namespace yaze
#endif // YAZE_CLI_HANDLERS_MESSAGE_H_