yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
anthropic_ai_service.cc
Go to the documentation of this file.
2
3#include <atomic>
4#include <cstdlib>
5#include <iostream>
6#include <map>
7#include <mutex>
8#include <string>
9#include <vector>
10
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
14#include "absl/strings/strip.h"
15#include "absl/time/clock.h"
16#include "absl/time/time.h"
18#include "util/platform_paths.h"
19
20#if defined(__APPLE__)
21#include <TargetConditionals.h>
22#endif
23
24#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
26#define YAZE_AI_IOS_URLSESSION 1
27#endif
28
29#ifdef YAZE_WITH_JSON
30#include <filesystem>
31#include <fstream>
32
33#include "httplib.h"
34#include "nlohmann/json.hpp"
35#endif
36
37namespace yaze {
38namespace cli {
39
40#ifdef YAZE_AI_RUNTIME_AVAILABLE
41
42AnthropicAIService::AnthropicAIService(const AnthropicConfig& config)
43 : function_calling_enabled_(config.use_function_calling), config_(config) {
44 if (config_.verbose) {
45 std::cerr << "[DEBUG] Initializing Anthropic service..." << std::endl;
46 std::cerr << "[DEBUG] Model: " << config_.model << std::endl;
47 }
48
49 // Load command documentation into prompt builder
50 std::string catalogue_path = config_.prompt_version == "v2"
51 ? "assets/agent/prompt_catalogue_v2.yaml"
52 : "assets/agent/prompt_catalogue.yaml";
53 if (auto status = prompt_builder_.LoadResourceCatalogue(catalogue_path);
54 !status.ok()) {
55 std::cerr << "⚠️ Failed to load agent prompt catalogue: "
56 << status.message() << std::endl;
57 }
58
59 if (config_.system_instruction.empty()) {
60 // Load system prompt file
61 std::string prompt_file;
62 if (config_.prompt_version == "v3") {
63 prompt_file = "agent/system_prompt_v3.txt";
64 } else if (config_.prompt_version == "v2") {
65 prompt_file = "agent/system_prompt_v2.txt";
66 } else {
67 prompt_file = "agent/system_prompt.txt";
68 }
69
70 auto prompt_path = util::PlatformPaths::FindAsset(prompt_file);
71 if (prompt_path.ok()) {
72 std::ifstream file(prompt_path->string());
73 if (file.good()) {
74 std::stringstream buffer;
75 buffer << file.rdbuf();
76 config_.system_instruction = buffer.str();
77 if (config_.verbose) {
78 std::cerr << "[DEBUG] Loaded prompt: " << prompt_path->string()
79 << std::endl;
80 }
81 }
82 }
83
84 if (config_.system_instruction.empty()) {
85 config_.system_instruction = BuildSystemInstruction();
86 }
87 }
88
89 if (config_.verbose) {
90 std::cerr << "[DEBUG] Anthropic service initialized" << std::endl;
91 }
92}
93
94void AnthropicAIService::EnableFunctionCalling(bool enable) {
95 function_calling_enabled_ = enable;
96}
97
98std::vector<std::string> AnthropicAIService::GetAvailableTools() const {
99 return {"resource-list", "resource-search",
100 "dungeon-list-sprites", "dungeon-describe-room",
101 "overworld-find-tile", "overworld-describe-map",
102 "overworld-list-warps"};
103}
104
105std::string AnthropicAIService::BuildFunctionCallSchemas() {
106#ifndef YAZE_WITH_JSON
107 return "[]";
108#else
109 std::string schemas = prompt_builder_.BuildFunctionCallSchemas();
110 if (!schemas.empty() && schemas != "[]") {
111 return schemas;
112 }
113
114 auto schema_path_or =
115 util::PlatformPaths::FindAsset("agent/function_schemas.json");
116
117 if (!schema_path_or.ok()) {
118 return "[]";
119 }
120
121 std::ifstream file(schema_path_or->string());
122 if (!file.is_open()) {
123 return "[]";
124 }
125
126 try {
127 nlohmann::json schemas_json;
128 file >> schemas_json;
129 return schemas_json.dump();
130 } catch (const nlohmann::json::exception& e) {
131 std::cerr << "⚠️ Failed to parse function schemas JSON: " << e.what()
132 << std::endl;
133 return "[]";
134 }
135#endif
136}
137
138std::string AnthropicAIService::BuildSystemInstruction() {
139 return prompt_builder_.BuildSystemInstruction();
140}
141
142void AnthropicAIService::SetRomContext(Rom* rom) {
143 prompt_builder_.SetRom(rom);
144}
145
146absl::StatusOr<std::vector<ModelInfo>>
147AnthropicAIService::ListAvailableModels() {
148 // Anthropic doesn't have a simple public "list models" endpoint like OpenAI/Gemini
149 // We'll return a hardcoded list of supported models
150 std::vector<ModelInfo> defaults = {
151 {.name = "claude-3-5-sonnet-20241022",
152 .display_name = "Claude 3.5 Sonnet",
153 .provider = "anthropic",
154 .description = "Most intelligent model"},
155 {.name = "claude-3-5-haiku-20241022",
156 .display_name = "Claude 3.5 Haiku",
157 .provider = "anthropic",
158 .description = "Fastest and most cost-effective"},
159 {.name = "claude-3-opus-20240229",
160 .display_name = "Claude 3 Opus",
161 .provider = "anthropic",
162 .description = "Strong reasoning model"}};
163 return defaults;
164}
165
166absl::Status AnthropicAIService::CheckAvailability() {
167#ifndef YAZE_WITH_JSON
168 return absl::UnimplementedError(
169 "Anthropic AI service requires JSON support. Build with "
170 "-DYAZE_WITH_JSON=ON");
171#else
172 if (config_.api_key.empty()) {
173 return absl::FailedPreconditionError(
174 "❌ Anthropic API key not configured\n"
175 " Set ANTHROPIC_API_KEY environment variable\n"
176 " Get your API key at: https://console.anthropic.com/");
177 }
178 return absl::OkStatus();
179#endif
180}
181
182absl::StatusOr<AgentResponse> AnthropicAIService::GenerateResponse(
183 const std::string& prompt) {
184 return GenerateResponse(
185 {{{agent::ChatMessage::Sender::kUser, prompt, absl::Now()}}});
186}
187
188absl::StatusOr<AgentResponse> AnthropicAIService::GenerateResponse(
189 const std::vector<agent::ChatMessage>& history) {
190#ifndef YAZE_WITH_JSON
191 return absl::UnimplementedError(
192 "Anthropic AI service requires JSON support. Build with "
193 "-DYAZE_WITH_JSON=ON");
194#else
195 if (history.empty()) {
196 return absl::InvalidArgumentError("History cannot be empty.");
197 }
198
199 if (config_.api_key.empty()) {
200 return absl::FailedPreconditionError("Anthropic API key not configured");
201 }
202
203 absl::Time request_start = absl::Now();
204
205 try {
206 if (config_.verbose) {
207 std::cerr << "[DEBUG] Using curl for Anthropic HTTPS request"
208 << std::endl;
209 }
210
211 // Build messages array
212 nlohmann::json messages = nlohmann::json::array();
213
214 // Add conversation history
215 int start_idx = std::max(0, static_cast<int>(history.size()) - 10);
216 for (size_t i = start_idx; i < history.size(); ++i) {
217 const auto& msg = history[i];
218 std::string role = (msg.sender == agent::ChatMessage::Sender::kUser)
219 ? "user"
220 : "assistant";
221
222 messages.push_back({{"role", role}, {"content", msg.message}});
223 }
224
225 // Build request body
226 nlohmann::json request_body = {{"model", config_.model},
227 {"max_tokens", config_.max_output_tokens},
228 {"system", config_.system_instruction},
229 {"messages", messages}};
230
231 // Add function calling tools if enabled
232 if (function_calling_enabled_) {
233 try {
234 std::string schemas_str = BuildFunctionCallSchemas();
235 if (config_.verbose) {
236 std::cerr << "[DEBUG] Function calling schemas: "
237 << schemas_str.substr(0, 200) << "..." << std::endl;
238 }
239
240 nlohmann::json schemas = nlohmann::json::parse(schemas_str);
241
242 if (schemas.is_array() && !schemas.empty()) {
243 // Convert OpenAI-style tools to Anthropic format
244 nlohmann::json tools = nlohmann::json::array();
245 for (const auto& schema : schemas) {
246 // Check if it's already in tool format or just the function schema
247 nlohmann::json tool_def;
248
249 // Handle both bare schema and wrapped "function" schema
250 nlohmann::json func_schema = schema;
251 if (schema.contains("function")) {
252 func_schema = schema["function"];
253 }
254
255 tool_def = {
256 {"name", func_schema.value("name", "")},
257 {"description", func_schema.value("description", "")},
258 {"input_schema",
259 func_schema.value("parameters", nlohmann::json::object())}};
260
261 tools.push_back(tool_def);
262 }
263 request_body["tools"] = tools;
264 }
265 } catch (const nlohmann::json::exception& e) {
266 std::cerr << "⚠️ Failed to parse function schemas: " << e.what()
267 << std::endl;
268 }
269 }
270
271 if (config_.verbose) {
272 std::cerr << "[DEBUG] Sending " << messages.size()
273 << " messages to Anthropic" << std::endl;
274 }
275
276 std::string response_str;
277#if defined(YAZE_AI_IOS_URLSESSION)
278 std::map<std::string, std::string> headers;
279 headers.emplace("x-api-key", config_.api_key);
280 headers.emplace("anthropic-version", "2023-06-01");
281 headers.emplace("content-type", "application/json");
282 auto resp_or = ios::UrlSessionHttpRequest(
283 "POST", "https://api.anthropic.com/v1/messages", headers,
284 request_body.dump(), 60000);
285 if (!resp_or.ok()) {
286 return resp_or.status();
287 }
288 if (resp_or->status_code != 200) {
289 return absl::InternalError(
290 absl::StrCat("Anthropic API error: ", resp_or->status_code, "\n",
291 resp_or->body));
292 }
293 response_str = resp_or->body;
294#else
295 // Write request body to temp file
296 std::string temp_file = "/tmp/anthropic_request.json";
297 std::ofstream out(temp_file);
298 out << request_body.dump();
299 out.close();
300
301 // Use curl to make the request
302 std::string curl_cmd =
303 "curl -s -X POST 'https://api.anthropic.com/v1/messages' "
304 "-H 'x-api-key: " +
305 config_.api_key +
306 "' "
307 "-H 'anthropic-version: 2023-06-01' "
308 "-H 'content-type: application/json' "
309 "-d @" +
310 temp_file + " 2>&1";
311
312 if (config_.verbose) {
313 std::cerr << "[DEBUG] Executing Anthropic API request..." << std::endl;
314 }
315
316#ifdef _WIN32
317 FILE* pipe = _popen(curl_cmd.c_str(), "r");
318#else
319 FILE* pipe = popen(curl_cmd.c_str(), "r");
320#endif
321 if (!pipe) {
322 return absl::InternalError("Failed to execute curl command");
323 }
324
325 char buffer[4096];
326 while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
327 response_str += buffer;
328 }
329
330#ifdef _WIN32
331 int status = _pclose(pipe);
332#else
333 int status = pclose(pipe);
334#endif
335 std::remove(temp_file.c_str());
336
337 if (status != 0) {
338 return absl::InternalError(
339 absl::StrCat("Curl failed with status ", status));
340 }
341#endif // YAZE_AI_IOS_URLSESSION
342
343 if (response_str.empty()) {
344 return absl::InternalError("Empty response from Anthropic API");
345 }
346
347 if (config_.verbose) {
348 std::cout << "\n"
349 << "\033[35m"
350 << "🔍 Raw Anthropic API Response:"
351 << "\033[0m"
352 << "\n"
353 << "\033[2m" << response_str.substr(0, 500) << "\033[0m"
354 << "\n\n";
355 }
356
357 if (config_.verbose) {
358 std::cerr << "[DEBUG] Parsing response..." << std::endl;
359 }
360
361 auto parsed_or = ParseAnthropicResponse(response_str);
362 if (!parsed_or.ok()) {
363 return parsed_or.status();
364 }
365
366 AgentResponse agent_response = std::move(parsed_or.value());
367 agent_response.provider = "anthropic";
368 agent_response.model = config_.model;
369 agent_response.latency_seconds =
370 absl::ToDoubleSeconds(absl::Now() - request_start);
371 agent_response.parameters["prompt_version"] = config_.prompt_version;
372 agent_response.parameters["temperature"] =
373 absl::StrFormat("%.2f", config_.temperature);
374 agent_response.parameters["max_output_tokens"] =
375 absl::StrFormat("%d", config_.max_output_tokens);
376 agent_response.parameters["function_calling"] =
377 function_calling_enabled_ ? "true" : "false";
378
379 return agent_response;
380
381 } catch (const std::exception& e) {
382 if (config_.verbose) {
383 std::cerr << "[ERROR] Exception: " << e.what() << std::endl;
384 }
385 return absl::InternalError(
386 absl::StrCat("Exception during generation: ", e.what()));
387 }
388#endif
389}
390
391absl::StatusOr<AgentResponse> AnthropicAIService::ParseAnthropicResponse(
392 const std::string& response_body) {
393#ifndef YAZE_WITH_JSON
394 return absl::UnimplementedError("JSON support required");
395#else
396 AgentResponse agent_response;
397
398 auto response_json = nlohmann::json::parse(response_body, nullptr, false);
399 if (response_json.is_discarded()) {
400 return absl::InternalError("❌ Failed to parse Anthropic response JSON");
401 }
402
403 // Check for errors
404 if (response_json.contains("error")) {
405 std::string error_msg =
406 response_json["error"].value("message", "Unknown error");
407 return absl::InternalError(
408 absl::StrCat("❌ Anthropic API error: ", error_msg));
409 }
410
411 // Navigate Anthropic's response structure (Messages API)
412 if (!response_json.contains("content") ||
413 !response_json["content"].is_array()) {
414 return absl::InternalError("❌ No content in Anthropic response");
415 }
416
417 for (const auto& block : response_json["content"]) {
418 std::string type = block.value("type", "");
419
420 if (type == "text") {
421 std::string text_content = block.value("text", "");
422
423 if (config_.verbose) {
424 std::cout << "\n"
425 << "\033[35m"
426 << "🔍 Raw LLM Text:"
427 << "\033[0m"
428 << "\n"
429 << "\033[2m" << text_content << "\033[0m"
430 << "\n\n";
431 }
432
433 // Try to parse structured command format if present in text
434 // (similar to OpenAI logic)
435
436 // Strip markdown code blocks
437 std::string clean_text =
438 std::string(absl::StripAsciiWhitespace(text_content));
439 if (absl::StartsWith(clean_text, "```json")) {
440 clean_text = clean_text.substr(7);
441 } else if (absl::StartsWith(clean_text, "```")) {
442 clean_text = clean_text.substr(3);
443 }
444 if (absl::EndsWith(clean_text, "```")) {
445 clean_text = clean_text.substr(0, clean_text.length() - 3);
446 }
447 clean_text = std::string(absl::StripAsciiWhitespace(clean_text));
448
449 // Try to parse as JSON object
450 auto parsed_text = nlohmann::json::parse(clean_text, nullptr, false);
451 if (!parsed_text.is_discarded()) {
452 if (parsed_text.contains("text_response") &&
453 parsed_text["text_response"].is_string()) {
454 agent_response.text_response =
455 parsed_text["text_response"].get<std::string>();
456 }
457 if (parsed_text.contains("commands") &&
458 parsed_text["commands"].is_array()) {
459 for (const auto& cmd : parsed_text["commands"]) {
460 if (cmd.is_string()) {
461 std::string command = cmd.get<std::string>();
462 if (absl::StartsWith(command, "z3ed ")) {
463 command = command.substr(5);
464 }
465 agent_response.commands.push_back(command);
466 }
467 }
468 }
469 } else {
470 // Use raw text as response if JSON parsing fails
471 if (agent_response.text_response.empty()) {
472 agent_response.text_response = text_content;
473 } else {
474 agent_response.text_response += "\n\n" + text_content;
475 }
476 }
477 } else if (type == "tool_use") {
478 ToolCall tool_call;
479 tool_call.tool_name = block.value("name", "");
480
481 if (block.contains("input") && block["input"].is_object()) {
482 for (auto& [key, value] : block["input"].items()) {
483 if (value.is_string()) {
484 tool_call.args[key] = value.get<std::string>();
485 } else if (value.is_number()) {
486 tool_call.args[key] = std::to_string(value.get<double>());
487 } else if (value.is_boolean()) {
488 tool_call.args[key] = value.get<bool>() ? "true" : "false";
489 }
490 }
491 }
492 agent_response.tool_calls.push_back(tool_call);
493 }
494 }
495
496 if (agent_response.text_response.empty() && agent_response.commands.empty() &&
497 agent_response.tool_calls.empty()) {
498 return absl::InternalError(
499 "❌ No valid response extracted from Anthropic\n"
500 " Expected text or tool use");
501 }
502
503 return agent_response;
504#endif
505}
506
507#endif // YAZE_AI_RUNTIME_AVAILABLE
508
509} // namespace cli
510} // namespace yaze
AnthropicAIService(const AnthropicConfig &)