9#include "absl/strings/ascii.h"
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_join.h"
13#include "nlohmann/json.hpp"
17#ifdef YAZE_HAS_YAML_CPP
18#include "yaml-cpp/yaml.h"
26#ifdef YAZE_HAS_YAML_CPP
27bool IsYamlBool(
const std::string& value) {
28 const std::string lower = absl::AsciiStrToLower(value);
29 return lower ==
"true" || lower ==
"false" || lower ==
"yes" ||
30 lower ==
"no" || lower ==
"on" || lower ==
"off";
33nlohmann::json YamlToJson(
const YAML::Node& node) {
35 return nlohmann::json();
38 switch (node.Type()) {
39 case YAML::NodeType::Scalar: {
40 const std::string scalar = node.as<std::string>(
"");
42 if (IsYamlBool(scalar)) {
43 return node.as<
bool>();
46 if (scalar ==
"~" || absl::AsciiStrToLower(scalar) ==
"null") {
47 return nlohmann::json();
52 case YAML::NodeType::Sequence: {
53 nlohmann::json array = nlohmann::json::array();
54 for (
const auto& item : node) {
55 array.push_back(YamlToJson(item));
59 case YAML::NodeType::Map: {
60 nlohmann::json
object = nlohmann::json::object();
61 for (
const auto& kv : node) {
62 object[kv.first.as<std::string>()] = YamlToJson(kv.second);
67 return nlohmann::json();
85 const std::string& yaml_path)
const {
87 if (!yaml_path.empty()) {
89 if (std::filesystem::exists(yaml_path, ec) && !ec) {
95 if (
const char* env_path = std::getenv(
"YAZE_AGENT_CATALOGUE")) {
96 if (*env_path !=
'\0') {
98 if (std::filesystem::exists(env_path, ec) && !ec) {
99 return std::string(env_path);
106 std::string relative_path =
107 yaml_path.empty() ?
"agent/prompt_catalogue.yaml" : yaml_path;
111 return result->string();
114 return result.status();
118 const std::string& yaml_path) {
119#if !defined(YAZE_WITH_JSON) || !defined(YAZE_HAS_YAML_CPP)
123 <<
"⚠️ PromptBuilder requires JSON and yaml-cpp support for catalogue loading\n"
124 <<
" Build with -DYAZE_WITH_JSON=ON (mac-ai preset already does) and install yaml-cpp\n"
125 <<
" AI features will use basic prompts without tool definitions\n";
126 return absl::OkStatus();
129 if (!resolved_or.ok()) {
131 return resolved_or.status();
134 const std::string& resolved_path = resolved_or.value();
138 root = YAML::LoadFile(resolved_path);
139 }
catch (
const YAML::BadFile& e) {
141 return absl::NotFoundError(absl::StrCat(
142 "Unable to open prompt catalogue at ", resolved_path,
": ", e.what()));
143 }
catch (
const YAML::ParserException& e) {
145 return absl::InvalidArgumentError(absl::StrCat(
146 "Failed to parse prompt catalogue at ", resolved_path,
": ", e.what()));
149 nlohmann::json catalogue = YamlToJson(root);
152 if (catalogue.contains(
"commands")) {
153 if (
auto status =
ParseCommands(catalogue[
"commands"]); !status.ok()) {
158 if (catalogue.contains(
"tools")) {
159 if (
auto status =
ParseTools(catalogue[
"tools"]); !status.ok()) {
164 if (catalogue.contains(
"examples")) {
165 if (
auto status =
ParseExamples(catalogue[
"examples"]); !status.ok()) {
170 if (catalogue.contains(
"tile16_reference")) {
175 return absl::OkStatus();
180 if (!commands.is_object()) {
181 return absl::InvalidArgumentError(
182 "commands section must be an object mapping command names to docs");
185 for (
const auto& [name, value] : commands.items()) {
186 if (!value.is_string()) {
187 return absl::InvalidArgumentError(
188 absl::StrCat(
"Command entry for ", name,
" must be a string"));
193 return absl::OkStatus();
197 if (!tools.is_array()) {
198 return absl::InvalidArgumentError(
"tools section must be an array");
201 for (
const auto& tool_json : tools) {
202 if (!tool_json.is_object()) {
203 return absl::InvalidArgumentError(
204 "Each tool entry must be an object with name/description");
208 if (tool_json.contains(
"name") && tool_json[
"name"].is_string()) {
209 spec.
name = tool_json[
"name"].get<std::string>();
211 return absl::InvalidArgumentError(
"Tool entry missing name");
214 if (tool_json.contains(
"description") &&
215 tool_json[
"description"].is_string()) {
216 spec.
description = tool_json[
"description"].get<std::string>();
219 if (tool_json.contains(
"usage_notes") &&
220 tool_json[
"usage_notes"].is_string()) {
221 spec.
usage_notes = tool_json[
"usage_notes"].get<std::string>();
224 if (tool_json.contains(
"arguments")) {
225 const auto& args = tool_json[
"arguments"];
226 if (!args.is_array()) {
227 return absl::InvalidArgumentError(absl::StrCat(
228 "Tool arguments for ", spec.
name,
" must be an array"));
230 for (
const auto& arg_json : args) {
231 if (!arg_json.is_object()) {
232 return absl::InvalidArgumentError(absl::StrCat(
233 "Argument entries for ", spec.
name,
" must be objects"));
236 if (arg_json.contains(
"name") && arg_json[
"name"].is_string()) {
237 arg.
name = arg_json[
"name"].get<std::string>();
239 return absl::InvalidArgumentError(absl::StrCat(
240 "Argument entry for ", spec.
name,
" is missing a name"));
242 if (arg_json.contains(
"description") &&
243 arg_json[
"description"].is_string()) {
244 arg.
description = arg_json[
"description"].get<std::string>();
246 if (arg_json.contains(
"required")) {
247 if (!arg_json[
"required"].is_boolean()) {
248 return absl::InvalidArgumentError(
249 absl::StrCat(
"Argument 'required' flag for ", spec.
name,
250 "::", arg.
name,
" must be boolean"));
252 arg.
required = arg_json[
"required"].get<
bool>();
254 if (arg_json.contains(
"example") && arg_json[
"example"].is_string()) {
255 arg.
example = arg_json[
"example"].get<std::string>();
257 spec.
arguments.push_back(std::move(arg));
264 return absl::OkStatus();
268 if (!examples.is_array()) {
269 return absl::InvalidArgumentError(
"examples section must be an array");
272 for (
const auto& example_json : examples) {
273 if (!example_json.is_object()) {
274 return absl::InvalidArgumentError(
"Each example entry must be an object");
278 if (example_json.contains(
"user_prompt") &&
279 example_json[
"user_prompt"].is_string()) {
280 example.
user_prompt = example_json[
"user_prompt"].get<std::string>();
282 return absl::InvalidArgumentError(
"Example missing user_prompt");
285 if (example_json.contains(
"text_response") &&
286 example_json[
"text_response"].is_string()) {
287 example.
text_response = example_json[
"text_response"].get<std::string>();
290 if (example_json.contains(
"reasoning") &&
291 example_json[
"reasoning"].is_string()) {
292 example.
explanation = example_json[
"reasoning"].get<std::string>();
295 if (example_json.contains(
"commands")) {
296 const auto& commands = example_json[
"commands"];
297 if (!commands.is_array()) {
298 return absl::InvalidArgumentError(absl::StrCat(
299 "Example commands for ", example.
user_prompt,
" must be an array"));
301 for (
const auto& cmd : commands) {
302 if (!cmd.is_string()) {
303 return absl::InvalidArgumentError(absl::StrCat(
304 "Command entries for ", example.
user_prompt,
" must be strings"));
310 if (example_json.contains(
"tool_calls")) {
311 const auto& calls = example_json[
"tool_calls"];
312 if (!calls.is_array()) {
313 return absl::InvalidArgumentError(absl::StrCat(
314 "Tool calls for ", example.
user_prompt,
" must be an array"));
316 for (
const auto& call_json : calls) {
317 if (!call_json.is_object()) {
318 return absl::InvalidArgumentError(
319 absl::StrCat(
"Tool call entries for ", example.
user_prompt,
320 " must be objects"));
323 if (call_json.contains(
"tool_name") &&
324 call_json[
"tool_name"].is_string()) {
325 call.
tool_name = call_json[
"tool_name"].get<std::string>();
327 return absl::InvalidArgumentError(absl::StrCat(
328 "Tool call missing tool_name in example: ", example.
user_prompt));
330 if (call_json.contains(
"args")) {
331 const auto& args = call_json[
"args"];
332 if (!args.is_object()) {
333 return absl::InvalidArgumentError(
334 absl::StrCat(
"Tool call args for ", example.
user_prompt,
335 " must be an object"));
337 for (
const auto& [key, value] : args.items()) {
338 if (!value.is_string()) {
339 return absl::InvalidArgumentError(
340 absl::StrCat(
"Tool call arg value for ", example.
user_prompt,
341 " must be a string"));
343 call.
args[key] = value.get<std::string>();
346 example.
tool_calls.push_back(std::move(call));
351 example_json.value(
"explanation", example.
explanation);
355 return absl::OkStatus();
364 if (value.is_string()) {
379 std::ostringstream oss;
381 oss <<
"# Available z3ed Commands\n\n";
384 oss <<
"## " << cmd <<
"\n";
385 oss << docs <<
"\n\n";
396 std::ostringstream oss;
397 oss <<
"# Available Agent Tools\n\n";
400 oss <<
"## " << spec.name <<
"\n";
401 if (!spec.description.empty()) {
402 oss << spec.description <<
"\n\n";
405 if (!spec.arguments.empty()) {
406 oss <<
"| Argument | Required | Description | Example |\n";
407 oss <<
"| --- | --- | --- | --- |\n";
408 for (
const auto& arg : spec.arguments) {
409 oss <<
"| `" << arg.name <<
"` | " << (arg.required ?
"yes" :
"no")
410 <<
" | " << arg.description <<
" | "
411 << (arg.example.empty() ?
"" :
"`" + arg.example +
"`") <<
" |\n";
416 if (!spec.usage_notes.empty()) {
417 oss <<
"_Usage:_ " << spec.usage_notes <<
"\n\n";
429 nlohmann::json tools_array = nlohmann::json::array();
433 tool[
"type"] =
"function";
435 nlohmann::json function;
436 function[
"name"] = spec.name;
437 function[
"description"] = spec.description;
438 if (!spec.usage_notes.empty()) {
439 function[
"description"] = spec.description +
" " + spec.usage_notes;
442 nlohmann::json parameters;
443 parameters[
"type"] =
"object";
445 nlohmann::json properties = nlohmann::json::object();
446 nlohmann::json required = nlohmann::json::array();
448 for (
const auto& arg : spec.arguments) {
449 nlohmann::json arg_schema;
450 arg_schema[
"type"] =
"string";
451 arg_schema[
"description"] = arg.description;
452 if (!arg.example.empty()) {
453 arg_schema[
"example"] = arg.example;
455 properties[arg.name] = arg_schema;
458 required.push_back(arg.name);
462 parameters[
"properties"] = properties;
463 if (!required.empty()) {
464 parameters[
"required"] = required;
467 function[
"parameters"] = parameters;
468 tool[
"function"] = function;
469 tools_array.push_back(tool);
472 return tools_array.dump(2);
476 std::ostringstream oss;
478 oss <<
"# Example Command Sequences\n\n";
479 oss <<
"Here are proven examples of how to accomplish common tasks:\n\n";
482 oss <<
"**User Request:** \"" << example.user_prompt <<
"\"\n";
483 oss <<
"**Structured Response:**\n";
485 nlohmann::json example_json = nlohmann::json::object();
486 if (!example.text_response.empty()) {
487 example_json[
"text_response"] = example.text_response;
489 if (!example.expected_commands.empty()) {
490 example_json[
"commands"] = example.expected_commands;
492 if (!example.explanation.empty()) {
493 example_json[
"reasoning"] = example.explanation;
495 if (!example.tool_calls.empty()) {
496 nlohmann::json calls = nlohmann::json::array();
497 for (
const auto& call : example.tool_calls) {
498 nlohmann::json call_json;
499 call_json[
"tool_name"] = call.tool_name;
500 nlohmann::json args = nlohmann::json::object();
501 for (
const auto& [key, value] : call.args) {
504 call_json[
"args"] = std::move(args);
505 calls.push_back(std::move(call_json));
507 example_json[
"tool_calls"] = std::move(calls);
510 oss <<
"```json\n" << example_json.dump(2) <<
"\n```\n\n";
520 if (file_path.ok()) {
521 std::ifstream file(file_path->string());
522 if (file.is_open()) {
523 std::string content((std::istreambuf_iterator<char>(file)),
524 std::istreambuf_iterator<char>());
525 if (!content.empty()) {
526 std::ostringstream oss;
531 oss <<
"\n\n# Available Tools for ROM Inspection\n\n";
532 oss <<
"You have access to the following tools to answer "
537 oss <<
"**Tool Call Example (Initial Request):**\n";
542 "tool_name": "resource-list",
548 "reasoning": "I need to call the resource-list tool to get the dungeon information."
551 oss <<
"**Tool Result Response (After Tool Executes):**\n";
554 "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.",
555 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
570 std::ostringstream oss;
572# Critical Constraints
5741. **Output Format:** You MUST respond with ONLY a JSON object with the following structure:
576 "text_response": "Your natural language reply to the user.",
577 "tool_calls": [{ "tool_name": "tool_name", "args": { "arg1": "value1" } }],
578 "commands": ["command1", "command2"],
579 "reasoning": "Your thought process."
581 - `text_response` is for conversational replies.
582 - `tool_calls` is for asking questions about the ROM. Use the available tools listed below.
583 - `commands` is for generating commands to modify the ROM.
584 - All fields are optional, but you should always provide at least one.
5862. **Tool Calling Workflow (CRITICAL):**
587 WHEN YOU CALL A TOOL:
588 a) First response: Include tool_calls with the tool name and arguments
589 b) The tool will execute and you'll receive results in the next message
590 c) Second response: You MUST provide a text_response that answers the user's question using the tool results
591 d) DO NOT call the same tool again unless you need different parameters
592 e) DO NOT leave text_response empty after receiving tool results
594 Example conversation flow:
595 User: "What dungeons are in this ROM?"
596 You (first): {"tool_calls": [{"tool_name": "resource-list", "args": {"type": "dungeon"}}]}
597 [Tool executes and returns: {"dungeons": ["Hyrule Castle", "Eastern Palace", ...]}]
598 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."}
6003. **Tool Usage:** When the user asks a question about the ROM state, use tool_calls instead of commands
601 - Tools are read-only and return information
602 - Commands modify the ROM and should only be used when explicitly requested
603 - You can call multiple tools in one response
604 - Always use JSON format for tool results
605 - ALWAYS provide text_response after receiving tool results
6074. **Command Syntax:** Follow the exact syntax shown in examples
608 - Use correct flag names (--group, --id, --to, --from, etc.)
609 - Use hex format for colors (0xRRGGBB) and tile IDs (0xNNN)
610 - Coordinates are 0-based indices
6125. **Common Patterns:**
613 - Palette modifications: export → set-color → import
614 - Multiple tile placement: multiple overworld set-tile commands
615 - Validation: single rom validate command
6176. **Error Prevention:**
618 - Always export before modifying palettes
619 - Use temporary file names (temp_*.json) for intermediate files
620 - Validate coordinates are within bounds
624 oss <<
"\n# Available Tools for ROM Inspection\n\n";
625 oss <<
"You have access to the following tools to answer questions:\n\n";
629 oss <<
"**Tool Call Example (Initial Request):**\n";
634 "tool_name": "resource-list",
640 "reasoning": "I need to call the resource-list tool to get the dungeon information."
643 oss <<
"**Tool Result Response (After Tool Executes):**\n";
646 "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.",
647 "reasoning": "The tool returned a list of 12 dungeons which I've formatted into a readable response."
660 std::ostringstream oss;
661 oss <<
"# Tile16 Reference (ALTTP)\n\n";
664 oss <<
"- " << alias <<
": " << value <<
"\n";
672 std::ostringstream oss;
674 oss <<
"# Current ROM Context\n\n";
680 std::make_unique<ResourceContextBuilder>(
rom_);
682 auto resource_context_or =
684 if (resource_context_or.ok()) {
685 oss << resource_context_or.value();
689 if (context.rom_loaded) {
690 oss <<
"- **ROM Loaded:** Yes (" << context.rom_path <<
")\n";
692 oss <<
"- **ROM Loaded:** No\n";
695 if (!context.current_editor.empty()) {
696 oss <<
"- **Active Editor:** " << context.current_editor <<
"\n";
699 if (!context.editor_state.empty()) {
700 oss <<
"- **Editor State:**\n";
701 for (
const auto& [key, value] : context.editor_state) {
702 oss <<
" - " << key <<
": " << value <<
"\n";
713 if (file_path.ok()) {
714 std::ifstream file(file_path->string());
715 if (file.is_open()) {
716 std::string content((std::istreambuf_iterator<char>(file)),
717 std::istreambuf_iterator<char>());
718 if (!content.empty()) {
719 std::ostringstream oss;
738 std::ostringstream oss;
740 oss <<
"You are an expert ROM hacking assistant for The Legend of Zelda: "
741 <<
"A Link to the Past (ALTTP).\n\n";
743 oss <<
"Your task is to generate a sequence of z3ed CLI commands to achieve "
744 <<
"the user's request.\n\n";
757 oss <<
"\n**Response Format:**\n";
759 oss <<
"[\"command1 --flag value\", \"command2 --flag value\"]\n";
766 std::ostringstream oss;
776 const RomContext& context) {
777 std::ostringstream oss;
779 if (context.rom_loaded || !context.current_editor.empty()) {
784 oss <<
"**User Request:** " << user_prompt <<
"\n\n";
785 oss <<
"Generate the appropriate z3ed commands as a JSON array.";
791 const std::vector<agent::ChatMessage>& history) {
792 std::ostringstream oss;
793 oss <<
"This is a conversation between a user and an expert ROM hacking "
796 for (
const auto& msg : history) {
798 oss <<
"User: " << msg.message <<
"\n";
800 oss <<
"Agent: " << msg.message <<
"\n";
803 oss <<
"\nBased on this conversation, provide a response in the required "
814 const std::string& category) {
815 std::vector<FewShotExample> result;
819 if (category ==
"palette" &&
820 (example.user_prompt.find(
"palette") != std::string::npos ||
821 example.user_prompt.find(
"color") != std::string::npos)) {
822 result.push_back(example);
823 }
else if (category ==
"overworld" &&
824 (example.user_prompt.find(
"place") != std::string::npos ||
825 example.user_prompt.find(
"tree") != std::string::npos ||
826 example.user_prompt.find(
"house") != std::string::npos)) {
827 result.push_back(example);
828 }
else if (category ==
"validation" &&
829 example.user_prompt.find(
"validate") != std::string::npos) {
830 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