Add YAML support and enhance AI service context handling

- Integrated yaml-cpp library into the project for YAML file parsing.
- Updated ConversationalAgentService to set ROM context in AI services.
- Extended AIService interface with SetRomContext method for context injection.
- Implemented SetRomContext in GeminiAIService and OllamaAIService.
- Enhanced PromptBuilder to load resource catalogues from YAML files.
- Added functions to parse commands, tools, examples, and tile references from YAML.
- Improved error handling for loading YAML files and added search paths for catalogues.
- Updated CMake configuration to fetch yaml-cpp if not found.
- Modified vcpkg.json to include yaml-cpp as a dependency.
This commit is contained in:
scawful
2025-10-03 16:44:29 -04:00
parent 467b0926e5
commit 42c64db904
12 changed files with 746 additions and 233 deletions

View File

@@ -0,0 +1,192 @@
commands:
palette export: |-
Export palette data to JSON file
--group <group> Palette group (overworld, dungeon, sprite)
--id <id> Palette ID (0-based index)
--to <file> Output JSON file path
palette import: |-
Import palette data from JSON file
--group <group> Palette group (overworld, dungeon, sprite)
--id <id> Palette ID (0-based index)
--from <file> Input JSON file path
palette set-color: |-
Modify a color in palette JSON file
--file <file> Palette JSON file to modify
--index <index> Color index (0-15 per palette)
--color <hex> New color in hex (0xRRGGBB format)
overworld set-tile: |-
Place a tile in the overworld
--map <id> Map ID (0-based)
--x <x> X coordinate (0-63)
--y <y> Y coordinate (0-63)
--tile <hex> Tile ID in hex (e.g., 0x02E for tree)
sprite set-position: |-
Move a sprite to a new position
--id <id> Sprite ID
--x <x> X coordinate
--y <y> Y coordinate
dungeon set-room-tile: |-
Place a tile in a dungeon room
--room <id> Room ID
--x <x> X coordinate
--y <y> Y coordinate
--tile <hex> Tile ID
rom validate: "Validate ROM integrity and structure"
tools:
- name: resource-list
description: "List project-defined resource labels for the requested category."
usage_notes: "Use this whenever you need to reference project-specific labels or IDs from the ROM."
arguments:
- name: type
description: "Resource category (dungeon, sprite, overworld, entrance, room, etc.)."
required: true
example: dungeon
- name: format
description: "Response format (json or table). Defaults to JSON if omitted."
required: false
example: json
- name: dungeon-list-sprites
description: "Inspect sprite placements for a specific dungeon room."
usage_notes: "Returns sprite IDs, positions, and metadata for the requested room."
arguments:
- name: room
description: "Room label or numeric ID (supports hex like 0x123)."
required: true
example: hyrule_castle_throne
- name: dungeon
description: "Optional dungeon ID when room names are ambiguous."
required: false
example: 0x00
- name: format
description: "Response format (json or table). Defaults to JSON if omitted."
required: false
example: json
- name: overworld-find-tile
description: "Search all overworld maps for occurrences of a specific tile16 ID."
usage_notes: "Ideal for tile lookup questions. Includes coordinates for each match."
arguments:
- name: tile
description: "Tile16 ID to search for (accepts hex or decimal)."
required: true
example: 0x02E
- name: map
description: "Optional map ID filter (0=Light World, 1=Dark World, etc.)."
required: false
example: 0
- name: format
description: "Response format (json or table). Defaults to JSON if omitted."
required: false
example: json
- name: overworld-describe-map
description: "Summarize metadata for an overworld map, including regions and labels."
usage_notes: "Use this before proposing edits to understand map properties and labels."
arguments:
- name: map
description: "Map ID or label to describe."
required: true
example: 0
- name: format
description: "Response format (json or table). Defaults to JSON if omitted."
required: false
example: json
- name: overworld-list-warps
description: "List warp entrances and exits for overworld maps, including destinations."
usage_notes: "Helpful for navigation questions and verifying warp destinations."
arguments:
- name: map
description: "Optional map filter. Defaults to all maps when omitted."
required: false
example: 1
- name: format
description: "Response format (json or table). Defaults to JSON if omitted."
required: false
example: json
tile16_reference:
grass: 0x020
dirt: 0x022
tree: 0x02E
bush: 0x003
rock: 0x004
flower: 0x021
sand: 0x023
water_top: 0x14C
water_middle: 0x14D
water_bottom: 0x14E
deep_water: 0x14F
shallow_water: 0x150
water_single: 0x038
house_00: 0x0C0
house_01: 0x0C1
house_10: 0x0D0
house_11: 0x0D1
examples:
- user_prompt: "Place a tree at position 10, 20 on the Light World map"
text_response: "Okay, I can place that tree for you. Here is the command:"
reasoning: "Single tile16 placement. Tree tile ID is 0x02E in vanilla ALTTP"
commands:
- "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"
- user_prompt: "Create a 3x3 water pond at coordinates 15, 10"
text_response: "Creating a 3x3 pond requires nine `set-tile` commands. Here they are:"
reasoning: "Water areas use different edge tiles: 0x14C (top), 0x14D (middle), 0x14E (bottom)"
commands:
- "overworld set-tile --map 0 --x 15 --y 10 --tile 0x14C"
- "overworld set-tile --map 0 --x 16 --y 10 --tile 0x14D"
- "overworld set-tile --map 0 --x 17 --y 10 --tile 0x14C"
- "overworld set-tile --map 0 --x 15 --y 11 --tile 0x14D"
- "overworld set-tile --map 0 --x 16 --y 11 --tile 0x14D"
- "overworld set-tile --map 0 --x 17 --y 11 --tile 0x14D"
- "overworld set-tile --map 0 --x 15 --y 12 --tile 0x14E"
- "overworld set-tile --map 0 --x 16 --y 12 --tile 0x14E"
- "overworld set-tile --map 0 --x 17 --y 12 --tile 0x14E"
- user_prompt: "Add a dirt path from position 5,5 to 5,15"
text_response: "I will generate a `set-tile` command for each point along the path."
reasoning: "Linear paths are created by placing tiles sequentially. Dirt tile is 0x022"
commands:
- "overworld set-tile --map 0 --x 5 --y 5 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 6 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 7 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 8 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 9 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 10 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 11 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 12 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 13 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 14 --tile 0x022"
- "overworld set-tile --map 0 --x 5 --y 15 --tile 0x022"
- user_prompt: "Plant a row of trees horizontally at y=8 from x=20 to x=25"
text_response: "Here are the commands to plant that row of trees:"
reasoning: "Tree rows create natural barriers and visual boundaries"
commands:
- "overworld set-tile --map 0 --x 20 --y 8 --tile 0x02E"
- "overworld set-tile --map 0 --x 21 --y 8 --tile 0x02E"
- "overworld set-tile --map 0 --x 22 --y 8 --tile 0x02E"
- "overworld set-tile --map 0 --x 23 --y 8 --tile 0x02E"
- "overworld set-tile --map 0 --x 24 --y 8 --tile 0x02E"
- "overworld set-tile --map 0 --x 25 --y 8 --tile 0x02E"
- user_prompt: "Add 3 soldiers to the Eastern Palace entrance room"
text_response: "I've identified the dungeon and sprite IDs from your project's labels. Here are the commands:"
reasoning: "Dungeon ID 0x02 is Eastern Palace. Sprite 0x41 is soldier. Spread placement for balance"
commands:
- "dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 5 --y 3"
- "dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 10 --y 3"
- "dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 7 --y 8"
- user_prompt: "Place a chest in the Hyrule Castle treasure room"
text_response: "Certainly. I will place a chest containing a small key in the center of the room."
reasoning: "Dungeon 0x00 is Hyrule Castle. Item 0x12 is a small key. Position centered in room"
commands:
- "dungeon add-chest --dungeon 0x00 --room 0x60 --x 7 --y 5 --item 0x12 --big false"
- user_prompt: "Check if my overworld changes are valid"
text_response: "Yes, I can validate the ROM for you."
reasoning: "Validation ensures ROM integrity after tile modifications"
commands:
- "rom validate"
- user_prompt: "What dungeons are in this project?"
text_response: "I can list the dungeons for you. Let me check the resource labels."
reasoning: "The user is asking a question. I need to use the `resource-list` tool to find the answer."
tool_calls:
- tool_name: resource-list
args:
type: dungeon

