yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
local_gemini_cli_service.cc
Go to the documentation of this file.
2
3#include <array>
4#include <cstdio>
5#include <iostream>
6#include <memory>
7#include <stdexcept>
8#include <string>
9
10#include "nlohmann/json.hpp"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_split.h"
13#include "absl/strings/strip.h"
14
15namespace yaze {
16namespace cli {
17
19 : model_(model) {}
20
22#ifdef YAZE_AI_RUNTIME_AVAILABLE
23 prompt_builder_.SetRom(rom);
24#else
25 (void)rom;
26#endif
27}
28
29std::string LocalGeminiCliService::EscapeShellArg(const std::string& arg) {
30 // Basic single-quote escaping for Unix shells
31 // ' -> '\''
32 std::string escaped = "'";
33 for (char c : arg) {
34 if (c == '"') {
35 escaped += "'\\''";
36 } else {
37 escaped += c;
38 }
39 }
40 escaped += "'";
41 return escaped;
42}
43
44absl::StatusOr<AgentResponse> LocalGeminiCliService::ExecuteGeminiCli(const std::string& prompt) {
45 std::string cmd = "gemini " + EscapeShellArg(prompt) + " --output-format json --model " + model_;
46
47 // Redirect stderr to /dev/null to avoid cluttering app logs with CLI status messages
48 // cmd += " 2>/dev/null";
49 // Note: Removed redirection for now to debug potential issues if it fails
50
51#ifdef _WIN32
52 FILE* pipe = _popen(cmd.c_str(), "r");
53#else
54 FILE* pipe = popen(cmd.c_str(), "r");
55#endif
56
57 if (!pipe) {
58 return absl::InternalError("popen() failed! Is gemini-cli installed and in PATH?");
59 }
60
61 std::array<char, 128> buffer;
62 std::string result;
63 while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
64 result += buffer.data();
65 }
66
67#ifdef _WIN32
68 int status = _pclose(pipe);
69#else
70 int status = pclose(pipe);
71#endif
72
73 if (status != 0) {
74 return absl::InternalError(absl::StrCat("gemini-cli exited with status ", status, ". Output: ", result));
75 }
76
77 if (result.empty()) {
78 return absl::InternalError("Empty response from gemini-cli");
79 }
80
81 try {
82 // Find the start of JSON object (skip any preamble logs)
83 size_t json_start = result.find('{');
84 if (json_start == std::string::npos) {
85 return absl::InternalError("Invalid JSON response from gemini-cli: " + result);
86 }
87 std::string json_str = result.substr(json_start);
88
89 auto json = nlohmann::json::parse(json_str);
90 AgentResponse response;
91
92 if (json.contains("response")) {
93 response.text_response = json["response"].get<std::string>();
94 } else {
95 return absl::InternalError("JSON response missing 'response' field");
96 }
97
98 response.provider = "gemini-cli";
99 response.model = model_;
100
101 // Parse response for commands (basic heuristic since we don't have the structured parser here yet)
102 // Same logic as ParseGeminiResponse in GeminiAIService
103 std::string text_content = response.text_response;
104
105 // Strip markdown code blocks
106 if (absl::StartsWith(text_content, "```json")) {
107 text_content = text_content.substr(7);
108 } else if (absl::StartsWith(text_content, "```")) {
109 text_content = text_content.substr(3);
110 }
111 if (absl::EndsWith(text_content, "```")) {
112 text_content = text_content.substr(0, text_content.length() - 3);
113 }
114
115 // Try to parse inner JSON if the LLM output JSON
116 auto parsed_inner = nlohmann::json::parse(text_content, nullptr, false);
117 if (!parsed_inner.is_discarded()) {
118 if (parsed_inner.contains("text_response") && parsed_inner["text_response"].is_string()) {
119 response.text_response = parsed_inner["text_response"].get<std::string>();
120 }
121 if (parsed_inner.contains("commands") && parsed_inner["commands"].is_array()) {
122 for (const auto& cmd : parsed_inner["commands"]) {
123 if (cmd.is_string()) {
124 std::string command = cmd.get<std::string>();
125 if (absl::StartsWith(command, "z3ed ")) {
126 command = command.substr(5);
127 }
128 response.commands.push_back(command);
129 }
130 }
131 }
132 } else {
133 // Fallback: Line scanning
134 std::vector<std::string> lines = absl::StrSplit(text_content, '\n');
135 for (const auto& line : lines) {
136 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
137 if (!trimmed.empty() && (absl::StartsWith(trimmed, "z3ed ") ||
138 absl::StartsWith(trimmed, "palette ") ||
139 absl::StartsWith(trimmed, "overworld ") ||
140 absl::StartsWith(trimmed, "sprite ") ||
141 absl::StartsWith(trimmed, "dungeon "))) {
142 if (absl::StartsWith(trimmed, "z3ed ")) {
143 trimmed = trimmed.substr(5);
144 }
145 response.commands.push_back(trimmed);
146 }
147 }
148 }
149
150 return response;
151
152 } catch (const std::exception& e) {
153 return absl::InternalError(absl::StrCat("JSON parse error: ", e.what()));
154 }
155}
156
157absl::StatusOr<AgentResponse> LocalGeminiCliService::GenerateResponse(const std::string& prompt) {
158 return ExecuteGeminiCli(prompt);
159}
160
161absl::StatusOr<AgentResponse> LocalGeminiCliService::GenerateResponse(const std::vector<agent::ChatMessage>& history) {
162#ifdef YAZE_AI_RUNTIME_AVAILABLE
163 std::string full_prompt = prompt_builder_.BuildPromptFromHistory(history);
164 return ExecuteGeminiCli(full_prompt);
165#else
166 return absl::UnimplementedError("AI Runtime disabled");
167#endif
168}
169
170} // namespace cli
171} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
absl::StatusOr< AgentResponse > GenerateResponse(const std::string &prompt) override
LocalGeminiCliService(const std::string &model="gemini-2.5-flash")
std::string EscapeShellArg(const std::string &arg)
absl::StatusOr< AgentResponse > ExecuteGeminiCli(const std::string &prompt)
std::vector< std::string > commands
Definition common.h:26
std::string model
Definition common.h:33
std::string text_response
Definition common.h:20
std::string provider
Definition common.h:32