10#include "absl/strings/ascii.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_join.h"
15#include "nlohmann/json.hpp"
19#ifdef YAZE_HAS_YAML_CPP
20#include "yaml-cpp/yaml.h"
28#ifdef YAZE_HAS_YAML_CPP
29bool IsYamlBool(
const std::string& value) {
30 const std::string lower = absl::AsciiStrToLower(value);
31 return lower ==
"true" || lower ==
"false" || lower ==
"yes" ||
32 lower ==
"no" || lower ==
"on" || lower ==
"off";
35nlohmann::json YamlToJson(
const YAML::Node& node) {
37 return nlohmann::json();
40 switch (node.Type()) {
41 case YAML::NodeType::Scalar: {
42 const std::string scalar = node.as<std::string>(
"");
44 if (IsYamlBool(scalar)) {
45 return node.as<
bool>();
48 if (scalar ==
"~" || absl::AsciiStrToLower(scalar) ==
"null") {
49 return nlohmann::json();
54 case YAML::NodeType::Sequence: {
55 nlohmann::json array = nlohmann::json::array();
56 for (
const auto& item : node) {
57 array.push_back(YamlToJson(item));
61 case YAML::NodeType::Map: {
62 nlohmann::json
object = nlohmann::json::object();
63 for (
const auto& kv : node) {
64 object[kv.first.as<std::string>()] = YamlToJson(kv.second);
69 return nlohmann::json();
87 const std::string& yaml_path)
const {
89 if (!yaml_path.empty()) {
91 if (std::filesystem::exists(yaml_path, ec) && !ec) {
97 if (
const char* env_path = std::getenv(
"YAZE_AGENT_CATALOGUE")) {
98 if (*env_path !=
'\0') {
100 if (std::filesystem::exists(env_path, ec) && !ec) {
101 return std::string(env_path);
108 std::string relative_path =
109 yaml_path.empty() ?
"agent/prompt_catalogue.yaml" : yaml_path;
110 constexpr std::string_view kAssetsPrefix =
"assets/";
111 if (relative_path.compare(0, kAssetsPrefix.size(), kAssetsPrefix) == 0) {
112 relative_path.erase(0, kAssetsPrefix.size());
117 return result->string();
120 return result.status();
124 const std::string& yaml_path) {
125#if !defined(YAZE_WITH_JSON) || !defined(YAZE_HAS_YAML_CPP)
129 <<
"⚠️ PromptBuilder requires JSON and yaml-cpp support for catalogue "
131 <<
" Build with -DYAZE_WITH_JSON=ON (mac-ai preset already does) and "
133 <<
" AI features will use basic prompts without tool definitions\n";
134 return absl::OkStatus();
137 if (!resolved_or.ok()) {
139 return resolved_or.status();
142 const std::string& resolved_path = resolved_or.value();
146 root = YAML::LoadFile(resolved_path);
147 }
catch (
const YAML::BadFile& e) {
149 return absl::NotFoundError(absl::StrCat(
150 "Unable to open prompt catalogue at ", resolved_path,
": ", e.what()));
151 }
catch (
const YAML::ParserException& e) {
153 return absl::InvalidArgumentError(absl::StrCat(
154 "Failed to parse prompt catalogue at ", resolved_path,
": ", e.what()));
157 nlohmann::json catalogue = YamlToJson(root);
160 if (catalogue.contains(
"commands")) {
161 if (
auto status =
ParseCommands(catalogue[
"commands"]); !status.ok()) {
166 if (catalogue.contains(
"tools")) {
167 if (
auto status =
ParseTools(catalogue[
"tools"]); !status.ok()) {
172 if (catalogue.contains(
"examples")) {
173 if (
auto status =
ParseExamples(catalogue[
"examples"]); !status.ok()) {
178 if (catalogue.contains(
"tile16_reference")) {
183 return absl::OkStatus();
188 if (!commands.is_object()) {
189 return absl::InvalidArgumentError(
190 "commands section must be an object mapping command names to docs");
193 for (
const auto& [name, value] : commands.items()) {
194 if (!value.is_string()) {
195 return absl::InvalidArgumentError(
196 absl::StrCat(
"Command entry for ", name,
" must be a string"));
201 return absl::OkStatus();
205 if (!tools.is_array()) {
206 return absl::InvalidArgumentError(
"tools section must be an array");
209 for (
const auto& tool_json : tools) {
210 if (!tool_json.is_object()) {
211 return absl::InvalidArgumentError(
212 "Each tool entry must be an object with name/description");
216 if (tool_json.contains(
"name") && tool_json[
"name"].is_string()) {
217 spec.
name = tool_json[
"name"].get<std::string>();
219 return absl::InvalidArgumentError(
"Tool entry missing name");
222 if (tool_json.contains(
"description") &&
223 tool_json[
"description"].is_string()) {
224 spec.
description = tool_json[
"description"].get<std::string>();
227 if (tool_json.contains(
"usage_notes") &&
228 tool_json[
"usage_notes"].is_string()) {
229 spec.
usage_notes = tool_json[
"usage_notes"].get<std::string>();
232 if (tool_json.contains(
"arguments")) {
233 const auto& args = tool_json[
"arguments"];
234 if (!args.is_array()) {
235 return absl::InvalidArgumentError(absl::StrCat(
236 "Tool arguments for ", spec.
name,
" must be an array"));
238 for (
const auto& arg_json : args) {
239 if (!arg_json.is_object()) {
240 return absl::InvalidArgumentError(absl::StrCat(
241 "Argument entries for ", spec.
name,
" must be objects"));
244 if (arg_json.contains(
"name") && arg_json[
"name"].is_string()) {
245 arg.
name = arg_json[
"name"].get<std::string>();
247 return absl::InvalidArgumentError(absl::StrCat(
248 "Argument entry for ", spec.
name,
" is missing a name"));
250 if (arg_json.contains(
"description") &&
251 arg_json[
"description"].is_string()) {
252 arg.
description = arg_json[
"description"].get<std::string>();
254 if (arg_json.contains(
"required")) {
255 if (!arg_json[
"required"].is_boolean()) {
256 return absl::InvalidArgumentError(
257 absl::StrCat(
"Argument 'required' flag for ", spec.
name,
258 "::", arg.
name,
" must be boolean"));
260 arg.
required = arg_json[
"required"].get<
bool>();
262 if (arg_json.contains(
"example") && arg_json[
"example"].is_string()) {
263 arg.
example = arg_json[
"example"].get<std::string>();
265 spec.
arguments.push_back(std::move(arg));
272 return absl::OkStatus();
276 if (!examples.is_array()) {
277 return absl::InvalidArgumentError(
"examples section must be an array");
280 for (
const auto& example_json : examples) {
281 if (!example_json.is_object()) {
282 return absl::InvalidArgumentError(
"Each example entry must be an object");
286 if (example_json.contains(
"user_prompt") &&
287 example_json[
"user_prompt"].is_string()) {
288 example.
user_prompt = example_json[
"user_prompt"].get<std::string>();
290 return absl::InvalidArgumentError(
"Example missing user_prompt");
293 if (example_json.contains(
"text_response") &&
294 example_json[
"text_response"].is_string()) {
295 example.
text_response = example_json[
"text_response"].get<std::string>();
298 if (example_json.contains(
"reasoning") &&
299 example_json[
"reasoning"].is_string()) {
300 example.
explanation = example_json[
"reasoning"].get<std::string>();
303 if (example_json.contains(
"commands")) {
304 const auto& commands = example_json[
"commands"];
305 if (!commands.is_array()) {
306 return absl::InvalidArgumentError(absl::StrCat(
307 "Example commands for ", example.
user_prompt,
" must be an array"));
309 for (
const auto& cmd : commands) {
310 if (!cmd.is_string()) {
311 return absl::InvalidArgumentError(absl::StrCat(
312 "Command entries for ", example.
user_prompt,
" must be strings"));
318 if (example_json.contains(
"tool_calls")) {
319 const auto& calls = example_json[
"tool_calls"];
320 if (!calls.is_array()) {
321 return absl::InvalidArgumentError(absl::StrCat(
322 "Tool calls for ", example.
user_prompt,
" must be an array"));
324 for (
const auto& call_json : calls) {
325 if (!call_json.is_object()) {
326 return absl::InvalidArgumentError(
327 absl::StrCat(
"Tool call entries for ", example.
user_prompt,
328 " must be objects"));
331 if (call_json.contains(
"tool_name") &&
332 call_json[
"tool_name"].is_string()) {
333 call.
tool_name = call_json[
"tool_name"].get<std::string>();
335 return absl::InvalidArgumentError(absl::StrCat(
336 "Tool call missing tool_name in example: ", example.
user_prompt));
338 if (call_json.contains(
"args")) {
339 const auto& args = call_json[
"args"];
340 if (!args.is_object()) {
341 return absl::InvalidArgumentError(
342 absl::StrCat(
"Tool call args for ", example.
user_prompt,
343 " must be an object"));
345 for (
const auto& [key, value] : args.items()) {
346 if (!value.is_string()) {
347 return absl::InvalidArgumentError(
348 absl::StrCat(
"Tool call arg value for ", example.
user_prompt,
349 " must be a string"));
351 call.
args[key] = value.get<std::string>();
354 example.
tool_calls.push_back(std::move(call));
359 example_json.value(
"explanation", example.
explanation);
363 return absl::OkStatus();
372 if (value.is_string()) {
387 std::ostringstream oss;
389 oss <<
"# Available z3ed Commands\n\n";
392 oss <<
"## " << cmd <<
"\n";
393 oss << docs <<
"\n\n";
404 std::ostringstream oss;
405 oss <<
"# Available Agent Tools\n\n";
408 oss <<
"## " << spec.name <<
"\n";
409 if (!spec.description.empty()) {
410 oss << spec.description <<
"\n\n";
413 if (!spec.arguments.empty()) {
414 oss <<
"| Argument | Required | Description | Example |\n";
415 oss <<
"| --- | --- | --- | --- |\n";
416 for (
const auto& arg : spec.arguments) {
417 oss <<
"| `" << arg.name <<
"` | " << (arg.required ?
"yes" :
"no")
418 <<
" | " << arg.description <<
" | "
419 << (arg.example.empty() ?
"" :
"`" + arg.example +
"`") <<
" |\n";
424 if (!spec.usage_notes.empty()) {
425 oss <<
"_Usage:_ " << spec.usage_notes <<
"\n\n";
441 std::ostringstream oss;
443 oss <<
"# Example Command Sequences\n\n";
444 oss <<
"Here are proven examples of how to accomplish common tasks:\n\n";
447 oss <<
"**User Request:** \"" << example.user_prompt <<
"\"\n";
448 oss <<
"**Structured Response:**\n";
450 nlohmann::json example_json = nlohmann::json::object();
451 if (!example.text_response.empty()) {
452 example_json[
"text_response"] = example.text_response;
454 if (!example.expected_commands.empty()) {
455 example_json[
"commands"] = example.expected_commands;
457 if (!example.explanation.empty()) {
458 example_json[
"reasoning"] = example.explanation;
460 if (!example.tool_calls.empty()) {
461 nlohmann::json calls = nlohmann::json::array();
462 for (
const auto& call : example.tool_calls) {
463 nlohmann::json call_json;
464 call_json[
"tool_name"] = call.tool_name;
465 nlohmann::json args = nlohmann::json::object();
466 for (
const auto& [key, value] : call.args) {
469 call_json[
"args"] = std::move(args);
470 calls.push_back(std::move(call_json));
472 example_json[
"tool_calls"] = std::move(calls);
475 oss <<
"```json\n" << example_json.dump(2) <<
"\n```\n\n";
485 if (file_path.ok()) {
486 std::ifstream file(file_path->string());
487 if (file.is_open()) {
488 std::string content((std::istreambuf_iterator<char>(file)),
489 std::istreambuf_iterator<char>());
490 if (!content.empty()) {
491 std::ostringstream oss;
496 oss <<
"\n\n# Available Tools for ROM Inspection\n\n";
497 oss <<
"You have access to the following tools to answer "
502 oss <<
"**Tool Call Example (Initial Request):**\n";
507 "tool_name": "resource-list",
513 "reasoning": "I need to call the resource-list tool to get the dungeon information."
516 oss <<
"**Tool Result Response (After Tool Executes):**\n";
519 "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.",
520 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
535 std::ostringstream oss;
537# Critical Constraints
5391. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
541 "text_response": "Your natural language reply to the user.",
542 "tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
543 "commands": ["command1", "command2"],
544 "reasoning": "Your thought process."
546 - `text_response` is for conversational replies.
547 - `tool_calls` is for asking questions about the ROM. Use the available tools listed below.
548 - `commands` is for generating commands to modify the ROM.
549 - All fields are optional, but you should always provide at least one.
5512. **Tool Calling Workflow (CRITICAL):**
552 WHEN YOU CALL A TOOL:
553 a) First response: Include tool_calls with the tool name and arguments
554 b) The tool will execute and you'll receive results in the next message
555 c) Second response: You MUST provide a text_response that answers the user's question using the tool results
556 d) DO NOT call the same tool again unless you need different parameters
557 e) DO NOT leave text_response empty after receiving tool results
559 Example conversation flow:
560 User: "What dungeons are in this ROM?"
561 You (first): {"tool_calls": [{"tool_name": "resource-list", "args": {"type": "dungeon"}}]}
562 [Tool executes and returns: {"dungeons": ["Hyrule Castle", "Eastern Palace", ...]}]
563 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."}
5653. **Tool Usage:** When the user asks a question about the ROM state, use tool_calls instead of commands
566 - Tools are read-only and return information
567 - Commands modify the ROM and should only be used when explicitly requested
568 - You can call multiple tools in one response
569 - Always use JSON format for tool results
570 - ALWAYS provide text_response after receiving tool results
5724. **Command Syntax:** Follow the exact syntax shown in examples
573 - Use correct flag names (--group, --id, --to, --from, etc.)
574 - Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
575 - Coordinates are 0-based indices
5775. **Common Patterns:**
578 - Palette modifications: export → set-color → import
579 - Multiple tile placement: multiple overworld set-tile commands
580 - Validation: single rom validate command
5826. **Error Prevention:**
583 - Always export before modifying palettes
584 - Use temporary file names (temp_*.json) for intermediate files
585 - Validate coordinates are within bounds
589 oss <<
"\n# Available Tools for ROM Inspection\n\n";
590 oss <<
"You have access to the following tools to answer questions:\n\n";
594 oss <<
"**Tool Call Example (Initial Request):**\n";
599 "tool_name": "resource-list",
605 "reasoning": "I need to call the resource-list tool to get the dungeon information."
608 oss <<
"**Tool Result Response (After Tool Executes):**\n";
611 "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.",
612 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
625 std::ostringstream oss;
626 oss <<
"# Tile16 Reference (ALTTP)\n\n";
629 oss <<
"- " << alias <<
": " << value <<
"\n";
637 std::ostringstream oss;
639 oss <<
"# Current ROM Context\n\n";
645 std::make_unique<ResourceContextBuilder>(
rom_);
647 auto resource_context_or =
649 if (resource_context_or.ok()) {
650 oss << resource_context_or.value();
654 if (context.rom_loaded) {
655 oss <<
"- **ROM Loaded:** Yes (" << context.rom_path <<
")\n";
657 oss <<
"- **ROM Loaded:** No\n";
660 if (!context.current_editor.empty()) {
661 oss <<
"- **Active Editor:** " << context.current_editor <<
"\n";
664 if (!context.editor_state.empty()) {
665 oss <<
"- **Editor State:**\n";
666 for (
const auto& [key, value] : context.editor_state) {
667 oss <<
" - " << key <<
": " << value <<
"\n";
678 if (file_path.ok()) {
679 std::ifstream file(file_path->string());
680 if (file.is_open()) {
681 std::string content((std::istreambuf_iterator<char>(file)),
682 std::istreambuf_iterator<char>());
683 if (!content.empty()) {
684 std::ostringstream oss;
703 std::ostringstream oss;
705 oss <<
"You are an expert ROM hacking assistant for The Legend of Zelda: "
706 <<
"A Link to the Past (ALTTP).\n\n";
708 oss <<
"Your task is to generate a sequence of z3ed CLI commands to achieve "
709 <<
"the user's request.\n\n";
722 oss <<
"\n**Response Format:**\n";
724 oss <<
"[\"command1 --flag value\", \"command2 --flag value\"]\n";
731 std::ostringstream oss;
741 const RomContext& context) {
742 std::ostringstream oss;
744 if (context.rom_loaded || !context.current_editor.empty()) {
749 oss <<
"**User Request:** " << user_prompt <<
"\n\n";
750 oss <<
"Generate the appropriate z3ed commands as a JSON array.";
756 const std::vector<agent::ChatMessage>& history) {
757 std::ostringstream oss;
758 oss <<
"This is a conversation between a user and an expert ROM hacking "
761 for (
const auto& msg : history) {
763 oss <<
"User: " << msg.message <<
"\n";
765 oss <<
"Agent: " << msg.message <<
"\n";
768 oss <<
"\nBased on this conversation, provide a response in the required "
779 const std::string& category) {
780 std::vector<FewShotExample> result;
784 if (category ==
"palette" &&
785 (example.user_prompt.find(
"palette") != std::string::npos ||
786 example.user_prompt.find(
"color") != std::string::npos)) {
787 result.push_back(example);
788 }
else if (category ==
"overworld" &&
789 (example.user_prompt.find(
"place") != std::string::npos ||
790 example.user_prompt.find(
"tree") != std::string::npos ||
791 example.user_prompt.find(
"house") != std::string::npos)) {
792 result.push_back(example);
793 }
else if (category ==
"validation" &&
794 example.user_prompt.find(
"validate") != std::string::npos) {
795 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