9#include "absl/strings/ascii.h"
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_join.h"
14#include "nlohmann/json.hpp"
18#ifdef YAZE_HAS_YAML_CPP
19#include "yaml-cpp/yaml.h"
27#ifdef YAZE_HAS_YAML_CPP
28bool IsYamlBool(
const std::string& value) {
29 const std::string lower = absl::AsciiStrToLower(value);
30 return lower ==
"true" || lower ==
"false" || lower ==
"yes" ||
31 lower ==
"no" || lower ==
"on" || lower ==
"off";
34nlohmann::json YamlToJson(
const YAML::Node& node) {
36 return nlohmann::json();
39 switch (node.Type()) {
40 case YAML::NodeType::Scalar: {
41 const std::string scalar = node.as<std::string>(
"");
43 if (IsYamlBool(scalar)) {
44 return node.as<
bool>();
47 if (scalar ==
"~" || absl::AsciiStrToLower(scalar) ==
"null") {
48 return nlohmann::json();
53 case YAML::NodeType::Sequence: {
54 nlohmann::json array = nlohmann::json::array();
55 for (
const auto& item : node) {
56 array.push_back(YamlToJson(item));
60 case YAML::NodeType::Map: {
61 nlohmann::json
object = nlohmann::json::object();
62 for (
const auto& kv : node) {
63 object[kv.first.as<std::string>()] = YamlToJson(kv.second);
68 return nlohmann::json();
86 const std::string& yaml_path)
const {
88 if (!yaml_path.empty()) {
90 if (std::filesystem::exists(yaml_path, ec) && !ec) {
96 if (
const char* env_path = std::getenv(
"YAZE_AGENT_CATALOGUE")) {
97 if (*env_path !=
'\0') {
99 if (std::filesystem::exists(env_path, ec) && !ec) {
100 return std::string(env_path);
107 std::string relative_path =
108 yaml_path.empty() ?
"agent/prompt_catalogue.yaml" : yaml_path;
112 return result->string();
115 return result.status();
119 const std::string& yaml_path) {
120#if !defined(YAZE_WITH_JSON) || !defined(YAZE_HAS_YAML_CPP)
124 <<
"⚠️ PromptBuilder requires JSON and yaml-cpp support for catalogue "
126 <<
" Build with -DYAZE_WITH_JSON=ON (mac-ai preset already does) and "
128 <<
" AI features will use basic prompts without tool definitions\n";
129 return absl::OkStatus();
132 if (!resolved_or.ok()) {
134 return resolved_or.status();
137 const std::string& resolved_path = resolved_or.value();
141 root = YAML::LoadFile(resolved_path);
142 }
catch (
const YAML::BadFile& e) {
144 return absl::NotFoundError(absl::StrCat(
145 "Unable to open prompt catalogue at ", resolved_path,
": ", e.what()));
146 }
catch (
const YAML::ParserException& e) {
148 return absl::InvalidArgumentError(absl::StrCat(
149 "Failed to parse prompt catalogue at ", resolved_path,
": ", e.what()));
152 nlohmann::json catalogue = YamlToJson(root);
155 if (catalogue.contains(
"commands")) {
156 if (
auto status =
ParseCommands(catalogue[
"commands"]); !status.ok()) {
161 if (catalogue.contains(
"tools")) {
162 if (
auto status =
ParseTools(catalogue[
"tools"]); !status.ok()) {
167 if (catalogue.contains(
"examples")) {
168 if (
auto status =
ParseExamples(catalogue[
"examples"]); !status.ok()) {
173 if (catalogue.contains(
"tile16_reference")) {
178 return absl::OkStatus();
183 if (!commands.is_object()) {
184 return absl::InvalidArgumentError(
185 "commands section must be an object mapping command names to docs");
188 for (
const auto& [name, value] : commands.items()) {
189 if (!value.is_string()) {
190 return absl::InvalidArgumentError(
191 absl::StrCat(
"Command entry for ", name,
" must be a string"));
196 return absl::OkStatus();
200 if (!tools.is_array()) {
201 return absl::InvalidArgumentError(
"tools section must be an array");
204 for (
const auto& tool_json : tools) {
205 if (!tool_json.is_object()) {
206 return absl::InvalidArgumentError(
207 "Each tool entry must be an object with name/description");
211 if (tool_json.contains(
"name") && tool_json[
"name"].is_string()) {
212 spec.
name = tool_json[
"name"].get<std::string>();
214 return absl::InvalidArgumentError(
"Tool entry missing name");
217 if (tool_json.contains(
"description") &&
218 tool_json[
"description"].is_string()) {
219 spec.
description = tool_json[
"description"].get<std::string>();
222 if (tool_json.contains(
"usage_notes") &&
223 tool_json[
"usage_notes"].is_string()) {
224 spec.
usage_notes = tool_json[
"usage_notes"].get<std::string>();
227 if (tool_json.contains(
"arguments")) {
228 const auto& args = tool_json[
"arguments"];
229 if (!args.is_array()) {
230 return absl::InvalidArgumentError(absl::StrCat(
231 "Tool arguments for ", spec.
name,
" must be an array"));
233 for (
const auto& arg_json : args) {
234 if (!arg_json.is_object()) {
235 return absl::InvalidArgumentError(absl::StrCat(
236 "Argument entries for ", spec.
name,
" must be objects"));
239 if (arg_json.contains(
"name") && arg_json[
"name"].is_string()) {
240 arg.
name = arg_json[
"name"].get<std::string>();
242 return absl::InvalidArgumentError(absl::StrCat(
243 "Argument entry for ", spec.
name,
" is missing a name"));
245 if (arg_json.contains(
"description") &&
246 arg_json[
"description"].is_string()) {
247 arg.
description = arg_json[
"description"].get<std::string>();
249 if (arg_json.contains(
"required")) {
250 if (!arg_json[
"required"].is_boolean()) {
251 return absl::InvalidArgumentError(
252 absl::StrCat(
"Argument 'required' flag for ", spec.
name,
253 "::", arg.
name,
" must be boolean"));
255 arg.
required = arg_json[
"required"].get<
bool>();
257 if (arg_json.contains(
"example") && arg_json[
"example"].is_string()) {
258 arg.
example = arg_json[
"example"].get<std::string>();
260 spec.
arguments.push_back(std::move(arg));
267 return absl::OkStatus();
271 if (!examples.is_array()) {
272 return absl::InvalidArgumentError(
"examples section must be an array");
275 for (
const auto& example_json : examples) {
276 if (!example_json.is_object()) {
277 return absl::InvalidArgumentError(
"Each example entry must be an object");
281 if (example_json.contains(
"user_prompt") &&
282 example_json[
"user_prompt"].is_string()) {
283 example.
user_prompt = example_json[
"user_prompt"].get<std::string>();
285 return absl::InvalidArgumentError(
"Example missing user_prompt");
288 if (example_json.contains(
"text_response") &&
289 example_json[
"text_response"].is_string()) {
290 example.
text_response = example_json[
"text_response"].get<std::string>();
293 if (example_json.contains(
"reasoning") &&
294 example_json[
"reasoning"].is_string()) {
295 example.
explanation = example_json[
"reasoning"].get<std::string>();
298 if (example_json.contains(
"commands")) {
299 const auto& commands = example_json[
"commands"];
300 if (!commands.is_array()) {
301 return absl::InvalidArgumentError(absl::StrCat(
302 "Example commands for ", example.
user_prompt,
" must be an array"));
304 for (
const auto& cmd : commands) {
305 if (!cmd.is_string()) {
306 return absl::InvalidArgumentError(absl::StrCat(
307 "Command entries for ", example.
user_prompt,
" must be strings"));
313 if (example_json.contains(
"tool_calls")) {
314 const auto& calls = example_json[
"tool_calls"];
315 if (!calls.is_array()) {
316 return absl::InvalidArgumentError(absl::StrCat(
317 "Tool calls for ", example.
user_prompt,
" must be an array"));
319 for (
const auto& call_json : calls) {
320 if (!call_json.is_object()) {
321 return absl::InvalidArgumentError(
322 absl::StrCat(
"Tool call entries for ", example.
user_prompt,
323 " must be objects"));
326 if (call_json.contains(
"tool_name") &&
327 call_json[
"tool_name"].is_string()) {
328 call.
tool_name = call_json[
"tool_name"].get<std::string>();
330 return absl::InvalidArgumentError(absl::StrCat(
331 "Tool call missing tool_name in example: ", example.
user_prompt));
333 if (call_json.contains(
"args")) {
334 const auto& args = call_json[
"args"];
335 if (!args.is_object()) {
336 return absl::InvalidArgumentError(
337 absl::StrCat(
"Tool call args for ", example.
user_prompt,
338 " must be an object"));
340 for (
const auto& [key, value] : args.items()) {
341 if (!value.is_string()) {
342 return absl::InvalidArgumentError(
343 absl::StrCat(
"Tool call arg value for ", example.
user_prompt,
344 " must be a string"));
346 call.
args[key] = value.get<std::string>();
349 example.
tool_calls.push_back(std::move(call));
354 example_json.value(
"explanation", example.
explanation);
358 return absl::OkStatus();
367 if (value.is_string()) {
382 std::ostringstream oss;
384 oss <<
"# Available z3ed Commands\n\n";
387 oss <<
"## " << cmd <<
"\n";
388 oss << docs <<
"\n\n";
399 std::ostringstream oss;
400 oss <<
"# Available Agent Tools\n\n";
403 oss <<
"## " << spec.name <<
"\n";
404 if (!spec.description.empty()) {
405 oss << spec.description <<
"\n\n";
408 if (!spec.arguments.empty()) {
409 oss <<
"| Argument | Required | Description | Example |\n";
410 oss <<
"| --- | --- | --- | --- |\n";
411 for (
const auto& arg : spec.arguments) {
412 oss <<
"| `" << arg.name <<
"` | " << (arg.required ?
"yes" :
"no")
413 <<
" | " << arg.description <<
" | "
414 << (arg.example.empty() ?
"" :
"`" + arg.example +
"`") <<
" |\n";
419 if (!spec.usage_notes.empty()) {
420 oss <<
"_Usage:_ " << spec.usage_notes <<
"\n\n";
436 std::ostringstream oss;
438 oss <<
"# Example Command Sequences\n\n";
439 oss <<
"Here are proven examples of how to accomplish common tasks:\n\n";
442 oss <<
"**User Request:** \"" << example.user_prompt <<
"\"\n";
443 oss <<
"**Structured Response:**\n";
445 nlohmann::json example_json = nlohmann::json::object();
446 if (!example.text_response.empty()) {
447 example_json[
"text_response"] = example.text_response;
449 if (!example.expected_commands.empty()) {
450 example_json[
"commands"] = example.expected_commands;
452 if (!example.explanation.empty()) {
453 example_json[
"reasoning"] = example.explanation;
455 if (!example.tool_calls.empty()) {
456 nlohmann::json calls = nlohmann::json::array();
457 for (
const auto& call : example.tool_calls) {
458 nlohmann::json call_json;
459 call_json[
"tool_name"] = call.tool_name;
460 nlohmann::json args = nlohmann::json::object();
461 for (
const auto& [key, value] : call.args) {
464 call_json[
"args"] = std::move(args);
465 calls.push_back(std::move(call_json));
467 example_json[
"tool_calls"] = std::move(calls);
470 oss <<
"```json\n" << example_json.dump(2) <<
"\n```\n\n";
480 if (file_path.ok()) {
481 std::ifstream file(file_path->string());
482 if (file.is_open()) {
483 std::string content((std::istreambuf_iterator<char>(file)),
484 std::istreambuf_iterator<char>());
485 if (!content.empty()) {
486 std::ostringstream oss;
491 oss <<
"\n\n# Available Tools for ROM Inspection\n\n";
492 oss <<
"You have access to the following tools to answer "
497 oss <<
"**Tool Call Example (Initial Request):**\n";
502 "tool_name": "resource-list",
508 "reasoning": "I need to call the resource-list tool to get the dungeon information."
511 oss <<
"**Tool Result Response (After Tool Executes):**\n";
514 "text_response": "I found the following dungeons in the ROM: Hyrule Castle, Eastern Palace, Desert Palace, Tower of Hera, Palace of Darkness, Swamp Palace, Skull Woods, Thieves' Town, Ice Palace, Misery Mire, Turtle Rock, and Ganon's Tower.",
515 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
530 std::ostringstream oss;
532# Critical Constraints
5341. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
536 "text_response": "Your natural language reply to the user.",
537 "tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
538 "commands": ["command1", "command2"],
539 "reasoning": "Your thought process."
541 - `text_response` is for conversational replies.
542 - `tool_calls` is for asking questions about the ROM. Use the available tools listed below.
543 - `commands` is for generating commands to modify the ROM.
544 - All fields are optional, but you should always provide at least one.
5462. **Tool Calling Workflow (CRITICAL):**
547 WHEN YOU CALL A TOOL:
548 a) First response: Include tool_calls with the tool name and arguments
549 b) The tool will execute and you'll receive results in the next message
550 c) Second response: You MUST provide a text_response that answers the user's question using the tool results
551 d) DO NOT call the same tool again unless you need different parameters
552 e) DO NOT leave text_response empty after receiving tool results
554 Example conversation flow:
555 User: "What dungeons are in this ROM?"
556 You (first): {"tool_calls": [{"tool_name": "resource-list", "args": {"type": "dungeon"}}]}
557 [Tool executes and returns: {"dungeons": ["Hyrule Castle", "Eastern Palace", ...]}]
558 You (second): {"text_response": "Based on the ROM data, there are 12 dungeons including Hyrule Castle, Eastern Palace, Desert Palace, Tower of Hera, and more."}
5603. **Tool Usage:** When the user asks a question about the ROM state, use tool_calls instead of commands
561 - Tools are read-only and return information
562 - Commands modify the ROM and should only be used when explicitly requested
563 - You can call multiple tools in one response
564 - Always use JSON format for tool results
565 - ALWAYS provide text_response after receiving tool results
5674. **Command Syntax:** Follow the exact syntax shown in examples
568 - Use correct flag names (--group, --id, --to, --from, etc.)
569 - Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
570 - Coordinates are 0-based indices
5725. **Common Patterns:**
573 - Palette modifications: export → set-color → import
574 - Multiple tile placement: multiple overworld set-tile commands
575 - Validation: single rom validate command
5776. **Error Prevention:**
578 - Always export before modifying palettes
579 - Use temporary file names (temp_*.json) for intermediate files
580 - Validate coordinates are within bounds
584 oss <<
"\n# Available Tools for ROM Inspection\n\n";
585 oss <<
"You have access to the following tools to answer questions:\n\n";
589 oss <<
"**Tool Call Example (Initial Request):**\n";
594 "tool_name": "resource-list",
600 "reasoning": "I need to call the resource-list tool to get the dungeon information."
603 oss <<
"**Tool Result Response (After Tool Executes):**\n";
606 "text_response": "I found the following dungeons in the ROM: Hyrule Castle, Eastern Palace, Desert Palace, Tower of Hera, Palace of Darkness, Swamp Palace, Skull Woods, Thieves' Town, Ice Palace, Misery Mire, Turtle Rock, and Ganon's Tower.",
607 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
620 std::ostringstream oss;
621 oss <<
"# Tile16 Reference (ALTTP)\n\n";
624 oss <<
"- " << alias <<
": " << value <<
"\n";
632 std::ostringstream oss;
634 oss <<
"# Current ROM Context\n\n";
640 std::make_unique<ResourceContextBuilder>(
rom_);
642 auto resource_context_or =
644 if (resource_context_or.ok()) {
645 oss << resource_context_or.value();
649 if (context.rom_loaded) {
650 oss <<
"- **ROM Loaded:** Yes (" << context.rom_path <<
")\n";
652 oss <<
"- **ROM Loaded:** No\n";
655 if (!context.current_editor.empty()) {
656 oss <<
"- **Active Editor:** " << context.current_editor <<
"\n";
659 if (!context.editor_state.empty()) {
660 oss <<
"- **Editor State:**\n";
661 for (
const auto& [key, value] : context.editor_state) {
662 oss <<
" - " << key <<
": " << value <<
"\n";
673 if (file_path.ok()) {
674 std::ifstream file(file_path->string());
675 if (file.is_open()) {
676 std::string content((std::istreambuf_iterator<char>(file)),
677 std::istreambuf_iterator<char>());
678 if (!content.empty()) {
679 std::ostringstream oss;
698 std::ostringstream oss;
700 oss <<
"You are an expert ROM hacking assistant for The Legend of Zelda: "
701 <<
"A Link to the Past (ALTTP).\n\n";
703 oss <<
"Your task is to generate a sequence of z3ed CLI commands to achieve "
704 <<
"the user's request.\n\n";
717 oss <<
"\n**Response Format:**\n";
719 oss <<
"[\"command1 --flag value\", \"command2 --flag value\"]\n";
726 std::ostringstream oss;
736 const RomContext& context) {
737 std::ostringstream oss;
739 if (context.rom_loaded || !context.current_editor.empty()) {
744 oss <<
"**User Request:** " << user_prompt <<
"\n\n";
745 oss <<
"Generate the appropriate z3ed commands as a JSON array.";
751 const std::vector<agent::ChatMessage>& history) {
752 std::ostringstream oss;
753 oss <<
"This is a conversation between a user and an expert ROM hacking "
756 for (
const auto& msg : history) {
758 oss <<
"User: " << msg.message <<
"\n";
760 oss <<
"Agent: " << msg.message <<
"\n";
763 oss <<
"\nBased on this conversation, provide a response in the required "
774 const std::string& category) {
775 std::vector<FewShotExample> result;
779 if (category ==
"palette" &&
780 (example.user_prompt.find(
"palette") != std::string::npos ||
781 example.user_prompt.find(
"color") != std::string::npos)) {
782 result.push_back(example);
783 }
else if (category ==
"overworld" &&
784 (example.user_prompt.find(
"place") != std::string::npos ||
785 example.user_prompt.find(
"tree") != std::string::npos ||
786 example.user_prompt.find(
"house") != std::string::npos)) {
787 result.push_back(example);
788 }
else if (category ==
"validation" &&
789 example.user_prompt.find(
"validate") != std::string::npos) {
790 result.push_back(example);
std::string BuildContextualPrompt(const std::string &user_prompt, const RomContext &context)
std::vector< FewShotExample > examples_
std::map< std::string, std::string > command_docs_
absl::Status ParseTools(const nlohmann::json &tools)
std::map< std::string, std::string > tile_reference_
std::unique_ptr< ResourceContextBuilder > resource_context_builder_
const std::map< std::string, std::string > & tile_reference() const
std::string BuildConstraintsSection() const
std::string BuildTileReferenceSection() const
void AddFewShotExample(const FewShotExample &example)
std::string BuildFunctionCallSchemas() const
std::string BuildSystemInstructionWithExamples()
std::string BuildPromptFromHistory(const std::vector< agent::ChatMessage > &history)
std::string BuildToolReference() const
std::string BuildContextSection(const RomContext &context)
void ParseTileReference(const nlohmann::json &tile_reference)
std::string BuildSystemInstruction()
std::string LookupTileId(const std::string &alias) const
absl::Status ParseExamples(const nlohmann::json &examples)
std::string BuildFewShotExamplesSection() const
std::vector< ToolSpecification > tool_specs_
std::string BuildCommandReference() const
absl::StatusOr< std::string > ResolveCataloguePath(const std::string &yaml_path) const
absl::Status LoadResourceCatalogue(const std::string &yaml_path)
absl::Status ParseCommands(const nlohmann::json &commands)
std::vector< FewShotExample > GetExamplesForCategory(const std::string &category)
std::string text_response
std::vector< ToolCall > tool_calls
std::vector< std::string > expected_commands