Refactor CLI Service Structure and Enhance AI Integration
- Restructured CLI service source files to improve organization, moving files into dedicated directories for better maintainability. - Introduced new AI service components, including `AIService`, `MockAIService`, and `GeminiAIService`, to facilitate natural language command generation. - Implemented `PolicyEvaluator` and `ProposalRegistry` for enhanced proposal management and policy enforcement in AI workflows. - Updated CMake configurations to reflect new file paths and ensure proper linking of the restructured components. - Enhanced test suite with new test workflow generation capabilities, improving the robustness of automated testing. This commit significantly advances the architecture of the z3ed system, laying the groundwork for more sophisticated AI-driven features and streamlined development processes.
This commit is contained in:
263
src/cli/service/planning/tile16_proposal_generator.cc
Normal file
263
src/cli/service/planning/tile16_proposal_generator.cc
Normal file
@@ -0,0 +1,263 @@
|
||||
#include "cli/service/planning/tile16_proposal_generator.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "app/zelda3/overworld/overworld.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
std::string Tile16Change::ToString() const {
|
||||
std::ostringstream oss;
|
||||
oss << "Map " << map_id << " @ (" << x << "," << y << "): "
|
||||
<< "0x" << std::hex << old_tile << " → 0x" << new_tile;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string Tile16Proposal::ToJson() const {
|
||||
std::ostringstream json;
|
||||
json << "{\n";
|
||||
json << " \"id\": \"" << id << "\",\n";
|
||||
json << " \"prompt\": \"" << prompt << "\",\n";
|
||||
json << " \"ai_service\": \"" << ai_service << "\",\n";
|
||||
json << " \"reasoning\": \"" << reasoning << "\",\n";
|
||||
json << " \"status\": ";
|
||||
|
||||
switch (status) {
|
||||
case Status::PENDING: json << "\"pending\""; break;
|
||||
case Status::ACCEPTED: json << "\"accepted\""; break;
|
||||
case Status::REJECTED: json << "\"rejected\""; break;
|
||||
case Status::APPLIED: json << "\"applied\""; break;
|
||||
}
|
||||
json << ",\n";
|
||||
|
||||
json << " \"changes\": [\n";
|
||||
for (size_t i = 0; i < changes.size(); ++i) {
|
||||
const auto& change = changes[i];
|
||||
json << " {\n";
|
||||
json << " \"map_id\": " << change.map_id << ",\n";
|
||||
json << " \"x\": " << change.x << ",\n";
|
||||
json << " \"y\": " << change.y << ",\n";
|
||||
json << " \"old_tile\": \"0x" << std::hex << change.old_tile << "\",\n";
|
||||
json << " \"new_tile\": \"0x" << std::hex << change.new_tile << "\"\n";
|
||||
json << " }";
|
||||
if (i < changes.size() - 1) json << ",";
|
||||
json << "\n";
|
||||
}
|
||||
json << " ]\n";
|
||||
json << "}\n";
|
||||
|
||||
return json.str();
|
||||
}
|
||||
|
||||
absl::StatusOr<Tile16Proposal> Tile16Proposal::FromJson(const std::string& /* json */) {
|
||||
// TODO: Implement JSON parsing using nlohmann/json when available
|
||||
return absl::UnimplementedError("JSON parsing not yet implemented");
|
||||
}
|
||||
|
||||
std::string Tile16ProposalGenerator::GenerateProposalId() const {
|
||||
// Generate a simple timestamp-based ID
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()).count();
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "proposal_" << ms;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
absl::StatusOr<Tile16Change> Tile16ProposalGenerator::ParseSetTileCommand(
|
||||
const std::string& command,
|
||||
Rom* rom) {
|
||||
|
||||
// Expected format: "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"
|
||||
std::vector<std::string> parts = absl::StrSplit(command, ' ');
|
||||
|
||||
if (parts.size() < 10) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrCat("Invalid command format: ", command));
|
||||
}
|
||||
|
||||
if (parts[0] != "overworld" || parts[1] != "set-tile") {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrCat("Not a set-tile command: ", command));
|
||||
}
|
||||
|
||||
Tile16Change change;
|
||||
|
||||
// Parse arguments
|
||||
for (size_t i = 2; i < parts.size(); i += 2) {
|
||||
if (i + 1 >= parts.size()) break;
|
||||
|
||||
const std::string& flag = parts[i];
|
||||
const std::string& value = parts[i + 1];
|
||||
|
||||
if (flag == "--map") {
|
||||
change.map_id = std::stoi(value);
|
||||
} else if (flag == "--x") {
|
||||
change.x = std::stoi(value);
|
||||
} else if (flag == "--y") {
|
||||
change.y = std::stoi(value);
|
||||
} else if (flag == "--tile") {
|
||||
// Parse as hex (both 0x prefix and plain hex)
|
||||
change.new_tile = static_cast<uint16_t>(std::stoi(value, nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
// Load the ROM to get the old tile value
|
||||
if (rom && rom->is_loaded()) {
|
||||
zelda3::Overworld overworld(rom);
|
||||
auto status = overworld.Load(rom);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Set the correct world based on map_id
|
||||
if (change.map_id < 0x40) {
|
||||
overworld.set_current_world(0); // Light World
|
||||
} else if (change.map_id < 0x80) {
|
||||
overworld.set_current_world(1); // Dark World
|
||||
} else {
|
||||
overworld.set_current_world(2); // Special World
|
||||
}
|
||||
|
||||
change.old_tile = overworld.GetTile(change.x, change.y);
|
||||
} else {
|
||||
change.old_tile = 0x0000; // Unknown
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
absl::StatusOr<Tile16Proposal> Tile16ProposalGenerator::GenerateFromCommands(
|
||||
const std::string& prompt,
|
||||
const std::vector<std::string>& commands,
|
||||
const std::string& ai_service,
|
||||
Rom* rom) {
|
||||
|
||||
Tile16Proposal proposal;
|
||||
proposal.id = GenerateProposalId();
|
||||
proposal.prompt = prompt;
|
||||
proposal.ai_service = ai_service;
|
||||
proposal.created_at = std::chrono::system_clock::now();
|
||||
proposal.status = Tile16Proposal::Status::PENDING;
|
||||
|
||||
// Parse each command
|
||||
for (const auto& command : commands) {
|
||||
// Skip empty commands or comments
|
||||
if (command.empty() || command[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it's a set-tile command
|
||||
if (absl::StrContains(command, "overworld set-tile")) {
|
||||
auto change_or = ParseSetTileCommand(command, rom);
|
||||
if (change_or.ok()) {
|
||||
proposal.changes.push_back(change_or.value());
|
||||
} else {
|
||||
return change_or.status();
|
||||
}
|
||||
}
|
||||
// TODO: Add support for other command types (set-area, replace-tile, etc.)
|
||||
}
|
||||
|
||||
if (proposal.changes.empty()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"No valid tile16 changes found in commands");
|
||||
}
|
||||
|
||||
proposal.reasoning = absl::StrCat(
|
||||
"Generated ", proposal.changes.size(), " tile16 changes from prompt");
|
||||
|
||||
return proposal;
|
||||
}
|
||||
|
||||
absl::Status Tile16ProposalGenerator::ApplyProposal(
|
||||
const Tile16Proposal& proposal,
|
||||
Rom* rom) {
|
||||
|
||||
if (!rom || !rom->is_loaded()) {
|
||||
return absl::FailedPreconditionError("ROM not loaded");
|
||||
}
|
||||
|
||||
zelda3::Overworld overworld(rom);
|
||||
auto status = overworld.Load(rom);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Apply each change
|
||||
for (const auto& change : proposal.changes) {
|
||||
// Set the correct world
|
||||
if (change.map_id < 0x40) {
|
||||
overworld.set_current_world(0); // Light World
|
||||
} else if (change.map_id < 0x80) {
|
||||
overworld.set_current_world(1); // Dark World
|
||||
} else {
|
||||
overworld.set_current_world(2); // Special World
|
||||
}
|
||||
|
||||
// Apply the tile change
|
||||
overworld.SetTile(change.x, change.y, change.new_tile);
|
||||
}
|
||||
|
||||
// Note: We don't save to disk here - that's the caller's responsibility
|
||||
// This allows for sandbox testing before committing
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<gfx::Bitmap> Tile16ProposalGenerator::GenerateDiff(
|
||||
const Tile16Proposal& /* proposal */,
|
||||
Rom* /* before_rom */,
|
||||
Rom* /* after_rom */) {
|
||||
|
||||
// TODO: Implement visual diff generation
|
||||
// This would:
|
||||
// 1. Load overworld from both ROMs
|
||||
// 2. Render the affected regions
|
||||
// 3. Create side-by-side or overlay comparison
|
||||
// 4. Highlight changed tiles
|
||||
|
||||
return absl::UnimplementedError("Visual diff generation not yet implemented");
|
||||
}
|
||||
|
||||
absl::Status Tile16ProposalGenerator::SaveProposal(
|
||||
const Tile16Proposal& proposal,
|
||||
const std::string& path) {
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrCat("Failed to open file for writing: ", path));
|
||||
}
|
||||
|
||||
file << proposal.ToJson();
|
||||
file.close();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<Tile16Proposal> Tile16ProposalGenerator::LoadProposal(
|
||||
const std::string& path) {
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrCat("Failed to open file for reading: ", path));
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
file.close();
|
||||
|
||||
return Tile16Proposal::FromJson(buffer.str());
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user