yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
openai_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/match.h"
12#include "absl/strings/str_cat.h"
13#include "absl/strings/str_format.h"
14#include "absl/strings/str_split.h"
15#include "absl/strings/strip.h"
16#include "absl/time/clock.h"
17#include "absl/time/time.h"
22#include "util/platform_paths.h"
23
24#if defined(__APPLE__)
25#include <TargetConditionals.h>
26#endif
27
28#if defined(__APPLE__) && \
29 (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
31#define YAZE_AI_IOS_URLSESSION 1
32#endif
33
34#ifdef YAZE_WITH_JSON
35#include <filesystem>
36#include <fstream>
37
38#include "httplib.h"
39#include "nlohmann/json.hpp"
40
41// OpenSSL initialization for HTTPS support
42#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
43#include <openssl/crypto.h>
44#include <openssl/err.h>
45#include <openssl/ssl.h>
46
47// OpenSSL initialization guards (local to this TU)
48static std::atomic<bool> g_openssl_initialized{false};
49static std::mutex g_openssl_init_mutex;
50
51static void EnsureOpenSSLInitialized() {
52 std::lock_guard<std::mutex> lock(g_openssl_init_mutex);
53 if (!g_openssl_initialized.exchange(true)) {
54 OPENSSL_init_ssl(
55 OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS,
56 nullptr);
57 std::cerr << "✓ OpenSSL initialized for HTTPS support" << std::endl;
58 }
59}
60#endif
61#endif
62
63namespace yaze {
64namespace cli {
65
66#ifdef YAZE_AI_RUNTIME_AVAILABLE
67
68namespace {
69
70absl::StatusOr<nlohmann::json> BuildOpenAIToolPayload(
71 const PromptBuilder& prompt_builder) {
72 auto declarations_or =
74 if (!declarations_or.ok()) {
75 return declarations_or.status();
76 }
77 return ToolSchemaBuilder::BuildOpenAITools(*declarations_or);
78}
79
80} // namespace
81
82OpenAIAIService::OpenAIAIService(const OpenAIConfig& config)
83 : function_calling_enabled_(config.use_function_calling), config_(config) {
84 if (config_.verbose) {
85 std::cerr << "[DEBUG] Initializing OpenAI service..." << std::endl;
86 std::cerr << "[DEBUG] Model: " << config_.model << std::endl;
87 std::cerr << "[DEBUG] Function calling: "
88 << (function_calling_enabled_ ? "enabled" : "disabled")
89 << std::endl;
90 }
91
92#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
93 EnsureOpenSSLInitialized();
94 if (config_.verbose) {
95 std::cerr << "[DEBUG] OpenSSL initialized for HTTPS" << std::endl;
96 }
97#endif
98
99 // Load command documentation into prompt builder
100 std::string catalogue_path = config_.prompt_version == "v2"
101 ? "assets/agent/prompt_catalogue_v2.yaml"
102 : "assets/agent/prompt_catalogue.yaml";
103 if (auto status = prompt_builder_.LoadResourceCatalogue(catalogue_path);
104 !status.ok()) {
105 std::cerr << "⚠️ Failed to load agent prompt catalogue: "
106 << status.message() << std::endl;
107 }
108
109 if (config_.system_instruction.empty()) {
110 // Load system prompt file
111 std::string prompt_file;
112 if (config_.prompt_version == "v3") {
113 prompt_file = "agent/system_prompt_v3.txt";
114 } else if (config_.prompt_version == "v2") {
115 prompt_file = "agent/system_prompt_v2.txt";
116 } else {
117 prompt_file = "agent/system_prompt.txt";
118 }
119
120 auto prompt_path = util::PlatformPaths::FindAsset(prompt_file);
121 if (prompt_path.ok()) {
122 std::ifstream file(prompt_path->string());
123 if (file.good()) {
124 std::stringstream buffer;
125 buffer << file.rdbuf();
126 config_.system_instruction = buffer.str();
127 if (config_.verbose) {
128 std::cerr << "[DEBUG] Loaded prompt: " << prompt_path->string()
129 << std::endl;
130 }
131 }
132 }
133
134 if (config_.system_instruction.empty()) {
135 config_.system_instruction = BuildSystemInstruction();
136 }
137 }
138
139 if (config_.verbose) {
140 std::cerr << "[DEBUG] OpenAI service initialized" << std::endl;
141 }
142}
143
144void OpenAIAIService::EnableFunctionCalling(bool enable) {
145 function_calling_enabled_ = enable;
146}
147
148std::vector<std::string> OpenAIAIService::GetAvailableTools() const {
149 return {"resource-list", "resource-search",
150 "dungeon-list-sprites", "dungeon-describe-room",
151 "overworld-find-tile", "overworld-describe-map",
152 "overworld-list-warps"};
153}
154
155std::string OpenAIAIService::BuildSystemInstruction() {
156 return prompt_builder_.BuildSystemInstruction();
157}
158
159void OpenAIAIService::SetRomContext(Rom* rom) {
160 prompt_builder_.SetRom(rom);
161}
162
163absl::StatusOr<std::vector<ModelInfo>> OpenAIAIService::ListAvailableModels() {
164#ifndef YAZE_WITH_JSON
165 return absl::UnimplementedError("OpenAI AI service requires JSON support");
166#else
167 const bool is_openai_cloud =
168 absl::StrContains(config_.base_url, "api.openai.com");
169 if (config_.api_key.empty() && is_openai_cloud) {
170 // Return default known models if API key is missing
171 std::vector<ModelInfo> defaults = {
172 {.name = "gpt-4o",
173 .display_name = "GPT-4o",
174 .provider = kProviderOpenAi,
175 .description = "Most capable GPT-4 model"},
176 {.name = "gpt-4o-mini",
177 .display_name = "GPT-4o Mini",
178 .provider = kProviderOpenAi,
179 .description = "Fast and cost-effective"},
180 {.name = "gpt-4-turbo",
181 .display_name = "GPT-4 Turbo",
182 .provider = kProviderOpenAi,
183 .description = "GPT-4 with larger context"},
184 {.name = "gpt-3.5-turbo",
185 .display_name = "GPT-3.5 Turbo",
186 .provider = kProviderOpenAi,
187 .description = "Fast and efficient"}};
188 return defaults;
189 }
190
191 try {
192 if (config_.verbose) {
193 std::cerr << "[DEBUG] Listing OpenAI models..." << std::endl;
194 }
195
196 std::string response_str;
197#if defined(YAZE_AI_IOS_URLSESSION)
198 std::map<std::string, std::string> headers;
199 if (!config_.api_key.empty()) {
200 headers.emplace("Authorization", "Bearer " + config_.api_key);
201 }
202 auto resp_or = ios::UrlSessionHttpRequest(
203 "GET", config_.base_url + "/v1/models", headers, "", 8000);
204 if (!resp_or.ok()) {
205 if (config_.verbose) {
206 std::cerr << "[DEBUG] OpenAI /v1/models failed: "
207 << resp_or.status().message() << std::endl;
208 }
209 // Return defaults on failure so the UI remains usable.
210 std::vector<ModelInfo> defaults = {{.name = "gpt-4o-mini",
211 .display_name = "GPT-4o Mini",
212 .provider = kProviderOpenAi},
213 {.name = "gpt-4o",
214 .display_name = "GPT-4o",
215 .provider = kProviderOpenAi},
216 {.name = "gpt-3.5-turbo",
217 .display_name = "GPT-3.5 Turbo",
218 .provider = kProviderOpenAi}};
219 return defaults;
220 }
221 if (resp_or->status_code != 200) {
222 if (config_.verbose) {
223 std::cerr << "[DEBUG] OpenAI /v1/models HTTP " << resp_or->status_code
224 << std::endl;
225 }
226 std::vector<ModelInfo> defaults = {{.name = "gpt-4o-mini",
227 .display_name = "GPT-4o Mini",
228 .provider = kProviderOpenAi},
229 {.name = "gpt-4o",
230 .display_name = "GPT-4o",
231 .provider = kProviderOpenAi},
232 {.name = "gpt-3.5-turbo",
233 .display_name = "GPT-3.5 Turbo",
234 .provider = kProviderOpenAi}};
235 return defaults;
236 }
237 response_str = resp_or->body;
238#else
239 // Use curl to list models from the API
240 std::string auth_header =
241 config_.api_key.empty()
242 ? ""
243 : "-H 'Authorization: Bearer " + config_.api_key + "' ";
244 std::string curl_cmd = "curl -s -X GET '" + config_.base_url +
245 "/v1/models' " + auth_header + "2>&1";
246
247#ifdef _WIN32
248 FILE* pipe = _popen(curl_cmd.c_str(), "r");
249#else
250 FILE* pipe = popen(curl_cmd.c_str(), "r");
251#endif
252 if (!pipe) {
253 return absl::InternalError("Failed to execute curl command");
254 }
255
256 char buffer[4096];
257 while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
258 response_str += buffer;
259 }
260
261#ifdef _WIN32
262 _pclose(pipe);
263#else
264 pclose(pipe);
265#endif
266#endif // YAZE_AI_IOS_URLSESSION
267
268 auto models_json = nlohmann::json::parse(response_str, nullptr, false);
269 if (models_json.is_discarded()) {
270 return absl::InternalError("Failed to parse OpenAI models JSON");
271 }
272
273 if (!models_json.contains("data")) {
274 // Return defaults on error
275 std::vector<ModelInfo> defaults = {{.name = "gpt-4o-mini",
276 .display_name = "GPT-4o Mini",
277 .provider = kProviderOpenAi},
278 {.name = "gpt-4o",
279 .display_name = "GPT-4o",
280 .provider = kProviderOpenAi},
281 {.name = "gpt-3.5-turbo",
282 .display_name = "GPT-3.5 Turbo",
283 .provider = kProviderOpenAi}};
284 return defaults;
285 }
286
287 std::vector<ModelInfo> models;
288 for (const auto& m : models_json["data"]) {
289 std::string id = m.value("id", "");
290
291 // Filter for chat models (gpt-4*, gpt-3.5-turbo*, o1*, chatgpt*)
292 // For local servers (LM Studio), we accept all models.
293 bool is_local = !absl::StrContains(config_.base_url, "api.openai.com");
294
295 if (is_local || absl::StartsWith(id, "gpt-4") ||
296 absl::StartsWith(id, "gpt-3.5") || absl::StartsWith(id, "o1") ||
297 absl::StartsWith(id, "chatgpt")) {
298 ModelInfo info;
299 info.name = id;
300 info.display_name = id;
301 info.provider = kProviderOpenAi;
302 info.family = is_local ? "local" : "gpt";
303 info.is_local = is_local;
304
305 // Set display name based on model
306 if (id == "gpt-4o")
307 info.display_name = "GPT-4o";
308 else if (id == "gpt-4o-mini")
309 info.display_name = "GPT-4o Mini";
310 else if (id == "gpt-4-turbo")
311 info.display_name = "GPT-4 Turbo";
312 else if (id == "gpt-3.5-turbo")
313 info.display_name = "GPT-3.5 Turbo";
314 else if (id == "o1-preview")
315 info.display_name = "o1 Preview";
316 else if (id == "o1-mini")
317 info.display_name = "o1 Mini";
318
319 models.push_back(std::move(info));
320 }
321 }
322 return models;
323
324 } catch (const std::exception& e) {
325 return absl::InternalError(
326 absl::StrCat("Failed to list models: ", e.what()));
327 }
328#endif
329}
330
331absl::Status OpenAIAIService::CheckAvailability() {
332#ifndef YAZE_WITH_JSON
333 return absl::UnimplementedError(
334 "OpenAI AI service requires JSON support. Build with "
335 "-DYAZE_WITH_JSON=ON");
336#else
337 try {
338 // LMStudio and other local servers don't require API keys
339 bool is_local_server = config_.base_url != "https://api.openai.com";
340 if (config_.api_key.empty() && !is_local_server) {
341 return absl::FailedPreconditionError(
342 "❌ OpenAI API key not configured\n"
343 " Set OPENAI_API_KEY environment variable\n"
344 " Get your API key at: https://platform.openai.com/api-keys\n"
345 " For LMStudio, use --openai_base_url=http://localhost:1234");
346 }
347
348 // Test API connectivity with a simple request
349#if defined(YAZE_AI_IOS_URLSESSION)
350 std::map<std::string, std::string> headers;
351 if (!config_.api_key.empty()) {
352 headers.emplace("Authorization", "Bearer " + config_.api_key);
353 }
354 auto resp_or = ios::UrlSessionHttpRequest(
355 "GET", config_.base_url + "/v1/models", headers, "", 8000);
356 if (!resp_or.ok()) {
357 return absl::UnavailableError(absl::StrCat(
358 "❌ Cannot reach OpenAI API\n ", resp_or.status().message()));
359 }
360 if (resp_or->status_code == 401) {
361 return absl::PermissionDeniedError(
362 "❌ Invalid OpenAI API key\n"
363 " Verify your key at: https://platform.openai.com/api-keys");
364 }
365 if (resp_or->status_code != 200) {
366 return absl::InternalError(
367 absl::StrCat("❌ OpenAI API error: ", resp_or->status_code, "\n ",
368 resp_or->body));
369 }
370#else
371 httplib::Client cli(config_.base_url);
372 cli.set_connection_timeout(5, 0);
373
374 httplib::Headers headers = {};
375 if (!config_.api_key.empty()) {
376 headers.emplace("Authorization", "Bearer " + config_.api_key);
377 }
378
379 auto res = cli.Get("/v1/models", headers);
380
381 if (!res) {
382 return absl::UnavailableError(
383 "❌ Cannot reach OpenAI API\n"
384 " Check your internet connection");
385 }
386
387 if (res->status == 401) {
388 return absl::PermissionDeniedError(
389 "❌ Invalid OpenAI API key\n"
390 " Verify your key at: https://platform.openai.com/api-keys");
391 }
392
393 if (res->status != 200) {
394 return absl::InternalError(absl::StrCat(
395 "❌ OpenAI API error: ", res->status, "\n ", res->body));
396 }
397#endif
398
399 return absl::OkStatus();
400 } catch (const std::exception& e) {
401 return absl::InternalError(
402 absl::StrCat("Exception during availability check: ", e.what()));
403 }
404#endif
405}
406
407absl::StatusOr<AgentResponse> OpenAIAIService::GenerateResponse(
408 const std::string& prompt) {
409 return GenerateResponse(
410 {{{agent::ChatMessage::Sender::kUser, prompt, absl::Now()}}});
411}
412
413absl::StatusOr<AgentResponse> OpenAIAIService::GenerateResponse(
414 const std::vector<agent::ChatMessage>& history) {
415#ifndef YAZE_WITH_JSON
416 return absl::UnimplementedError(
417 "OpenAI AI service requires JSON support. Build with "
418 "-DYAZE_WITH_JSON=ON");
419#else
420 if (history.empty()) {
421 return absl::InvalidArgumentError("History cannot be empty.");
422 }
423
424 const bool is_openai_cloud =
425 absl::StrContains(config_.base_url, "api.openai.com");
426 if (config_.api_key.empty() && is_openai_cloud) {
427 return absl::FailedPreconditionError("OpenAI API key not configured");
428 }
429
430 absl::Time request_start = absl::Now();
431
432 try {
433 if (config_.verbose) {
434 std::cerr << "[DEBUG] Using curl for OpenAI HTTPS request" << std::endl;
435 std::cerr << "[DEBUG] Processing " << history.size()
436 << " messages in history" << std::endl;
437 }
438
439 // Build messages array for OpenAI format
440 nlohmann::json messages = nlohmann::json::array();
441
442 // Add system message
443 messages.push_back(
444 {{"role", "system"}, {"content", config_.system_instruction}});
445
446 // Add conversation history (up to last 10 messages for context window)
447 int start_idx = std::max(0, static_cast<int>(history.size()) - 10);
448 for (size_t i = start_idx; i < history.size(); ++i) {
449 const auto& msg = history[i];
450 std::string role = (msg.sender == agent::ChatMessage::Sender::kUser)
451 ? "user"
452 : "assistant";
453
454 messages.push_back({{"role", role}, {"content", msg.message}});
455 }
456
457 // Build request body
458 nlohmann::json request_body = {{"model", config_.model},
459 {"messages", messages},
460 {"temperature", config_.temperature},
461 {"max_tokens", config_.max_output_tokens}};
462
463 // Add function calling tools if enabled
464 if (function_calling_enabled_) {
465 auto tools_or = BuildOpenAIToolPayload(prompt_builder_);
466 if (!tools_or.ok()) {
467 if (config_.verbose) {
468 std::cerr << "[DEBUG] Function calling schemas unavailable: "
469 << tools_or.status().message() << std::endl;
470 }
471 } else if (!tools_or->empty()) {
472 if (config_.verbose) {
473 std::string tools_str = tools_or->dump();
474 std::cerr << "[DEBUG] Function calling schemas: "
475 << tools_str.substr(0, 200) << "..." << std::endl;
476 }
477
478 request_body["tools"] = *tools_or;
479 }
480 }
481
482 if (config_.verbose) {
483 std::cerr << "[DEBUG] Sending " << messages.size()
484 << " messages to OpenAI" << std::endl;
485 }
486
487 std::string response_str;
488#if defined(YAZE_AI_IOS_URLSESSION)
489 std::map<std::string, std::string> headers;
490 headers.emplace("Content-Type", "application/json");
491 if (!config_.api_key.empty()) {
492 headers.emplace("Authorization", "Bearer " + config_.api_key);
493 }
494 auto resp_or = ios::UrlSessionHttpRequest(
495 "POST", config_.base_url + "/v1/chat/completions", headers,
496 request_body.dump(), 60000);
497 if (!resp_or.ok()) {
498 return resp_or.status();
499 }
500 if (resp_or->status_code == 401) {
501 return absl::PermissionDeniedError(
502 "❌ Invalid OpenAI API key\n"
503 " Verify your key at: https://platform.openai.com/api-keys");
504 }
505 if (resp_or->status_code != 200) {
506 return absl::InternalError(
507 absl::StrCat("❌ OpenAI API error: ", resp_or->status_code, "\n ",
508 resp_or->body));
509 }
510 response_str = resp_or->body;
511#else
512 // Write request body to temp file
513 std::string temp_file = "/tmp/openai_request.json";
514 std::ofstream out(temp_file);
515 out << request_body.dump();
516 out.close();
517
518 // Use curl to make the request
519 std::string auth_header =
520 config_.api_key.empty()
521 ? ""
522 : "-H 'Authorization: Bearer " + config_.api_key + "' ";
523 std::string curl_cmd = "curl -s -X POST '" + config_.base_url +
524 "/v1/chat/completions' "
525 "-H 'Content-Type: application/json' " +
526 auth_header + "-d @" + temp_file + " 2>&1";
527
528 if (config_.verbose) {
529 std::cerr << "[DEBUG] Executing OpenAI API request..." << std::endl;
530 }
531
532#ifdef _WIN32
533 FILE* pipe = _popen(curl_cmd.c_str(), "r");
534#else
535 FILE* pipe = popen(curl_cmd.c_str(), "r");
536#endif
537 if (!pipe) {
538 return absl::InternalError("Failed to execute curl command");
539 }
540
541 char buffer[4096];
542 while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
543 response_str += buffer;
544 }
545
546#ifdef _WIN32
547 int status = _pclose(pipe);
548#else
549 int status = pclose(pipe);
550#endif
551 std::remove(temp_file.c_str());
552
553 if (status != 0) {
554 return absl::InternalError(
555 absl::StrCat("Curl failed with status ", status));
556 }
557#endif // YAZE_AI_IOS_URLSESSION
558
559 if (response_str.empty()) {
560 return absl::InternalError("Empty response from OpenAI API");
561 }
562
563 if (config_.verbose) {
564 std::cout << "\n"
565 << "\033[35m"
566 << "🔍 Raw OpenAI API Response:"
567 << "\033[0m"
568 << "\n"
569 << "\033[2m" << response_str.substr(0, 500) << "\033[0m"
570 << "\n\n";
571 }
572
573 if (config_.verbose) {
574 std::cerr << "[DEBUG] Parsing response..." << std::endl;
575 }
576
577 auto parsed_or = ParseOpenAIResponse(response_str);
578 if (!parsed_or.ok()) {
579 return parsed_or.status();
580 }
581
582 AgentResponse agent_response = std::move(parsed_or.value());
583 agent_response.provider = kProviderOpenAi;
584 agent_response.model = config_.model;
585 agent_response.latency_seconds =
586 absl::ToDoubleSeconds(absl::Now() - request_start);
587 agent_response.parameters["prompt_version"] = config_.prompt_version;
588 agent_response.parameters["temperature"] =
589 absl::StrFormat("%.2f", config_.temperature);
590 agent_response.parameters["max_output_tokens"] =
591 absl::StrFormat("%d", config_.max_output_tokens);
592 agent_response.parameters["function_calling"] =
593 function_calling_enabled_ ? "true" : "false";
594
595 return agent_response;
596
597 } catch (const std::exception& e) {
598 if (config_.verbose) {
599 std::cerr << "[ERROR] Exception: " << e.what() << std::endl;
600 }
601 return absl::InternalError(
602 absl::StrCat("Exception during generation: ", e.what()));
603 }
604#endif
605}
606
607absl::StatusOr<AgentResponse> OpenAIAIService::ParseOpenAIResponse(
608 const std::string& response_body) {
609#ifndef YAZE_WITH_JSON
610 return absl::UnimplementedError("JSON support required");
611#else
612 AgentResponse agent_response;
613
614 auto response_json = nlohmann::json::parse(response_body, nullptr, false);
615 if (response_json.is_discarded()) {
616 return absl::InternalError("❌ Failed to parse OpenAI response JSON");
617 }
618
619 // Check for errors
620 if (response_json.contains("error")) {
621 std::string error_msg =
622 response_json["error"].value("message", "Unknown error");
623 return absl::InternalError(
624 absl::StrCat("❌ OpenAI API error: ", error_msg));
625 }
626
627 // Navigate OpenAI's response structure
628 if (!response_json.contains("choices") || response_json["choices"].empty()) {
629 return absl::InternalError("❌ No choices in OpenAI response");
630 }
631
632 const auto& choice = response_json["choices"][0];
633 if (!choice.contains("message")) {
634 return absl::InternalError("❌ No message in OpenAI response");
635 }
636
637 const auto& message = choice["message"];
638
639 // Extract text content
640 if (message.contains("content") && !message["content"].is_null()) {
641 std::string text_content = message["content"].get<std::string>();
642
643 if (config_.verbose) {
644 std::cout << "\n"
645 << "\033[35m"
646 << "🔍 Raw LLM Response:"
647 << "\033[0m"
648 << "\n"
649 << "\033[2m" << text_content << "\033[0m"
650 << "\n\n";
651 }
652
653 // Strip markdown code blocks if present
654 text_content = std::string(absl::StripAsciiWhitespace(text_content));
655 if (absl::StartsWith(text_content, "```json")) {
656 text_content = text_content.substr(7);
657 } else if (absl::StartsWith(text_content, "```")) {
658 text_content = text_content.substr(3);
659 }
660 if (absl::EndsWith(text_content, "```")) {
661 text_content = text_content.substr(0, text_content.length() - 3);
662 }
663 text_content = std::string(absl::StripAsciiWhitespace(text_content));
664
665 // Try to parse as JSON object
666 auto parsed_text = nlohmann::json::parse(text_content, nullptr, false);
667 if (!parsed_text.is_discarded()) {
668 // Extract text_response
669 if (parsed_text.contains("text_response") &&
670 parsed_text["text_response"].is_string()) {
671 agent_response.text_response =
672 parsed_text["text_response"].get<std::string>();
673 }
674
675 // Extract reasoning
676 if (parsed_text.contains("reasoning") &&
677 parsed_text["reasoning"].is_string()) {
678 agent_response.reasoning = parsed_text["reasoning"].get<std::string>();
679 }
680
681 // Extract commands
682 if (parsed_text.contains("commands") &&
683 parsed_text["commands"].is_array()) {
684 for (const auto& cmd : parsed_text["commands"]) {
685 if (cmd.is_string()) {
686 std::string command = cmd.get<std::string>();
687 if (absl::StartsWith(command, "z3ed ")) {
688 command = command.substr(5);
689 }
690 agent_response.commands.push_back(command);
691 }
692 }
693 }
694
695 // Extract tool_calls from parsed JSON
696 if (parsed_text.contains("tool_calls") &&
697 parsed_text["tool_calls"].is_array()) {
698 for (const auto& call : parsed_text["tool_calls"]) {
699 if (call.contains("tool_name") && call["tool_name"].is_string()) {
700 ToolCall tool_call;
701 tool_call.tool_name = call["tool_name"].get<std::string>();
702 if (call.contains("args") && call["args"].is_object()) {
703 tool_call.args = ai::DecodeToolCallArguments(call["args"]);
704 }
705 agent_response.tool_calls.push_back(tool_call);
706 }
707 }
708 }
709 } else {
710 // Use raw text as response
711 agent_response.text_response = text_content;
712 }
713 }
714
715 // Handle native OpenAI tool calls
716 if (message.contains("tool_calls") && message["tool_calls"].is_array()) {
717 for (const auto& call : message["tool_calls"]) {
718 if (call.contains("function")) {
719 const auto& func = call["function"];
720 ToolCall tool_call;
721 tool_call.tool_name = func.value("name", "");
722
723 if (func.contains("arguments") && func["arguments"].is_string()) {
724 auto args_json = nlohmann::json::parse(
725 func["arguments"].get<std::string>(), nullptr, false);
726 if (!args_json.is_discarded() && args_json.is_object()) {
727 tool_call.args = ai::DecodeToolCallArguments(args_json);
728 }
729 }
730 agent_response.tool_calls.push_back(tool_call);
731 }
732 }
733 }
734
735 if (agent_response.text_response.empty() && agent_response.commands.empty() &&
736 agent_response.tool_calls.empty()) {
737 return absl::InternalError(
738 "❌ No valid response extracted from OpenAI\n"
739 " Expected at least one of: text_response, commands, or tool_calls");
740 }
741
742 return agent_response;
743#endif
744}
745
746#endif // YAZE_AI_RUNTIME_AVAILABLE
747
748} // namespace cli
749} // namespace yaze
OpenAIAIService(const OpenAIConfig &)
static absl::StatusOr< nlohmann::json > ResolveFunctionDeclarations(const PromptBuilder &prompt_builder)
static nlohmann::json BuildOpenAITools(const nlohmann::json &function_declarations)
constexpr char kProviderOpenAi[]