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