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:
@@ -24,6 +24,7 @@ target_link_libraries(yaze_agent
|
||||
PUBLIC
|
||||
yaze_common
|
||||
${ABSL_TARGETS}
|
||||
yaml-cpp
|
||||
)
|
||||
|
||||
target_include_directories(yaze_agent
|
||||
|
||||
@@ -152,6 +152,9 @@ ConversationalAgentService::ConversationalAgentService() {
|
||||
void ConversationalAgentService::SetRomContext(Rom* rom) {
|
||||
rom_context_ = rom;
|
||||
tool_dispatcher_.SetRomContext(rom_context_);
|
||||
if (ai_service_) {
|
||||
ai_service_->SetRomContext(rom_context_);
|
||||
}
|
||||
}
|
||||
|
||||
absl::StatusOr<ChatMessage> ConversationalAgentService::SendMessage(
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "cli/service/ai/common.h"
|
||||
|
||||
namespace yaze {
|
||||
class Rom;
|
||||
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
struct ChatMessage;
|
||||
@@ -18,6 +20,10 @@ class AIService {
|
||||
public:
|
||||
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.
|
||||
virtual absl::StatusOr<AgentResponse> GenerateResponse(
|
||||
const std::string& prompt) = 0;
|
||||
@@ -30,6 +36,7 @@ class AIService {
|
||||
// Mock implementation for testing
|
||||
class MockAIService : public AIService {
|
||||
public:
|
||||
void SetRomContext(Rom* rom) override { (void)rom; }
|
||||
absl::StatusOr<AgentResponse> GenerateResponse(
|
||||
const std::string& prompt) override;
|
||||
absl::StatusOr<AgentResponse> GenerateResponse(
|
||||
|
||||
@@ -21,7 +21,10 @@ namespace cli {
|
||||
GeminiAIService::GeminiAIService(const GeminiConfig& config)
|
||||
: config_(config) {
|
||||
// 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()) {
|
||||
// Use enhanced prompting by default
|
||||
@@ -39,6 +42,10 @@ std::string GeminiAIService::BuildSystemInstruction() {
|
||||
return prompt_builder_.BuildSystemInstruction();
|
||||
}
|
||||
|
||||
void GeminiAIService::SetRomContext(Rom* rom) {
|
||||
prompt_builder_.SetRom(rom);
|
||||
}
|
||||
|
||||
absl::Status GeminiAIService::CheckAvailability() {
|
||||
#ifndef YAZE_WITH_JSON
|
||||
return absl::UnimplementedError(
|
||||
|
||||
@@ -27,6 +27,7 @@ struct GeminiConfig {
|
||||
class GeminiAIService : public AIService {
|
||||
public:
|
||||
explicit GeminiAIService(const GeminiConfig& config);
|
||||
void SetRomContext(Rom* rom) override;
|
||||
|
||||
// Primary interface
|
||||
absl::StatusOr<AgentResponse> GenerateResponse(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "cli/service/ai/ollama_ai_service.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
@@ -33,7 +34,10 @@ namespace cli {
|
||||
|
||||
OllamaAIService::OllamaAIService(const OllamaConfig& config) : config_(config) {
|
||||
// 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()) {
|
||||
// Use enhanced prompting by default
|
||||
@@ -51,6 +55,10 @@ std::string OllamaAIService::BuildSystemPrompt() {
|
||||
return prompt_builder_.BuildSystemInstruction();
|
||||
}
|
||||
|
||||
void OllamaAIService::SetRomContext(Rom* rom) {
|
||||
prompt_builder_.SetRom(rom);
|
||||
}
|
||||
|
||||
absl::Status OllamaAIService::CheckAvailability() {
|
||||
#if !YAZE_HAS_HTTPLIB || !YAZE_HAS_JSON
|
||||
return absl::UnimplementedError(
|
||||
|
||||
@@ -25,6 +25,8 @@ struct OllamaConfig {
|
||||
class OllamaAIService : public AIService {
|
||||
public:
|
||||
explicit OllamaAIService(const OllamaConfig& config);
|
||||
|
||||
void SetRomContext(Rom* rom) override;
|
||||
|
||||
// Generate z3ed commands from natural language prompt
|
||||
absl::StatusOr<AgentResponse> GenerateResponse(
|
||||
|
||||
@@ -1,190 +1,365 @@
|
||||
#include "cli/service/ai/prompt_builder.h"
|
||||
#include "cli/service/agent/conversational_agent_service.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "yaml-cpp/yaml.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
|
||||
PromptBuilder::PromptBuilder() {
|
||||
LoadDefaultExamples();
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
nlohmann::json YamlToJson(const YAML::Node& node) {
|
||||
if (!node) {
|
||||
return nlohmann::json();
|
||||
}
|
||||
|
||||
switch (node.Type()) {
|
||||
case YAML::NodeType::Scalar:
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void PromptBuilder::LoadDefaultExamples() {
|
||||
// ==========================================================================
|
||||
// OVERWORLD TILE16 EDITING - Primary Focus
|
||||
// ==========================================================================
|
||||
|
||||
// Single tile placement
|
||||
examples_.push_back({
|
||||
"Place a tree at position 10, 20 on the Light World map",
|
||||
"Okay, I can place that tree for you. Here is the command:",
|
||||
{"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
|
||||
examples_.push_back({
|
||||
"Create a 3x3 water pond at coordinates 15, 10",
|
||||
"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
|
||||
examples_.push_back(
|
||||
{"Add a dirt path from position 5,5 to 5,15",
|
||||
"I will generate a `set-tile` command for each point along the path.",
|
||||
{"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"},
|
||||
"Linear paths are created by placing tiles sequentially. Dirt tile is "
|
||||
"0x022"});
|
||||
std::vector<fs::path> BuildCatalogueSearchPaths(const std::string& explicit_path) {
|
||||
std::vector<fs::path> paths;
|
||||
if (!explicit_path.empty()) {
|
||||
paths.emplace_back(explicit_path);
|
||||
}
|
||||
|
||||
// Forest/tree grouping
|
||||
examples_.push_back(
|
||||
{"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"});
|
||||
if (const char* env_path = std::getenv("YAZE_AGENT_CATALOGUE")) {
|
||||
if (*env_path != '\0') {
|
||||
paths.emplace_back(env_path);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// DUNGEON EDITING - Label-Aware Operations
|
||||
// ==========================================================================
|
||||
const std::vector<std::string> defaults = {
|
||||
"assets/agent/prompt_catalogue.yaml",
|
||||
"../assets/agent/prompt_catalogue.yaml",
|
||||
"../../assets/agent/prompt_catalogue.yaml",
|
||||
"assets/z3ed/prompt_catalogue.yaml",
|
||||
"../assets/z3ed/prompt_catalogue.yaml",
|
||||
};
|
||||
|
||||
// Sprite placement (label-aware)
|
||||
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"});
|
||||
for (const auto& candidate : defaults) {
|
||||
paths.emplace_back(candidate);
|
||||
}
|
||||
|
||||
// Object placement
|
||||
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"});
|
||||
return paths;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// COMMON TILE16 REFERENCE (for AI knowledge)
|
||||
// ==========================================================================
|
||||
// Grass: 0x020
|
||||
// Dirt: 0x022
|
||||
// Tree: 0x02E
|
||||
// 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)
|
||||
examples_.push_back(
|
||||
{"Check if my overworld changes are valid",
|
||||
"Yes, I can validate the ROM for you.",
|
||||
{"rom validate"},
|
||||
"Validation ensures ROM integrity after tile modifications"});
|
||||
} // namespace
|
||||
|
||||
// ==========================================================================
|
||||
// Q&A / Tool Use
|
||||
// ==========================================================================
|
||||
examples_.push_back(
|
||||
{"What dungeons are in this project?",
|
||||
"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"}}}}});
|
||||
PromptBuilder::PromptBuilder() = default;
|
||||
|
||||
void PromptBuilder::ClearCatalogData() {
|
||||
command_docs_.clear();
|
||||
examples_.clear();
|
||||
tool_specs_.clear();
|
||||
tile_reference_.clear();
|
||||
catalogue_loaded_ = false;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> PromptBuilder::ResolveCataloguePath(
|
||||
const std::string& yaml_path) const {
|
||||
const auto search_paths = BuildCatalogueSearchPaths(yaml_path);
|
||||
for (const auto& candidate : search_paths) {
|
||||
fs::path resolved = candidate;
|
||||
if (resolved.is_relative()) {
|
||||
resolved = fs::absolute(resolved);
|
||||
}
|
||||
if (fs::exists(resolved)) {
|
||||
return resolved.string();
|
||||
}
|
||||
}
|
||||
|
||||
return absl::NotFoundError(
|
||||
absl::StrCat("Prompt catalogue not found. Checked paths: ",
|
||||
absl::StrJoin(search_paths, ", ",
|
||||
[](std::string* out, const fs::path& path) {
|
||||
absl::StrAppend(out, path.string());
|
||||
})));
|
||||
}
|
||||
|
||||
absl::Status PromptBuilder::LoadResourceCatalogue(const std::string& yaml_path) {
|
||||
// TODO: Parse z3ed-resources.yaml when available
|
||||
// For now, use hardcoded command reference
|
||||
|
||||
command_docs_["palette export"] =
|
||||
"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"] =
|
||||
"Import palette data from JSON file\n"
|
||||
" --group <group> Palette group (overworld, dungeon, sprite)\n"
|
||||
" --id <id> Palette ID (0-based index)\n"
|
||||
" --from <file> Input JSON file path";
|
||||
|
||||
command_docs_["palette set-color"] =
|
||||
"Modify a color in palette JSON file\n"
|
||||
" --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"] =
|
||||
"Place a tile in the overworld\n"
|
||||
" --map <id> Map ID (0-based)\n"
|
||||
" --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"] =
|
||||
"Move a sprite to new position\n"
|
||||
" --id <id> Sprite ID\n"
|
||||
" --x <x> X coordinate\n"
|
||||
" --y <y> Y coordinate";
|
||||
|
||||
command_docs_["dungeon set-room-tile"] =
|
||||
"Place a tile in dungeon room\n"
|
||||
" --room <id> Room ID\n"
|
||||
" --x <x> X coordinate\n"
|
||||
" --y <y> Y coordinate\n"
|
||||
" --tile <hex> Tile ID";
|
||||
|
||||
command_docs_["rom validate"] =
|
||||
"Validate ROM integrity and structure";
|
||||
|
||||
auto resolved_or = ResolveCataloguePath(yaml_path);
|
||||
if (!resolved_or.ok()) {
|
||||
ClearCatalogData();
|
||||
return resolved_or.status();
|
||||
}
|
||||
|
||||
const std::string& resolved_path = resolved_or.value();
|
||||
|
||||
YAML::Node root;
|
||||
try {
|
||||
root = YAML::LoadFile(resolved_path);
|
||||
} catch (const YAML::BadFile& e) {
|
||||
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()));
|
||||
}
|
||||
|
||||
nlohmann::json catalogue = YamlToJson(root);
|
||||
ClearCatalogData();
|
||||
|
||||
if (catalogue.contains("commands")) {
|
||||
if (auto status = ParseCommands(catalogue["commands"]); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogue.contains("tools")) {
|
||||
if (auto status = ParseTools(catalogue["tools"]); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogue.contains("examples")) {
|
||||
if (auto status = ParseExamples(catalogue["examples"]); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogue.contains("tile16_reference")) {
|
||||
ParseTileReference(catalogue["tile16_reference"]);
|
||||
}
|
||||
|
||||
catalogue_loaded_ = true;
|
||||
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;
|
||||
|
||||
oss << "# Available z3ed Commands\n\n";
|
||||
@@ -197,82 +372,131 @@ std::string PromptBuilder::BuildCommandReference() {
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string PromptBuilder::BuildFewShotExamplesSection() {
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "# Example Command Sequences\n\n";
|
||||
oss << "Here are proven examples of how to accomplish common tasks:\n\n";
|
||||
|
||||
for (const auto& example : examples_) {
|
||||
oss << "**User Request:** \"" << example.user_prompt << "\"\n";
|
||||
oss << "**Commands:**\n";
|
||||
oss << "```json\n{";
|
||||
oss << " \"text_response\": \"" << example.text_response << "\",\n";
|
||||
oss << " \"tool_calls\": [";
|
||||
std::vector<std::string> tool_calls;
|
||||
for (const auto& call : example.tool_calls) {
|
||||
std::vector<std::string> args;
|
||||
for (const auto& [key, value] : call.args) {
|
||||
args.push_back("\"" + key + "\": \"" + value + "\"");
|
||||
}
|
||||
tool_calls.push_back("{\"tool_name\": \"" + call.tool_name +
|
||||
"\", \"args\": {" + absl::StrJoin(args, ", ") + "}}");
|
||||
}
|
||||
oss << absl::StrJoin(tool_calls, ", ");
|
||||
oss << "],\n";
|
||||
oss << " \"commands\": [";
|
||||
|
||||
std::vector<std::string> quoted_cmds;
|
||||
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";
|
||||
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::BuildConstraintsSection() {
|
||||
return R"(
|
||||
std::string PromptBuilder::BuildFewShotExamplesSection() const {
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "# Example Command Sequences\n\n";
|
||||
oss << "Here are proven examples of how to accomplish common tasks:\n\n";
|
||||
|
||||
for (const auto& example : examples_) {
|
||||
oss << "**User Request:** \"" << example.user_prompt << "\"\n";
|
||||
oss << "**Structured Response:**\n";
|
||||
|
||||
nlohmann::json example_json = nlohmann::json::object();
|
||||
if (!example.text_response.empty()) {
|
||||
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) {
|
||||
nlohmann::json call_json;
|
||||
call_json["tool_name"] = call.tool_name;
|
||||
nlohmann::json args = nlohmann::json::object();
|
||||
for (const auto& [key, value] : call.args) {
|
||||
args[key] = value;
|
||||
}
|
||||
call_json["args"] = std::move(args);
|
||||
calls.push_back(std::move(call_json));
|
||||
}
|
||||
example_json["tool_calls"] = std::move(calls);
|
||||
}
|
||||
|
||||
oss << "```json\n" << example_json.dump(2) << "\n```\n\n";
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string PromptBuilder::BuildConstraintsSection() const {
|
||||
std::ostringstream oss;
|
||||
oss << R"(
|
||||
# Critical Constraints
|
||||
|
||||
1. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
|
||||
{
|
||||
"text_response": "Your natural language reply to the user.",
|
||||
"tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
|
||||
"commands": ["command1", "command2"],
|
||||
"reasoning": "Your thought process."
|
||||
}
|
||||
- `text_response` is for conversational replies.
|
||||
- `tool_calls` is for asking questions about the ROM. Use the available tools.
|
||||
- `commands` is for generating commands to modify the ROM.
|
||||
- All fields are optional.
|
||||
{
|
||||
"text_response": "Your natural language reply to the user.",
|
||||
"tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
|
||||
"commands": ["command1", "command2"],
|
||||
"reasoning": "Your thought process."
|
||||
}
|
||||
- `text_response` is for conversational replies.
|
||||
- `tool_calls` is for asking questions about the ROM. Use the available tools.
|
||||
- `commands` is for generating commands to modify the ROM.
|
||||
- All fields are optional.
|
||||
|
||||
2. **Command Syntax:** Follow the exact syntax shown in examples
|
||||
- Use correct flag names (--group, --id, --to, --from, etc.)
|
||||
- Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
|
||||
- Coordinates are 0-based indices
|
||||
- Use correct flag names (--group, --id, --to, --from, etc.)
|
||||
- Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
|
||||
- Coordinates are 0-based indices
|
||||
|
||||
3. **Common Patterns:**
|
||||
- Palette modifications: export → set-color → import
|
||||
- Multiple tile placement: multiple overworld set-tile commands
|
||||
- Validation: single rom validate command
|
||||
- Palette modifications: export → set-color → import
|
||||
- Multiple tile placement: multiple overworld set-tile commands
|
||||
- Validation: single rom validate command
|
||||
|
||||
4. **Tile IDs Reference (ALTTP):**
|
||||
- Tree: 0x02E
|
||||
- House (2x2): 0x0C0, 0x0C1, 0x0D0, 0x0D1
|
||||
- Water: 0x038
|
||||
- Grass: 0x000
|
||||
|
||||
5. **Error Prevention:**
|
||||
- Always export before modifying palettes
|
||||
- Use temporary file names (temp_*.json) for intermediate files
|
||||
- Validate coordinates are within bounds
|
||||
4. **Error Prevention:**
|
||||
- Always export before modifying palettes
|
||||
- Use temporary file names (temp_*.json) for intermediate files
|
||||
- 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) {
|
||||
@@ -322,7 +546,12 @@ std::string PromptBuilder::BuildSystemInstruction() {
|
||||
<< "the user's request.\n\n";
|
||||
|
||||
if (catalogue_loaded_) {
|
||||
oss << BuildCommandReference();
|
||||
if (!command_docs_.empty()) {
|
||||
oss << BuildCommandReference();
|
||||
}
|
||||
if (!tool_specs_.empty()) {
|
||||
oss << BuildToolReference();
|
||||
}
|
||||
}
|
||||
|
||||
oss << BuildConstraintsSection();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#ifndef YAZE_CLI_SERVICE_PROMPT_BUILDER_H_
|
||||
#define YAZE_CLI_SERVICE_PROMPT_BUILDER_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "nlohmann/json_fwd.hpp"
|
||||
#include "cli/service/ai/common.h"
|
||||
#include "cli/service/resources/resource_context_builder.h"
|
||||
#include "app/rom.h"
|
||||
@@ -26,6 +28,20 @@ struct FewShotExample {
|
||||
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
|
||||
struct RomContext {
|
||||
std::string rom_path;
|
||||
@@ -65,22 +81,34 @@ class PromptBuilder {
|
||||
// Get few-shot examples for specific category
|
||||
std::vector<FewShotExample> GetExamplesForCategory(
|
||||
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)
|
||||
void SetVerbosity(int level) { verbosity_ = level; }
|
||||
|
||||
private:
|
||||
std::string BuildCommandReference();
|
||||
std::string BuildFewShotExamplesSection();
|
||||
std::string BuildCommandReference() const;
|
||||
std::string BuildFewShotExamplesSection() const;
|
||||
std::string BuildToolReference() const;
|
||||
std::string BuildContextSection(const RomContext& context);
|
||||
std::string BuildConstraintsSection();
|
||||
|
||||
void LoadDefaultExamples();
|
||||
std::string BuildConstraintsSection() const;
|
||||
std::string BuildTileReferenceSection() const;
|
||||
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;
|
||||
std::unique_ptr<ResourceContextBuilder> resource_context_builder_;
|
||||
std::map<std::string, std::string> command_docs_; // Command name -> docs
|
||||
std::vector<FewShotExample> examples_;
|
||||
std::vector<ToolSpecification> tool_specs_;
|
||||
std::map<std::string, std::string> tile_reference_;
|
||||
int verbosity_ = 1;
|
||||
bool catalogue_loaded_ = false;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,39 @@ if(NOT ftxui_POPULATED)
|
||||
add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
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
|
||||
if(APPLE)
|
||||
set(FILE_DIALOG_SRC
|
||||
@@ -128,6 +161,7 @@ target_link_libraries(
|
||||
z3ed PUBLIC
|
||||
asar-static
|
||||
yaze_agent
|
||||
yaml-cpp
|
||||
ftxui::component
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
|
||||
Reference in New Issue
Block a user