View File

@@ -24,6 +24,7 @@ target_link_libraries(yaze_agent
PUBLIC PUBLIC
yaze_common yaze_common
${ABSL_TARGETS} ${ABSL_TARGETS}
yaml-cpp
) )
target_include_directories(yaze_agent target_include_directories(yaze_agent

View File

@@ -152,6 +152,9 @@ ConversationalAgentService::ConversationalAgentService() {
void ConversationalAgentService::SetRomContext(Rom* rom) { void ConversationalAgentService::SetRomContext(Rom* rom) {
rom_context_ = rom; rom_context_ = rom;
tool_dispatcher_.SetRomContext(rom_context_); tool_dispatcher_.SetRomContext(rom_context_);
if (ai_service_) {
ai_service_->SetRomContext(rom_context_);
}
} }
absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage( absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(

View File

@@ -9,6 +9,8 @@
#include "cli/service/ai/common.h" #include "cli/service/ai/common.h"
namespace yaze { namespace yaze {
class Rom;
namespace cli { namespace cli {
namespace agent { namespace agent {
struct ChatMessage; struct ChatMessage;
@@ -18,6 +20,10 @@ class AIService {
public: public:
virtual ~AIService() = default; virtual ~AIService() = default;
// Provide the AI service with the active ROM so prompts can include
// project-specific context.
virtual void SetRomContext(Rom* rom) { (void)rom; }
// Generate a response from a single prompt. // Generate a response from a single prompt.
virtual absl::StatusOr<AgentResponse> GenerateResponse( virtual absl::StatusOr<AgentResponse> GenerateResponse(
const std::string& prompt) = 0; const std::string& prompt) = 0;
@@ -30,6 +36,7 @@ class AIService {
// Mock implementation for testing // Mock implementation for testing
class MockAIService : public AIService { class MockAIService : public AIService {
public: public:
void SetRomContext(Rom* rom) override { (void)rom; }
absl::StatusOr<AgentResponse> GenerateResponse( absl::StatusOr<AgentResponse> GenerateResponse(
const std::string& prompt) override; const std::string& prompt) override;
absl::StatusOr<AgentResponse> GenerateResponse( absl::StatusOr<AgentResponse> GenerateResponse(

View File

@@ -21,7 +21,10 @@ namespace cli {
GeminiAIService::GeminiAIService(const GeminiConfig& config) GeminiAIService::GeminiAIService(const GeminiConfig& config)
: config_(config) { : config_(config) {
// Load command documentation into prompt builder // Load command documentation into prompt builder
prompt_builder_.LoadResourceCatalogue(""); // TODO: Pass actual yaml path when available if (auto status = prompt_builder_.LoadResourceCatalogue(""); !status.ok()) {
std::cerr << "⚠️ Failed to load agent prompt catalogue: "
<< status.message() << std::endl;
}
if (config_.system_instruction.empty()) { if (config_.system_instruction.empty()) {
// Use enhanced prompting by default // Use enhanced prompting by default
@@ -39,6 +42,10 @@ std::string GeminiAIService::BuildSystemInstruction() {
return prompt_builder_.BuildSystemInstruction(); return prompt_builder_.BuildSystemInstruction();
} }
void GeminiAIService::SetRomContext(Rom* rom) {
prompt_builder_.SetRom(rom);
}
absl::Status GeminiAIService::CheckAvailability() { absl::Status GeminiAIService::CheckAvailability() {
#ifndef YAZE_WITH_JSON #ifndef YAZE_WITH_JSON
return absl::UnimplementedError( return absl::UnimplementedError(

View File

@@ -27,6 +27,7 @@ struct GeminiConfig {
class GeminiAIService : public AIService { class GeminiAIService : public AIService {
public: public:
explicit GeminiAIService(const GeminiConfig& config); explicit GeminiAIService(const GeminiConfig& config);
void SetRomContext(Rom* rom) override;
// Primary interface // Primary interface
absl::StatusOr<AgentResponse> GenerateResponse( absl::StatusOr<AgentResponse> GenerateResponse(

View File

@@ -1,6 +1,7 @@
#include "cli/service/ai/ollama_ai_service.h" #include "cli/service/ai/ollama_ai_service.h"
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include "absl/strings/str_cat.h" #include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h" #include "absl/strings/str_format.h"
@@ -33,7 +34,10 @@ namespace cli {
OllamaAIService::OllamaAIService(const OllamaConfig& config) : config_(config) { OllamaAIService::OllamaAIService(const OllamaConfig& config) : config_(config) {
// Load command documentation into prompt builder // Load command documentation into prompt builder
prompt_builder_.LoadResourceCatalogue(""); // TODO: Pass actual yaml path when available if (auto status = prompt_builder_.LoadResourceCatalogue(""); !status.ok()) {
std::cerr << "⚠️ Failed to load agent prompt catalogue: "
<< status.message() << std::endl;
}
if (config_.system_prompt.empty()) { if (config_.system_prompt.empty()) {
// Use enhanced prompting by default // Use enhanced prompting by default
@@ -51,6 +55,10 @@ std::string OllamaAIService::BuildSystemPrompt() {
return prompt_builder_.BuildSystemInstruction(); return prompt_builder_.BuildSystemInstruction();
} }
void OllamaAIService::SetRomContext(Rom* rom) {
prompt_builder_.SetRom(rom);
}
absl::Status OllamaAIService::CheckAvailability() { absl::Status OllamaAIService::CheckAvailability() {
#if !YAZE_HAS_HTTPLIB || !YAZE_HAS_JSON #if !YAZE_HAS_HTTPLIB || !YAZE_HAS_JSON
return absl::UnimplementedError( return absl::UnimplementedError(

View File

@@ -26,6 +26,8 @@ class OllamaAIService : public AIService {
public: public:
explicit OllamaAIService(const OllamaConfig& config); explicit OllamaAIService(const OllamaConfig& config);
void SetRomContext(Rom* rom) override;
// Generate z3ed commands from natural language prompt // Generate z3ed commands from natural language prompt
absl::StatusOr<AgentResponse> GenerateResponse( absl::StatusOr<AgentResponse> GenerateResponse(
const std::string& prompt) override; const std::string& prompt) override;

View File

@@ -1,190 +1,365 @@
#include "cli/service/ai/prompt_builder.h" #include "cli/service/ai/prompt_builder.h"
#include "cli/service/agent/conversational_agent_service.h" #include "cli/service/agent/conversational_agent_service.h"
#include <cstdlib>
#include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include <sstream> #include <sstream>
#include "absl/strings/str_cat.h" #include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h" #include "absl/strings/str_join.h"
#include "nlohmann/json.hpp"
#include "yaml-cpp/yaml.h"
namespace yaze { namespace yaze {
namespace cli { namespace cli {
PromptBuilder::PromptBuilder() { namespace {
LoadDefaultExamples();
namespace fs = std::filesystem;
nlohmann::json YamlToJson(const YAML::Node& node) {
if (!node) {
return nlohmann::json();
} }
void PromptBuilder::LoadDefaultExamples() { switch (node.Type()) {
// ========================================================================== case YAML::NodeType::Scalar:
// OVERWORLD TILE16 EDITING - Primary Focus return node.as<std::string>("");
// ========================================================================== case YAML::NodeType::Sequence: {
nlohmann::json array = nlohmann::json::array();
for (const auto& item : node) {
array.push_back(YamlToJson(item));
}
return array;
}
case YAML::NodeType::Map: {
nlohmann::json object = nlohmann::json::object();
for (const auto& kv : node) {
object[kv.first.as<std::string>()] = YamlToJson(kv.second);
}
return object;
}
default:
return nlohmann::json();
}
}
// Single tile placement std::vector<fs::path> BuildCatalogueSearchPaths(const std::string& explicit_path) {
examples_.push_back({ std::vector<fs::path> paths;
"Place a tree at position 10, 20 on the Light World map", if (!explicit_path.empty()) {
"Okay, I can place that tree for you. Here is the command:", paths.emplace_back(explicit_path);
{"overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"}, }
"Single tile16 placement. Tree tile ID is 0x02E in vanilla ALTTP"});
// Area/region editing if (const char* env_path = std::getenv("YAZE_AGENT_CATALOGUE")) {
examples_.push_back({ if (*env_path != '\0') {
"Create a 3x3 water pond at coordinates 15, 10", paths.emplace_back(env_path);
"Creating a 3x3 pond requires nine `set-tile` commands. Here they are:", }
{"overworld set-tile --map 0 --x 15 --y 10 --tile 0x14C", }
"overworld set-tile --map 0 --x 16 --y 10 --tile 0x14D",
"overworld set-tile --map 0 --x 17 --y 10 --tile 0x14C",
"overworld set-tile --map 0 --x 15 --y 11 --tile 0x14D",
"overworld set-tile --map 0 --x 16 --y 11 --tile 0x14D",
"overworld set-tile --map 0 --x 17 --y 11 --tile 0x14D",
"overworld set-tile --map 0 --x 15 --y 12 --tile 0x14E",
"overworld set-tile --map 0 --x 16 --y 12 --tile 0x14E",
"overworld set-tile --map 0 --x 17 --y 12 --tile 0x14E"},
"Water areas use different edge tiles: 0x14C (top), 0x14D (middle), "
"0x14E (bottom)"});
// Path/line creation const std::vector<std::string> defaults = {
examples_.push_back( "assets/agent/prompt_catalogue.yaml",
{"Add a dirt path from position 5,5 to 5,15", "../assets/agent/prompt_catalogue.yaml",
"I will generate a `set-tile` command for each point along the path.", "../../assets/agent/prompt_catalogue.yaml",
{"overworld set-tile --map 0 --x 5 --y 5 --tile 0x022", "assets/z3ed/prompt_catalogue.yaml",
"overworld set-tile --map 0 --x 5 --y 6 --tile 0x022", "../assets/z3ed/prompt_catalogue.yaml",
"overworld set-tile --map 0 --x 5 --y 7 --tile 0x022", };
"overworld set-tile --map 0 --x 5 --y 8 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 9 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 10 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 11 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 12 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 13 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 14 --tile 0x022",
"overworld set-tile --map 0 --x 5 --y 15 --tile 0x022"},
"Linear paths are created by placing tiles sequentially. Dirt tile is "
"0x022"});
// Forest/tree grouping for (const auto& candidate : defaults) {
examples_.push_back( paths.emplace_back(candidate);
{"Plant a row of trees horizontally at y=8 from x=20 to x=25", }
"Here are the commands to plant that row of trees:",
{"overworld set-tile --map 0 --x 20 --y 8 --tile 0x02E",
"overworld set-tile --map 0 --x 21 --y 8 --tile 0x02E",
"overworld set-tile --map 0 --x 22 --y 8 --tile 0x02E",
"overworld set-tile --map 0 --x 23 --y 8 --tile 0x02E",
"overworld set-tile --map 0 --x 24 --y 8 --tile 0x02E",
"overworld set-tile --map 0 --x 25 --y 8 --tile 0x02E"},
"Tree rows create natural barriers and visual boundaries"});
// ========================================================================== return paths;
// DUNGEON EDITING - Label-Aware Operations }
// ==========================================================================
// Sprite placement (label-aware) } // namespace
examples_.push_back(
{"Add 3 soldiers to the Eastern Palace entrance room",
"I've identified the dungeon and sprite IDs from your project's "
"labels. Here are the commands:",
{"dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 5 --y "
"3",
"dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 10 "
"--y 3",
"dungeon add-sprite --dungeon 0x02 --room 0x00 --sprite 0x41 --x 7 --y "
"8"},
"Dungeon ID 0x02 is Eastern Palace. Sprite 0x41 is soldier. Spread "
"placement for balance"});
// Object placement PromptBuilder::PromptBuilder() = default;
examples_.push_back(
{"Place a chest in the Hyrule Castle treasure room",
"Certainly. I will place a chest containing a small key in the center of "
"the room.",
{"dungeon add-chest --dungeon 0x00 --room 0x60 --x 7 --y 5 --item 0x12 "
"--big false"},
"Dungeon 0x00 is Hyrule Castle. Item 0x12 is a small key. Position "
"centered in room"});
// ========================================================================== void PromptBuilder::ClearCatalogData() {
// COMMON TILE16 REFERENCE (for AI knowledge) command_docs_.clear();
// ========================================================================== examples_.clear();
// Grass: 0x020 tool_specs_.clear();
// Dirt: 0x022 tile_reference_.clear();
// Tree: 0x02E catalogue_loaded_ = false;
// Water (top): 0x14C }
// Water (middle): 0x14D
// Water (bottom): 0x14E
// Bush: 0x003
// Rock: 0x004
// Flower: 0x021
// Sand: 0x023
// Deep Water: 0x14F
// Shallow Water: 0x150
// Validation example (still useful) absl::StatusOr<std::string> PromptBuilder::ResolveCataloguePath(
examples_.push_back( const std::string& yaml_path) const {
{"Check if my overworld changes are valid", const auto search_paths = BuildCatalogueSearchPaths(yaml_path);
"Yes, I can validate the ROM for you.", for (const auto& candidate : search_paths) {
{"rom validate"}, fs::path resolved = candidate;
"Validation ensures ROM integrity after tile modifications"}); if (resolved.is_relative()) {
resolved = fs::absolute(resolved);
}
if (fs::exists(resolved)) {
return resolved.string();
}
}
// ========================================================================== return absl::NotFoundError(
// Q&A / Tool Use absl::StrCat("Prompt catalogue not found. Checked paths: ",
// ========================================================================== absl::StrJoin(search_paths, ", ",
examples_.push_back( [](std::string* out, const fs::path& path) {
{"What dungeons are in this project?", absl::StrAppend(out, path.string());
"I can list the dungeons for you. Let me check the resource labels.", })));
{},
"The user is asking a question. I need to use the `resource-list` tool "
"to find the answer.",
{{"resource-list", {{"type", "dungeon"}}}}});
} }
absl::Status PromptBuilder::LoadResourceCatalogue(const std::string& yaml_path) { absl::Status PromptBuilder::LoadResourceCatalogue(const std::string& yaml_path) {
// TODO: Parse z3ed-resources.yaml when available auto resolved_or = ResolveCataloguePath(yaml_path);
// For now, use hardcoded command reference if (!resolved_or.ok()) {
ClearCatalogData();
return resolved_or.status();
}
command_docs_["palette export"] = const std::string& resolved_path = resolved_or.value();
"Export palette data to JSON file\n"
" --group <group> Palette group (overworld, dungeon, sprite)\n"
" --id <id> Palette ID (0-based index)\n"
" --to <file> Output JSON file path";
command_docs_["palette import"] = YAML::Node root;
"Import palette data from JSON file\n" try {
" --group <group> Palette group (overworld, dungeon, sprite)\n" root = YAML::LoadFile(resolved_path);
" --id <id> Palette ID (0-based index)\n" } catch (const YAML::BadFile& e) {
" --from <file> Input JSON file path"; ClearCatalogData();
return absl::NotFoundError(
absl::StrCat("Unable to open prompt catalogue at ", resolved_path,
": ", e.what()));
} catch (const YAML::ParserException& e) {
ClearCatalogData();
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse prompt catalogue at ", resolved_path,
": ", e.what()));
}
command_docs_["palette set-color"] = nlohmann::json catalogue = YamlToJson(root);
"Modify a color in palette JSON file\n" ClearCatalogData();
" --file <file> Palette JSON file to modify\n"
" --index <index> Color index (0-15 per palette)\n"
" --color <hex> New color in hex (0xRRGGBB format)";
command_docs_["overworld set-tile"] = if (catalogue.contains("commands")) {
"Place a tile in the overworld\n" if (auto status = ParseCommands(catalogue["commands"]); !status.ok()) {
" --map <id> Map ID (0-based)\n" return status;
" --x <x> X coordinate (0-63)\n" }
" --y <y> Y coordinate (0-63)\n" }
" --tile <hex> Tile ID in hex (e.g., 0x02E for tree)";
command_docs_["sprite set-position"] = if (catalogue.contains("tools")) {
"Move a sprite to new position\n" if (auto status = ParseTools(catalogue["tools"]); !status.ok()) {
" --id <id> Sprite ID\n" return status;
" --x <x> X coordinate\n" }
" --y <y> Y coordinate"; }
command_docs_["dungeon set-room-tile"] = if (catalogue.contains("examples")) {
"Place a tile in dungeon room\n" if (auto status = ParseExamples(catalogue["examples"]); !status.ok()) {
" --room <id> Room ID\n" return status;
" --x <x> X coordinate\n" }
" --y <y> Y coordinate\n" }
" --tile <hex> Tile ID";
command_docs_["rom validate"] = if (catalogue.contains("tile16_reference")) {
"Validate ROM integrity and structure"; ParseTileReference(catalogue["tile16_reference"]);
}
catalogue_loaded_ = true; catalogue_loaded_ = true;
return absl::OkStatus(); return absl::OkStatus();
} }
std::string PromptBuilder::BuildCommandReference() { absl::Status PromptBuilder::ParseCommands(const nlohmann::json& commands) {
if (!commands.is_object()) {
return absl::InvalidArgumentError(
"commands section must be an object mapping command names to docs");
}
for (const auto& [name, value] : commands.items()) {
if (!value.is_string()) {
return absl::InvalidArgumentError(
absl::StrCat("Command entry for ", name, " must be a string"));
}
command_docs_[name] = value.get<std::string>();
}
return absl::OkStatus();
}
absl::Status PromptBuilder::ParseTools(const nlohmann::json& tools) {
if (!tools.is_array()) {
return absl::InvalidArgumentError("tools section must be an array");
}
for (const auto& tool_json : tools) {
if (!tool_json.is_object()) {
return absl::InvalidArgumentError(
"Each tool entry must be an object with name/description");
}
ToolSpecification spec;
if (tool_json.contains("name") && tool_json["name"].is_string()) {
spec.name = tool_json["name"].get<std::string>();
} else {
return absl::InvalidArgumentError("Tool entry missing name");
}
if (tool_json.contains("description") && tool_json["description"].is_string()) {
spec.description = tool_json["description"].get<std::string>();
}
if (tool_json.contains("usage_notes") && tool_json["usage_notes"].is_string()) {
spec.usage_notes = tool_json["usage_notes"].get<std::string>();
}
if (tool_json.contains("arguments")) {
const auto& args = tool_json["arguments"];
if (!args.is_array()) {
return absl::InvalidArgumentError(
absl::StrCat("Tool arguments for ", spec.name, " must be an array"));
}
for (const auto& arg_json : args) {
if (!arg_json.is_object()) {
return absl::InvalidArgumentError(
absl::StrCat("Argument entries for ", spec.name,
" must be objects"));
}
ToolArgument arg;
if (arg_json.contains("name") && arg_json["name"].is_string()) {
arg.name = arg_json["name"].get<std::string>();
} else {
return absl::InvalidArgumentError(
absl::StrCat("Argument entry for ", spec.name,
" is missing a name"));
}
if (arg_json.contains("description") && arg_json["description"].is_string()) {
arg.description = arg_json["description"].get<std::string>();
}
if (arg_json.contains("required")) {
if (!arg_json["required"].is_boolean()) {
return absl::InvalidArgumentError(
absl::StrCat("Argument 'required' flag for ", spec.name,
"::", arg.name, " must be boolean"));
}
arg.required = arg_json["required"].get<bool>();
}
if (arg_json.contains("example") && arg_json["example"].is_string()) {
arg.example = arg_json["example"].get<std::string>();
}
spec.arguments.push_back(std::move(arg));
}
}
tool_specs_.push_back(std::move(spec));
}
return absl::OkStatus();
}
absl::Status PromptBuilder::ParseExamples(const nlohmann::json& examples) {
if (!examples.is_array()) {
return absl::InvalidArgumentError("examples section must be an array");
}
for (const auto& example_json : examples) {
if (!example_json.is_object()) {
return absl::InvalidArgumentError("Each example entry must be an object");
}
FewShotExample example;
if (example_json.contains("user_prompt") &&
example_json["user_prompt"].is_string()) {
example.user_prompt = example_json["user_prompt"].get<std::string>();
} else {
return absl::InvalidArgumentError("Example missing user_prompt");
}
if (example_json.contains("text_response") &&
example_json["text_response"].is_string()) {
example.text_response = example_json["text_response"].get<std::string>();
}
if (example_json.contains("reasoning") &&
example_json["reasoning"].is_string()) {
example.explanation = example_json["reasoning"].get<std::string>();
}
if (example_json.contains("commands")) {
const auto& commands = example_json["commands"];
if (!commands.is_array()) {
return absl::InvalidArgumentError(
absl::StrCat("Example commands for ", example.user_prompt,
" must be an array"));
}
for (const auto& cmd : commands) {
if (!cmd.is_string()) {
return absl::InvalidArgumentError(
absl::StrCat("Command entries for ", example.user_prompt,
" must be strings"));
}
example.expected_commands.push_back(cmd.get<std::string>());
}
}
if (example_json.contains("tool_calls")) {
const auto& calls = example_json["tool_calls"];
if (!calls.is_array()) {
return absl::InvalidArgumentError(
absl::StrCat("Tool calls for ", example.user_prompt,
" must be an array"));
}
for (const auto& call_json : calls) {
if (!call_json.is_object()) {
return absl::InvalidArgumentError(
absl::StrCat("Tool call entries for ", example.user_prompt,
" must be objects"));
}
ToolCall call;
if (call_json.contains("tool_name") && call_json["tool_name"].is_string()) {
call.tool_name = call_json["tool_name"].get<std::string>();
} else {
return absl::InvalidArgumentError(
absl::StrCat("Tool call missing tool_name in example: ",
example.user_prompt));
}
if (call_json.contains("args")) {
const auto& args = call_json["args"];
if (!args.is_object()) {
return absl::InvalidArgumentError(
absl::StrCat("Tool call args for ", example.user_prompt,
" must be an object"));
}
for (const auto& [key, value] : args.items()) {
if (!value.is_string()) {
return absl::InvalidArgumentError(
absl::StrCat("Tool call arg value for ", example.user_prompt,
" must be a string"));
}
call.args[key] = value.get<std::string>();
}
}
example.tool_calls.push_back(std::move(call));
}
}
example.explanation = example_json.value("explanation", example.explanation);
examples_.push_back(std::move(example));
}
return absl::OkStatus();
}
void PromptBuilder::ParseTileReference(const nlohmann::json& tile_reference) {
if (!tile_reference.is_object()) {
return;
}
for (const auto& [alias, value] : tile_reference.items()) {
if (value.is_string()) {
tile_reference_[alias] = value.get<std::string>();
}
}
}
std::string PromptBuilder::LookupTileId(const std::string& alias) const {
auto it = tile_reference_.find(alias);
if (it != tile_reference_.end()) {
return it->second;
}
return "";
}
std::string PromptBuilder::BuildCommandReference() const {
std::ostringstream oss; std::ostringstream oss;
oss << "# Available z3ed Commands\n\n"; oss << "# Available z3ed Commands\n\n";
@@ -197,7 +372,41 @@ std::string PromptBuilder::BuildCommandReference() {
return oss.str(); return oss.str();
} }
std::string PromptBuilder::BuildFewShotExamplesSection() { std::string PromptBuilder::BuildToolReference() const {
if (tool_specs_.empty()) {
return "";
}
std::ostringstream oss;
oss << "# Available Agent Tools\n\n";
for (const auto& spec : tool_specs_) {
oss << "## " << spec.name << "\n";
if (!spec.description.empty()) {
oss << spec.description << "\n\n";
}
if (!spec.arguments.empty()) {
oss << "| Argument | Required | Description | Example |\n";
oss << "| --- | --- | --- | --- |\n";
for (const auto& arg : spec.arguments) {
oss << "| `" << arg.name << "` | " << (arg.required ? "yes" : "no")
<< " | " << arg.description << " | "
<< (arg.example.empty() ? "" : "`" + arg.example + "`")
<< " |\n";
}
oss << "\n";
}
if (!spec.usage_notes.empty()) {
oss << "_Usage:_ " << spec.usage_notes << "\n\n";
}
}
return oss.str();
}
std::string PromptBuilder::BuildFewShotExamplesSection() const {
std::ostringstream oss; std::ostringstream oss;
oss << "# Example Command Sequences\n\n"; oss << "# Example Command Sequences\n\n";
@@ -205,39 +414,42 @@ std::string PromptBuilder::BuildFewShotExamplesSection() {
for (const auto& example : examples_) { for (const auto& example : examples_) {
oss << "**User Request:** \"" << example.user_prompt << "\"\n"; oss << "**User Request:** \"" << example.user_prompt << "\"\n";
oss << "**Commands:**\n"; oss << "**Structured Response:**\n";
oss << "```json\n{";
oss << " \"text_response\": \"" << example.text_response << "\",\n"; nlohmann::json example_json = nlohmann::json::object();
oss << " \"tool_calls\": ["; if (!example.text_response.empty()) {
std::vector<std::string> tool_calls; example_json["text_response"] = example.text_response;
}
if (!example.expected_commands.empty()) {
example_json["commands"] = example.expected_commands;
}
if (!example.explanation.empty()) {
example_json["reasoning"] = example.explanation;
}
if (!example.tool_calls.empty()) {
nlohmann::json calls = nlohmann::json::array();
for (const auto& call : example.tool_calls) { for (const auto& call : example.tool_calls) {
std::vector<std::string> args; nlohmann::json call_json;
call_json["tool_name"] = call.tool_name;
nlohmann::json args = nlohmann::json::object();
for (const auto& [key, value] : call.args) { for (const auto& [key, value] : call.args) {
args.push_back("\"" + key + "\": \"" + value + "\""); args[key] = value;
} }
tool_calls.push_back("{\"tool_name\": \"" + call.tool_name + call_json["args"] = std::move(args);
"\", \"args\": {" + absl::StrJoin(args, ", ") + "}}"); calls.push_back(std::move(call_json));
}
example_json["tool_calls"] = std::move(calls);
} }
oss << absl::StrJoin(tool_calls, ", ");
oss << "],\n";
oss << " \"commands\": [";
std::vector<std::string> quoted_cmds; oss << "```json\n" << example_json.dump(2) << "\n```\n\n";
for (const auto& cmd : example.expected_commands) {
quoted_cmds.push_back("\"" + cmd + "\"");
}
oss << absl::StrJoin(quoted_cmds, ", ");
oss << "],\n";
oss << " \"reasoning\": \"" << example.explanation << "\"\n";
oss << "}\n```\n\n";
} }
return oss.str(); return oss.str();
} }
std::string PromptBuilder::BuildConstraintsSection() { std::string PromptBuilder::BuildConstraintsSection() const {
return R"( std::ostringstream oss;
oss << R"(
# Critical Constraints # Critical Constraints
1. **Output Format:** You MUST respond with ONLY a JSON object with the following structure: 1. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
@@ -262,17 +474,29 @@ std::string PromptBuilder::BuildConstraintsSection() {
- Multiple tile placement: multiple overworld set-tile commands - Multiple tile placement: multiple overworld set-tile commands
- Validation: single rom validate command - Validation: single rom validate command
4. **Tile IDs Reference (ALTTP):** 4. **Error Prevention:**
- Tree: 0x02E
- House (2x2): 0x0C0, 0x0C1, 0x0D0, 0x0D1
- Water: 0x038
- Grass: 0x000
5. **Error Prevention:**
- Always export before modifying palettes - Always export before modifying palettes
- Use temporary file names (temp_*.json) for intermediate files - Use temporary file names (temp_*.json) for intermediate files
- Validate coordinates are within bounds - Validate coordinates are within bounds
)"; )";
if (!tile_reference_.empty()) {
oss << "\n" << BuildTileReferenceSection();
}
return oss.str();
}
std::string PromptBuilder::BuildTileReferenceSection() const {
std::ostringstream oss;
oss << "# Tile16 Reference (ALTTP)\n\n";
for (const auto& [alias, value] : tile_reference_) {
oss << "- " << alias << ": " << value << "\n";
}
oss << "\n";
return oss.str();
} }
std::string PromptBuilder::BuildContextSection(const RomContext& context) { std::string PromptBuilder::BuildContextSection(const RomContext& context) {
@@ -322,8 +546,13 @@ std::string PromptBuilder::BuildSystemInstruction() {
<< "the user's request.\n\n"; << "the user's request.\n\n";
if (catalogue_loaded_) { if (catalogue_loaded_) {
if (!command_docs_.empty()) {
oss << BuildCommandReference(); oss << BuildCommandReference();
} }
if (!tool_specs_.empty()) {
oss << BuildToolReference();
}
}
oss << BuildConstraintsSection(); oss << BuildConstraintsSection();

View File

@@ -1,11 +1,13 @@
#ifndef YAZE_CLI_SERVICE_PROMPT_BUILDER_H_ #ifndef YAZE_CLI_SERVICE_PROMPT_BUILDER_H_
#define YAZE_CLI_SERVICE_PROMPT_BUILDER_H_ #define YAZE_CLI_SERVICE_PROMPT_BUILDER_H_
#include <map>
#include <string> #include <string>
#include <vector> #include <vector>
#include <map>
#include "absl/status/status.h"
#include "absl/status/statusor.h" #include "absl/status/statusor.h"
#include "nlohmann/json_fwd.hpp"
#include "cli/service/ai/common.h" #include "cli/service/ai/common.h"
#include "cli/service/resources/resource_context_builder.h" #include "cli/service/resources/resource_context_builder.h"
#include "app/rom.h" #include "app/rom.h"
@@ -26,6 +28,20 @@ struct FewShotExample {
std::vector<ToolCall> tool_calls; std::vector<ToolCall> tool_calls;
}; };
struct ToolArgument {
std::string name;
std::string description;
bool required = false;
std::string example;
};
struct ToolSpecification {
std::string name;
std::string description;
std::vector<ToolArgument> arguments;
std::string usage_notes;
};
// ROM context information to inject into prompts // ROM context information to inject into prompts
struct RomContext { struct RomContext {
std::string rom_path; std::string rom_path;
@@ -65,22 +81,34 @@ class PromptBuilder {
// Get few-shot examples for specific category // Get few-shot examples for specific category
std::vector<FewShotExample> GetExamplesForCategory( std::vector<FewShotExample> GetExamplesForCategory(
const std::string& category); const std::string& category);
std::string LookupTileId(const std::string& alias) const;
const std::map<std::string, std::string>& tile_reference() const {
return tile_reference_;
}
// Set verbosity level (0=minimal, 1=standard, 2=verbose) // Set verbosity level (0=minimal, 1=standard, 2=verbose)
void SetVerbosity(int level) { verbosity_ = level; } void SetVerbosity(int level) { verbosity_ = level; }
private: private:
std::string BuildCommandReference(); std::string BuildCommandReference() const;
std::string BuildFewShotExamplesSection(); std::string BuildFewShotExamplesSection() const;
std::string BuildToolReference() const;
std::string BuildContextSection(const RomContext& context); std::string BuildContextSection(const RomContext& context);
std::string BuildConstraintsSection(); std::string BuildConstraintsSection() const;
std::string BuildTileReferenceSection() const;
void LoadDefaultExamples(); absl::StatusOr<std::string> ResolveCataloguePath(const std::string& yaml_path) const;
void ClearCatalogData();
absl::Status ParseCommands(const nlohmann::json& commands);
absl::Status ParseTools(const nlohmann::json& tools);
absl::Status ParseExamples(const nlohmann::json& examples);
void ParseTileReference(const nlohmann::json& tile_reference);
Rom* rom_ = nullptr; Rom* rom_ = nullptr;
std::unique_ptr<ResourceContextBuilder> resource_context_builder_; std::unique_ptr<ResourceContextBuilder> resource_context_builder_;
std::map<std::string, std::string> command_docs_; // Command name -> docs std::map<std::string, std::string> command_docs_; // Command name -> docs
std::vector<FewShotExample> examples_; std::vector<FewShotExample> examples_;
std::vector<ToolSpecification> tool_specs_;
std::map<std::string, std::string> tile_reference_;
int verbosity_ = 1; int verbosity_ = 1;
bool catalogue_loaded_ = false; bool catalogue_loaded_ = false;
}; };

View File

@@ -11,6 +11,39 @@ if(NOT ftxui_POPULATED)
add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL)
endif() endif()
find_package(yaml-cpp CONFIG)
if(NOT yaml-cpp_FOUND)
message(STATUS "yaml-cpp not found via package config, fetching from source")
FetchContent_Declare(yaml-cpp
GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
GIT_TAG 0.8.0
)
FetchContent_GetProperties(yaml-cpp)
if(NOT yaml-cpp_POPULATED)
FetchContent_Populate(yaml-cpp)
# Ensure compatibility with newer CMake versions by adjusting minimum requirement
set(_yaml_cpp_cmakelists "${yaml-cpp_SOURCE_DIR}/CMakeLists.txt")
if(EXISTS "${_yaml_cpp_cmakelists}")
file(READ "${_yaml_cpp_cmakelists}" _yaml_cpp_cmake_contents)
if(_yaml_cpp_cmake_contents MATCHES "cmake_minimum_required\\(VERSION 3\\.4\\)")
string(REPLACE "cmake_minimum_required(VERSION 3.4)"
"cmake_minimum_required(VERSION 3.5)"
_yaml_cpp_cmake_contents "${_yaml_cpp_cmake_contents}")
file(WRITE "${_yaml_cpp_cmakelists}" "${_yaml_cpp_cmake_contents}")
endif()
endif()
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "Disable yaml-cpp tests" FORCE)
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "Disable yaml-cpp contrib" FORCE)
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "Disable yaml-cpp tools" FORCE)
set(YAML_CPP_INSTALL OFF CACHE BOOL "Disable yaml-cpp install" FORCE)
set(YAML_CPP_FORMAT_SOURCE OFF CACHE BOOL "Disable yaml-cpp format target" FORCE)
add_subdirectory(${yaml-cpp_SOURCE_DIR} ${yaml-cpp_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
endif()
# Platform-specific file dialog sources # Platform-specific file dialog sources
if(APPLE) if(APPLE)
set(FILE_DIALOG_SRC set(FILE_DIALOG_SRC
@@ -128,6 +161,7 @@ target_link_libraries(
z3ed PUBLIC z3ed PUBLIC
asar-static asar-static
yaze_agent yaze_agent
yaml-cpp
ftxui::component ftxui::component
ftxui::screen ftxui::screen
ftxui::dom ftxui::dom

View File

@@ -7,7 +7,8 @@
"name": "sdl2", "name": "sdl2",
"platform": "!uwp", "platform": "!uwp",
"features": ["vulkan"] "features": ["vulkan"]
} },
"yaml-cpp"
], ],
"builtin-baseline": "4bee3f5aae7aefbc129ca81c33d6a062b02fcf3b", "builtin-baseline": "4bee3f5aae7aefbc129ca81c33d6a062b02fcf3b",
"overrides": [], "overrides": [],