10#include "absl/strings/ascii.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_join.h"
13#include "nlohmann/json.hpp"
15#include "yaml-cpp/yaml.h"
23 const std::string lower = absl::AsciiStrToLower(value);
24 return lower ==
"true" || lower ==
"false" || lower ==
"yes" ||
25 lower ==
"no" || lower ==
"on" || lower ==
"off";
30 return nlohmann::json();
33 switch (node.Type()) {
34 case YAML::NodeType::Scalar: {
35 const std::string scalar = node.as<std::string>(
"");
38 return node.as<
bool>();
41 if (scalar ==
"~" || absl::AsciiStrToLower(scalar) ==
"null") {
42 return nlohmann::json();
47 case YAML::NodeType::Sequence: {
48 nlohmann::json array = nlohmann::json::array();
49 for (
const auto& item : node) {
54 case YAML::NodeType::Map: {
55 nlohmann::json
object = nlohmann::json::object();
56 for (
const auto& kv : node) {
57 object[kv.first.as<std::string>()] =
YamlToJson(kv.second);
62 return nlohmann::json();
79 const std::string& yaml_path)
const {
81 if (!yaml_path.empty()) {
83 if (std::filesystem::exists(yaml_path, ec) && !ec) {
89 if (
const char* env_path = std::getenv(
"YAZE_AGENT_CATALOGUE")) {
90 if (*env_path !=
'\0') {
92 if (std::filesystem::exists(env_path, ec) && !ec) {
93 return std::string(env_path);
100 std::string relative_path = yaml_path.empty() ?
101 "agent/prompt_catalogue.yaml" : yaml_path;
105 return result->string();
108 return result.status();
112#ifndef YAZE_WITH_JSON
114 std::cerr <<
"⚠️ PromptBuilder requires JSON support for catalogue loading\n"
115 <<
" Build with -DZ3ED_AI=ON or -DYAZE_WITH_JSON=ON\n"
116 <<
" AI features will use basic prompts without tool definitions\n";
117 return absl::OkStatus();
120 if (!resolved_or.ok()) {
122 return resolved_or.status();
125 const std::string& resolved_path = resolved_or.value();
129 root = YAML::LoadFile(resolved_path);
130 }
catch (
const YAML::BadFile& e) {
132 return absl::NotFoundError(
133 absl::StrCat(
"Unable to open prompt catalogue at ", resolved_path,
135 }
catch (
const YAML::ParserException& e) {
137 return absl::InvalidArgumentError(
138 absl::StrCat(
"Failed to parse prompt catalogue at ", resolved_path,
142 nlohmann::json catalogue = YamlToJson(root);
145 if (catalogue.contains(
"commands")) {
146 if (
auto status =
ParseCommands(catalogue[
"commands"]); !status.ok()) {
151 if (catalogue.contains(
"tools")) {
152 if (
auto status =
ParseTools(catalogue[
"tools"]); !status.ok()) {
157 if (catalogue.contains(
"examples")) {
158 if (
auto status =
ParseExamples(catalogue[
"examples"]); !status.ok()) {
163 if (catalogue.contains(
"tile16_reference")) {
168 return absl::OkStatus();
173 if (!commands.is_object()) {
174 return absl::InvalidArgumentError(
175 "commands section must be an object mapping command names to docs");
178 for (
const auto& [name, value] : commands.items()) {
179 if (!value.is_string()) {
180 return absl::InvalidArgumentError(
181 absl::StrCat(
"Command entry for ", name,
" must be a string"));
186 return absl::OkStatus();
190 if (!tools.is_array()) {
191 return absl::InvalidArgumentError(
"tools section must be an array");
194 for (
const auto& tool_json : tools) {
195 if (!tool_json.is_object()) {
196 return absl::InvalidArgumentError(
197 "Each tool entry must be an object with name/description");
201 if (tool_json.contains(
"name") && tool_json[
"name"].is_string()) {
202 spec.
name = tool_json[
"name"].get<std::string>();
204 return absl::InvalidArgumentError(
"Tool entry missing name");
207 if (tool_json.contains(
"description") && tool_json[
"description"].is_string()) {
208 spec.
description = tool_json[
"description"].get<std::string>();
211 if (tool_json.contains(
"usage_notes") && tool_json[
"usage_notes"].is_string()) {
212 spec.
usage_notes = tool_json[
"usage_notes"].get<std::string>();
215 if (tool_json.contains(
"arguments")) {
216 const auto& args = tool_json[
"arguments"];
217 if (!args.is_array()) {
218 return absl::InvalidArgumentError(
219 absl::StrCat(
"Tool arguments for ", spec.
name,
" must be an array"));
221 for (
const auto& arg_json : args) {
222 if (!arg_json.is_object()) {
223 return absl::InvalidArgumentError(
224 absl::StrCat(
"Argument entries for ", spec.
name,
225 " must be objects"));
228 if (arg_json.contains(
"name") && arg_json[
"name"].is_string()) {
229 arg.
name = arg_json[
"name"].get<std::string>();
231 return absl::InvalidArgumentError(
232 absl::StrCat(
"Argument entry for ", spec.
name,
233 " is missing a name"));
235 if (arg_json.contains(
"description") && arg_json[
"description"].is_string()) {
236 arg.
description = arg_json[
"description"].get<std::string>();
238 if (arg_json.contains(
"required")) {
239 if (!arg_json[
"required"].is_boolean()) {
240 return absl::InvalidArgumentError(
241 absl::StrCat(
"Argument 'required' flag for ", spec.
name,
242 "::", arg.
name,
" must be boolean"));
244 arg.
required = arg_json[
"required"].get<
bool>();
246 if (arg_json.contains(
"example") && arg_json[
"example"].is_string()) {
247 arg.
example = arg_json[
"example"].get<std::string>();
249 spec.
arguments.push_back(std::move(arg));
256 return absl::OkStatus();
260 if (!examples.is_array()) {
261 return absl::InvalidArgumentError(
"examples section must be an array");
264 for (
const auto& example_json : examples) {
265 if (!example_json.is_object()) {
266 return absl::InvalidArgumentError(
"Each example entry must be an object");
270 if (example_json.contains(
"user_prompt") &&
271 example_json[
"user_prompt"].is_string()) {
272 example.
user_prompt = example_json[
"user_prompt"].get<std::string>();
274 return absl::InvalidArgumentError(
"Example missing user_prompt");
277 if (example_json.contains(
"text_response") &&
278 example_json[
"text_response"].is_string()) {
279 example.
text_response = example_json[
"text_response"].get<std::string>();
282 if (example_json.contains(
"reasoning") &&
283 example_json[
"reasoning"].is_string()) {
284 example.
explanation = example_json[
"reasoning"].get<std::string>();
287 if (example_json.contains(
"commands")) {
288 const auto& commands = example_json[
"commands"];
289 if (!commands.is_array()) {
290 return absl::InvalidArgumentError(
291 absl::StrCat(
"Example commands for ", example.
user_prompt,
292 " must be an array"));
294 for (
const auto& cmd : commands) {
295 if (!cmd.is_string()) {
296 return absl::InvalidArgumentError(
297 absl::StrCat(
"Command entries for ", example.
user_prompt,
298 " must be strings"));
304 if (example_json.contains(
"tool_calls")) {
305 const auto& calls = example_json[
"tool_calls"];
306 if (!calls.is_array()) {
307 return absl::InvalidArgumentError(
308 absl::StrCat(
"Tool calls for ", example.
user_prompt,
309 " must be an array"));
311 for (
const auto& call_json : calls) {
312 if (!call_json.is_object()) {
313 return absl::InvalidArgumentError(
314 absl::StrCat(
"Tool call entries for ", example.
user_prompt,
315 " must be objects"));
318 if (call_json.contains(
"tool_name") && call_json[
"tool_name"].is_string()) {
319 call.
tool_name = call_json[
"tool_name"].get<std::string>();
321 return absl::InvalidArgumentError(
322 absl::StrCat(
"Tool call missing tool_name in example: ",
325 if (call_json.contains(
"args")) {
326 const auto& args = call_json[
"args"];
327 if (!args.is_object()) {
328 return absl::InvalidArgumentError(
329 absl::StrCat(
"Tool call args for ", example.
user_prompt,
330 " must be an object"));
332 for (
const auto& [key, value] : args.items()) {
333 if (!value.is_string()) {
334 return absl::InvalidArgumentError(
335 absl::StrCat(
"Tool call arg value for ", example.
user_prompt,
336 " must be a string"));
338 call.
args[key] = value.get<std::string>();
341 example.
tool_calls.push_back(std::move(call));
349 return absl::OkStatus();
358 if (value.is_string()) {
373 std::ostringstream oss;
375 oss <<
"# Available z3ed Commands\n\n";
378 oss <<
"## " << cmd <<
"\n";
379 oss << docs <<
"\n\n";
390 std::ostringstream oss;
391 oss <<
"# Available Agent Tools\n\n";
394 oss <<
"## " << spec.name <<
"\n";
395 if (!spec.description.empty()) {
396 oss << spec.description <<
"\n\n";
399 if (!spec.arguments.empty()) {
400 oss <<
"| Argument | Required | Description | Example |\n";
401 oss <<
"| --- | --- | --- | --- |\n";
402 for (
const auto& arg : spec.arguments) {
403 oss <<
"| `" << arg.name <<
"` | " << (arg.required ?
"yes" :
"no")
404 <<
" | " << arg.description <<
" | "
405 << (arg.example.empty() ?
"" :
"`" + arg.example +
"`")
411 if (!spec.usage_notes.empty()) {
412 oss <<
"_Usage:_ " << spec.usage_notes <<
"\n\n";
424 nlohmann::json tools_array = nlohmann::json::array();
428 tool[
"type"] =
"function";
430 nlohmann::json function;
431 function[
"name"] = spec.name;
432 function[
"description"] = spec.description;
433 if (!spec.usage_notes.empty()) {
434 function[
"description"] = spec.description +
" " + spec.usage_notes;
437 nlohmann::json parameters;
438 parameters[
"type"] =
"object";
440 nlohmann::json properties = nlohmann::json::object();
441 nlohmann::json required = nlohmann::json::array();
443 for (
const auto& arg : spec.arguments) {
444 nlohmann::json arg_schema;
445 arg_schema[
"type"] =
"string";
446 arg_schema[
"description"] = arg.description;
447 if (!arg.example.empty()) {
448 arg_schema[
"example"] = arg.example;
450 properties[arg.name] = arg_schema;
453 required.push_back(arg.name);
457 parameters[
"properties"] = properties;
458 if (!required.empty()) {
459 parameters[
"required"] = required;
462 function[
"parameters"] = parameters;
463 tool[
"function"] = function;
464 tools_array.push_back(tool);
467 return tools_array.dump(2);
471 std::ostringstream oss;
473 oss <<
"# Example Command Sequences\n\n";
474 oss <<
"Here are proven examples of how to accomplish common tasks:\n\n";
477 oss <<
"**User Request:** \"" << example.user_prompt <<
"\"\n";
478 oss <<
"**Structured Response:**\n";
480 nlohmann::json example_json = nlohmann::json::object();
481 if (!example.text_response.empty()) {
482 example_json[
"text_response"] = example.text_response;
484 if (!example.expected_commands.empty()) {
485 example_json[
"commands"] = example.expected_commands;
487 if (!example.explanation.empty()) {
488 example_json[
"reasoning"] = example.explanation;
490 if (!example.tool_calls.empty()) {
491 nlohmann::json calls = nlohmann::json::array();
492 for (
const auto& call : example.tool_calls) {
493 nlohmann::json call_json;
494 call_json[
"tool_name"] = call.tool_name;
495 nlohmann::json args = nlohmann::json::object();
496 for (
const auto& [key, value] : call.args) {
499 call_json[
"args"] = std::move(args);
500 calls.push_back(std::move(call_json));
502 example_json[
"tool_calls"] = std::move(calls);
505 oss <<
"```json\n" << example_json.dump(2) <<
"\n```\n\n";
514 if (file_path.ok()) {
515 std::ifstream file(file_path->string());
516 if (file.is_open()) {
517 std::string content((std::istreambuf_iterator<char>(file)),
518 std::istreambuf_iterator<char>());
519 if (!content.empty()) {
520 std::ostringstream oss;
525 oss <<
"\n\n# Available Tools for ROM Inspection\n\n";
526 oss <<
"You have access to the following tools to answer questions:\n\n";
530 oss <<
"**Tool Call Example (Initial Request):**\n";
535 "tool_name": "resource-list",
541 "reasoning": "I need to call the resource-list tool to get the dungeon information."
544 oss <<
"**Tool Result Response (After Tool Executes):**\n";
547 "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.",
548 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
563 std::ostringstream oss;
565# Critical Constraints
5671. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
569 "text_response": "Your natural language reply to the user.",
570 "tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
571 "commands": ["command1", "command2"],
572 "reasoning": "Your thought process."
574 - `text_response` is for conversational replies.
575 - `tool_calls` is for asking questions about the ROM. Use the available tools listed below.
576 - `commands` is for generating commands to modify the ROM.
577 - All fields are optional, but you should always provide at least one.
5792. **Tool Calling Workflow (CRITICAL):**
580 WHEN YOU CALL A TOOL:
581 a) First response: Include tool_calls with the tool name and arguments
582 b) The tool will execute and you'll receive results in the next message
583 c) Second response: You MUST provide a text_response that answers the user's question using the tool results
584 d) DO NOT call the same tool again unless you need different parameters
585 e) DO NOT leave text_response empty after receiving tool results
587 Example conversation flow:
588 User: "What dungeons are in this ROM?"
589 You (first): {"tool_calls": [{"tool_name": "resource-list", "args": {"type": "dungeon"}}]}
590 [Tool executes and returns: {"dungeons": ["Hyrule Castle", "Eastern Palace", ...]}]
591 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."}
5933. **Tool Usage:** When the user asks a question about the ROM state, use tool_calls instead of commands
594 - Tools are read-only and return information
595 - Commands modify the ROM and should only be used when explicitly requested
596 - You can call multiple tools in one response
597 - Always use JSON format for tool results
598 - ALWAYS provide text_response after receiving tool results
6004. **Command Syntax:** Follow the exact syntax shown in examples
601 - Use correct flag names (--group, --id, --to, --from, etc.)
602 - Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
603 - Coordinates are 0-based indices
6055. **Common Patterns:**
606 - Palette modifications: export → set-color → import
607 - Multiple tile placement: multiple overworld set-tile commands
608 - Validation: single rom validate command
6106. **Error Prevention:**
611 - Always export before modifying palettes
612 - Use temporary file names (temp_*.json) for intermediate files
613 - Validate coordinates are within bounds
617 oss <<
"\n# Available Tools for ROM Inspection\n\n";
618 oss <<
"You have access to the following tools to answer questions:\n\n";
622 oss <<
"**Tool Call Example (Initial Request):**\n";
627 "tool_name": "resource-list",
633 "reasoning": "I need to call the resource-list tool to get the dungeon information."
636 oss <<
"**Tool Result Response (After Tool Executes):**\n";
639 "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.",
640 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
653 std::ostringstream oss;
654 oss <<
"# Tile16 Reference (ALTTP)\n\n";
657 oss <<
"- " << alias <<
": " << value <<
"\n";
665 std::ostringstream oss;
667 oss <<
"# Current ROM Context\n\n";
675 if (resource_context_or.ok()) {
676 oss << resource_context_or.value();
680 if (context.rom_loaded) {
681 oss <<
"- **ROM Loaded:** Yes (" << context.rom_path <<
")\n";
683 oss <<
"- **ROM Loaded:** No\n";
686 if (!context.current_editor.empty()) {
687 oss <<
"- **Active Editor:** " << context.current_editor <<
"\n";
690 if (!context.editor_state.empty()) {
691 oss <<
"- **Editor State:**\n";
692 for (
const auto& [key, value] : context.editor_state) {
693 oss <<
" - " << key <<
": " << value <<
"\n";
704 if (file_path.ok()) {
705 std::ifstream file(file_path->string());
706 if (file.is_open()) {
707 std::string content((std::istreambuf_iterator<char>(file)),
708 std::istreambuf_iterator<char>());
709 if (!content.empty()) {
710 std::ostringstream oss;
729 std::ostringstream oss;
731 oss <<
"You are an expert ROM hacking assistant for The Legend of Zelda: "
732 <<
"A Link to the Past (ALTTP).\n\n";
734 oss <<
"Your task is to generate a sequence of z3ed CLI commands to achieve "
735 <<
"the user's request.\n\n";
748 oss <<
"\n**Response Format:**\n";
750 oss <<
"[\"command1 --flag value\", \"command2 --flag value\"]\n";
757 std::ostringstream oss;
767 const std::string& user_prompt,
768 const RomContext& context) {
769 std::ostringstream oss;
771 if (context.rom_loaded || !context.current_editor.empty()) {
776 oss <<
"**User Request:** " << user_prompt <<
"\n\n";
777 oss <<
"Generate the appropriate z3ed commands as a JSON array.";
783 const std::vector<agent::ChatMessage>& history) {
784 std::ostringstream oss;
785 oss <<
"This is a conversation between a user and an expert ROM hacking "
788 for (
const auto& msg : history) {
790 oss <<
"User: " << msg.message <<
"\n";
792 oss <<
"Agent: " << msg.message <<
"\n";
795 oss <<
"\nBased on this conversation, provide a response in the required JSON "
805 const std::string& category) {
806 std::vector<FewShotExample> result;
810 if (category ==
"palette" &&
811 (example.user_prompt.find(
"palette") != std::string::npos ||
812 example.user_prompt.find(
"color") != std::string::npos)) {
813 result.push_back(example);
814 }
else if (category ==
"overworld" &&
815 (example.user_prompt.find(
"place") != std::string::npos ||
816 example.user_prompt.find(
"tree") != std::string::npos ||
817 example.user_prompt.find(
"house") != std::string::npos)) {
818 result.push_back(example);
819 }
else if (category ==
"validation" &&
820 example.user_prompt.find(
"validate") != std::string::npos) {
821 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)
bool IsYamlBool(const std::string &value)
nlohmann::json YamlToJson(const YAML::Node &node)
Main namespace for the application.
std::string text_response
std::vector< ToolCall > tool_calls
std::vector< std::string > expected_commands