yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tool_dispatcher.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <future>
6#include <memory>
7#include <sstream>
8#include <string>
9
10#include "absl/container/flat_hash_set.h"
11#include "absl/strings/ascii.h"
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_join.h"
14#ifdef YAZE_WITH_JSON
15#include "nlohmann/json.hpp"
16#endif
19
20namespace yaze {
21namespace cli {
22namespace agent {
23
24namespace {
25
26bool IsTruthyValue(const std::string& value) {
27 const std::string lower = absl::AsciiStrToLower(value);
28 return lower == "true" || lower == "1" || lower == "yes" || lower == "on";
29}
30
31bool IsFalsyValue(const std::string& value) {
32 const std::string lower = absl::AsciiStrToLower(value);
33 return lower == "false" || lower == "0" || lower == "no" || lower == "off";
34}
35
36// Convert tool call arguments map to command-line style vector
37absl::StatusOr<std::vector<std::string>> ConvertArgsToVector(
38 const ToolDefinition& def, const std::map<std::string, std::string>& args) {
39 std::vector<std::string> result;
40 const absl::flat_hash_set<std::string> flag_args(def.flag_args.begin(),
41 def.flag_args.end());
42
43 for (const auto& [key, value] : args) {
44 if (flag_args.contains(key)) {
45 if (value.empty() || IsTruthyValue(value)) {
46 result.push_back(absl::StrCat("--", key));
47 continue;
48 }
49 if (IsFalsyValue(value)) {
50 continue;
51 }
52 return absl::InvalidArgumentError(
53 absl::StrCat("Flag argument '--", key,
54 "' must use a boolean value when called as a tool"));
55 }
56
57 result.push_back(absl::StrCat("--", key, "=", value));
58 }
59
60 // Always request JSON format for tool calls (easier for AI to parse)
61 bool has_format = false;
62 for (const auto& arg : result) {
63 if (arg.find("--format=") == 0) {
64 has_format = true;
65 break;
66 }
67 }
68 if (!has_format) {
69 result.push_back("--format=json");
70 }
71
72 return result;
73}
74
75} // namespace
76
78 // Check preferences based on category
79 if (def.category == "resource")
81 if (def.category == "dungeon")
82 return preferences_.dungeon;
83 if (def.category == "overworld")
85 if (def.category == "message" || def.category == "dialogue")
86 return preferences_.messages; // Merge for simplicity or split if needed
87 if (def.category == "gui")
88 return preferences_.gui;
89 if (def.category == "music")
90 return preferences_.music;
91 if (def.category == "sprite")
92 return preferences_.sprite;
93 if (def.category == "emulator")
95 if (def.category == "filesystem")
97 if (def.category == "build")
98 return preferences_.build;
99 if (def.category == "memory")
101 if (def.category == "meta")
103 if (def.category == "tools")
104 return preferences_.test_helpers; // "tools" category in old mapping
105 if (def.category == "visual")
107 if (def.category == "codegen")
108 return preferences_.code_gen;
109 if (def.category == "project")
110 return preferences_.project;
111
112 return true; // Default enable
113}
114
116 const ToolCall& call) const {
117 if (def.access == ToolAccess::kMutating &&
119 return absl::PermissionDeniedError(
120 absl::StrCat("Tool '", call.tool_name,
121 "' performs a mutating action and is not authorized in "
122 "the current agent configuration"));
123 }
124
125 std::vector<std::string> missing;
126 for (const auto& arg : def.required_args) {
127 if (!call.args.contains(arg)) {
128 missing.push_back("--" + arg);
129 }
130 }
131 if (!missing.empty()) {
132 return absl::InvalidArgumentError(absl::StrCat(
133 "Tool '", call.tool_name,
134 "' is missing required arguments: ", absl::StrJoin(missing, ", ")));
135 }
136
137 return absl::OkStatus();
138}
139
140absl::StatusOr<std::string> ToolDispatcher::DispatchMetaTool(
141 const ToolCall& call) const {
142#ifdef YAZE_WITH_JSON
143 if (call.tool_name == "tools-list") {
144 nlohmann::json payload;
145 payload["tools"] = nlohmann::json::array();
146 for (const auto& info : GetAvailableTools()) {
147 payload["tools"].push_back(
148 {{"name", info.name},
149 {"category", info.category},
150 {"description", info.description},
151 {"usage", info.usage},
152 {"examples", info.examples},
153 {"requires_rom", info.requires_rom},
154 {"requires_project", info.requires_project},
155 {"requires_authorization", info.requires_authorization},
156 {"required_args", info.required_args},
157 {"flag_args", info.flag_args}});
158 }
159 payload["count"] = payload["tools"].size();
160 return payload.dump(2);
161 }
162
163 if (call.tool_name == "tools-describe") {
164 const auto it = call.args.find("name");
165 if (it == call.args.end()) {
166 return absl::InvalidArgumentError(
167 "Tool 'tools-describe' requires --name");
168 }
169 auto info = GetToolInfo(it->second);
170 if (!info.has_value()) {
171 return absl::NotFoundError(absl::StrCat("Tool not found: ", it->second));
172 }
173
174 nlohmann::json payload = {
175 {"name", info->name},
176 {"category", info->category},
177 {"description", info->description},
178 {"usage", info->usage},
179 {"examples", info->examples},
180 {"requires_rom", info->requires_rom},
181 {"requires_project", info->requires_project},
182 {"requires_authorization", info->requires_authorization},
183 {"required_args", info->required_args},
184 {"flag_args", info->flag_args}};
185 return payload.dump(2);
186 }
187
188 if (call.tool_name == "tools-search") {
189 const auto it = call.args.find("query");
190 if (it == call.args.end()) {
191 return absl::InvalidArgumentError("Tool 'tools-search' requires --query");
192 }
193
194 nlohmann::json payload;
195 payload["query"] = it->second;
196 payload["matches"] = nlohmann::json::array();
197 for (const auto& info : SearchTools(it->second)) {
198 payload["matches"].push_back(
199 {{"name", info.name},
200 {"category", info.category},
201 {"description", info.description},
202 {"usage", info.usage},
203 {"requires_rom", info.requires_rom},
204 {"requires_project", info.requires_project},
205 {"requires_authorization", info.requires_authorization}});
206 }
207 payload["count"] = payload["matches"].size();
208 return payload.dump(2);
209 }
210#endif
211
212 return absl::UnimplementedError(
213 absl::StrCat("Unhandled meta-tool: ", call.tool_name));
214}
215
216absl::StatusOr<std::string> ToolDispatcher::Dispatch(const ToolCall& call) {
217 auto tool_def_or = ToolRegistry::Get().GetToolDefinition(call.tool_name);
218 if (!tool_def_or) {
219 return absl::InvalidArgumentError(
220 absl::StrCat("Unknown tool: ", call.tool_name));
221 }
222 const ToolDefinition& tool_def = tool_def_or.value();
223
224 if (!IsToolEnabled(tool_def)) {
225 return absl::FailedPreconditionError(absl::StrCat(
226 "Tool '", call.tool_name, "' disabled by current agent configuration"));
227 }
228
229 absl::Status validation_status = ValidateCall(tool_def, call);
230 if (!validation_status.ok()) {
231 return validation_status;
232 }
233
234 if (tool_def.category == "meta") {
235 return DispatchMetaTool(call);
236 }
237
238 // Create handler from registry
239 auto handler_or = ToolRegistry::Get().CreateHandler(call.tool_name);
240 if (!handler_or.ok()) {
241 return handler_or.status();
242 }
243 auto handler = std::move(handler_or.value());
244
245 // Set contexts for the handler
246 handler->SetRomContext(rom_context_);
247 handler->SetProjectContext(project_context_);
248 handler->SetAsarWrapper(asar_wrapper_);
249
250 // Prefer an explicit symbol-table pointer when set by the host.
251 // Otherwise fall back to the Asar wrapper's table (snapshotted into a
252 // member cache on each dispatch so the pointer stays valid across calls).
254 handler->SetAssemblySymbolTable(assembly_symbol_table_);
255 } else if (asar_wrapper_) {
257 handler->SetAssemblySymbolTable(&asar_symbols_cache_);
258 }
259
260 // Convert arguments to command-line style
261 auto args_or = ConvertArgsToVector(tool_def, call.args);
262 if (!args_or.ok()) {
263 return args_or.status();
264 }
265 std::vector<std::string> args = std::move(args_or).value();
266
267 // Check if ROM context is required but not available
268 if (tool_def.requires_rom && !rom_context_) {
269 return absl::FailedPreconditionError(
270 absl::StrCat("Tool '", call.tool_name,
271 "' requires ROM context but none is available"));
272 }
273
274 // Check if Project context is required but not available
275 if (tool_def.requires_project && !project_context_) {
276 return absl::FailedPreconditionError(
277 absl::StrCat("Tool '", call.tool_name,
278 "' requires Project context but none is available"));
279 }
280
281 // Execute the command handler
282 std::stringstream output_buffer;
283 std::streambuf* old_cout = std::cout.rdbuf(output_buffer.rdbuf());
284
285 absl::Status status = handler->Run(args, rom_context_);
286
287 std::cout.rdbuf(old_cout);
288
289 if (!status.ok()) {
290 return status;
291 }
292
293 std::string output = output_buffer.str();
294 if (output.empty()) {
295 return absl::InternalError(
296 absl::StrCat("Tool '", call.tool_name, "' produced no output"));
297 }
298
299 return output;
300}
301
302std::vector<ToolDispatcher::ToolInfo> ToolDispatcher::GetAvailableTools()
303 const {
304 std::vector<ToolInfo> tools;
305 auto all_defs = ToolRegistry::Get().GetAllTools();
306
307 for (const auto& def : all_defs) {
308 if (IsToolEnabled(def)) {
309 tools.push_back({def.name, def.category, def.description, def.usage,
310 def.examples, def.requires_rom, def.requires_project,
311 def.access == ToolAccess::kMutating, def.required_args,
312 def.flag_args});
313 }
314 }
315 return tools;
316}
317
318std::optional<ToolDispatcher::ToolInfo> ToolDispatcher::GetToolInfo(
319 const std::string& tool_name) const {
320 auto def = ToolRegistry::Get().GetToolDefinition(tool_name);
321 if (def) {
322 return ToolInfo{def->name,
323 def->category,
324 def->description,
325 def->usage,
326 def->examples,
327 def->requires_rom,
328 def->requires_project,
329 def->access == ToolAccess::kMutating,
330 def->required_args,
331 def->flag_args};
332 }
333 return std::nullopt;
334}
335
336std::vector<ToolDispatcher::ToolInfo> ToolDispatcher::SearchTools(
337 const std::string& query) const {
338 std::vector<ToolInfo> matches;
339 std::string lower_query = absl::AsciiStrToLower(query);
340
341 auto all_tools = GetAvailableTools();
342 for (const auto& tool : all_tools) {
343 std::string lower_name = absl::AsciiStrToLower(tool.name);
344 std::string lower_desc = absl::AsciiStrToLower(tool.description);
345 std::string lower_category = absl::AsciiStrToLower(tool.category);
346
347 if (lower_name.find(lower_query) != std::string::npos ||
348 lower_desc.find(lower_query) != std::string::npos ||
349 lower_category.find(lower_query) != std::string::npos) {
350 matches.push_back(tool);
351 }
352 }
353
354 return matches;
355}
356
358 const BatchToolCall& batch) {
359 BatchResult result;
360 result.results.resize(batch.calls.size());
361 result.statuses.resize(batch.calls.size());
362
363 auto start_time = std::chrono::high_resolution_clock::now();
364
365 if (batch.parallel && batch.calls.size() > 1) {
366 // Parallel execution using std::async
367 std::vector<std::future<absl::StatusOr<std::string>>> futures;
368 futures.reserve(batch.calls.size());
369
370 for (const auto& call : batch.calls) {
371 futures.push_back(std::async(std::launch::async, [this, &call]() {
372 return this->Dispatch(call);
373 }));
374 }
375
376 // Collect results
377 for (size_t i = 0; i < futures.size(); ++i) {
378 auto status_or = futures[i].get();
379 if (status_or.ok()) {
380 result.results[i] = std::move(status_or.value());
381 result.statuses[i] = absl::OkStatus();
382 result.successful_count++;
383 } else {
384 result.results[i] = "";
385 result.statuses[i] = status_or.status();
386 result.failed_count++;
387 }
388 }
389 } else {
390 // Sequential execution
391 for (size_t i = 0; i < batch.calls.size(); ++i) {
392 auto status_or = Dispatch(batch.calls[i]);
393 if (status_or.ok()) {
394 result.results[i] = std::move(status_or.value());
395 result.statuses[i] = absl::OkStatus();
396 result.successful_count++;
397 } else {
398 result.results[i] = "";
399 result.statuses[i] = status_or.status();
400 result.failed_count++;
401 }
402 }
403 }
404
405 auto end_time = std::chrono::high_resolution_clock::now();
407 std::chrono::duration<double, std::milli>(end_time - start_time).count();
408
409 return result;
410}
411
412} // namespace agent
413} // namespace cli
414} // namespace yaze
const std::map< std::string, core::AsarSymbol > * assembly_symbol_table_
std::optional< ToolInfo > GetToolInfo(const std::string &tool_name) const
Get detailed information about a specific tool.
absl::Status ValidateCall(const ToolDefinition &def, const ToolCall &call) const
std::map< std::string, core::AsarSymbol > asar_symbols_cache_
std::vector< ToolInfo > SearchTools(const std::string &query) const
Search tools by keyword.
project::YazeProject * project_context_
bool IsToolEnabled(const ToolDefinition &def) const
BatchResult DispatchBatch(const BatchToolCall &batch)
Execute multiple tool calls in a batch.
std::vector< ToolInfo > GetAvailableTools() const
Get list of all available tools.
absl::StatusOr< std::string > Dispatch(const ::yaze::cli::ToolCall &tool_call)
absl::StatusOr< std::string > DispatchMetaTool(const ToolCall &call) const
std::optional< ToolDefinition > GetToolDefinition(const std::string &name) const
static ToolRegistry & Get()
std::vector< ToolDefinition > GetAllTools() const
absl::StatusOr< std::unique_ptr< resources::CommandHandler > > CreateHandler(const std::string &tool_name) const
std::map< std::string, AsarSymbol > GetSymbolTable() const
absl::StatusOr< std::vector< std::string > > ConvertArgsToVector(const ToolDefinition &def, const std::map< std::string, std::string > &args)
std::map< std::string, std::string > args
Definition common.h:14
std::string tool_name
Definition common.h:13
Metadata describing a tool for the LLM.
std::vector< std::string > required_args
std::vector< std::string > flag_args
Tool information for discoverability.