yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
ollama_ai_service.cc
Go to the documentation of this file.
2
3#include <cstdlib>
4#include <iostream>
5
6#include "absl/strings/str_cat.h"
7#include "absl/strings/str_format.h"
9
10#ifdef YAZE_WITH_JSON
11#include "httplib.h"
12#include "nlohmann/json.hpp"
13#endif
14
15namespace yaze {
16namespace cli {
17
18OllamaAIService::OllamaAIService(const OllamaConfig& config) : config_(config) {
19 // Load command documentation into prompt builder
20 if (auto status = prompt_builder_.LoadResourceCatalogue(""); !status.ok()) {
21 std::cerr << "⚠️ Failed to load agent prompt catalogue: "
22 << status.message() << std::endl;
23 }
24
25 if (config_.system_prompt.empty()) {
26 // Use enhanced prompting by default
29 } else {
31 }
32 }
33}
34
36 // Fallback prompt if enhanced prompting is disabled
37 // Use PromptBuilder's basic system instruction
39}
40
44
46#ifndef YAZE_WITH_JSON
47 return absl::UnimplementedError(
48 "Ollama service requires JSON support. "
49 "Build with -DZ3ED_AI=ON or -DYAZE_WITH_JSON=ON");
50#else
51 try {
52 httplib::Client cli(config_.base_url);
53 cli.set_connection_timeout(5); // 5 second timeout
54
55 auto res = cli.Get("/api/tags");
56 if (!res) {
57 return absl::UnavailableError(absl::StrFormat(
58 "Cannot connect to Ollama server at %s.\n"
59 "Make sure Ollama is installed and running:\n"
60 " 1. Install: brew install ollama (macOS) or https://ollama.com/download\n"
61 " 2. Start: ollama serve\n"
62 " 3. Verify: curl http://localhost:11434/api/tags",
64 }
65
66 if (res->status != 200) {
67 return absl::InternalError(absl::StrFormat(
68 "Ollama server error: HTTP %d\nResponse: %s",
69 res->status, res->body));
70 }
71
72 // Check if requested model is available
73 nlohmann::json models_json = nlohmann::json::parse(res->body);
74 bool model_found = false;
75
76 if (models_json.contains("models") && models_json["models"].is_array()) {
77 for (const auto& model : models_json["models"]) {
78 if (model.contains("name")) {
79 std::string model_name = model["name"].get<std::string>();
80 if (model_name.find(config_.model) != std::string::npos) {
81 model_found = true;
82 break;
83 }
84 }
85 }
86 }
87
88 if (!model_found) {
89 return absl::NotFoundError(absl::StrFormat(
90 "Model '%s' not found on Ollama server.\n"
91 "Pull it with: ollama pull %s\n"
92 "Available models: ollama list",
94 }
95
96 return absl::OkStatus();
97 } catch (const std::exception& e) {
98 return absl::InternalError(absl::StrCat(
99 "Ollama health check failed: ", e.what()));
100 }
101#endif
102}
103
104absl::StatusOr<std::vector<std::string>> OllamaAIService::ListAvailableModels() {
105#ifndef YAZE_WITH_JSON
106 return absl::UnimplementedError("Requires httplib and JSON support");
107#else
108 try {
109 httplib::Client cli(config_.base_url);
110 cli.set_connection_timeout(5);
111
112 auto res = cli.Get("/api/tags");
113
114 if (!res || res->status != 200) {
115 return absl::UnavailableError(
116 "Cannot list Ollama models. Is the server running?");
117 }
118
119 nlohmann::json models_json = nlohmann::json::parse(res->body);
120 std::vector<std::string> models;
121
122 if (models_json.contains("models") && models_json["models"].is_array()) {
123 for (const auto& model : models_json["models"]) {
124 if (model.contains("name")) {
125 models.push_back(model["name"].get<std::string>());
126 }
127 }
128 }
129
130 return models;
131 } catch (const std::exception& e) {
132 return absl::InternalError(absl::StrCat(
133 "Failed to list models: ", e.what()));
134 }
135#endif
136}
137
138absl::StatusOr<std::string> OllamaAIService::ParseOllamaResponse(
139 const std::string& json_response) {
140#if !YAZE_HAS_JSON
141 return absl::UnimplementedError("Requires JSON support");
142#else
143 try {
144 nlohmann::json response_json = nlohmann::json::parse(json_response);
145
146 if (!response_json.contains("response")) {
147 return absl::InvalidArgumentError(
148 "Ollama response missing 'response' field");
149 }
150
151 return response_json["response"].get<std::string>();
152 } catch (const nlohmann::json::exception& e) {
153 return absl::InternalError(absl::StrCat(
154 "Failed to parse Ollama response: ", e.what()));
155 }
156#endif
157}
158
159absl::StatusOr<AgentResponse> OllamaAIService::GenerateResponse(
160 const std::string& prompt) {
161 return GenerateResponse({{{agent::ChatMessage::Sender::kUser, prompt, absl::Now()}}});
162}
163
164absl::StatusOr<AgentResponse> OllamaAIService::GenerateResponse(
165 const std::vector<agent::ChatMessage>& history) {
166#ifndef YAZE_WITH_JSON
167 return absl::UnimplementedError(
168 "Ollama service requires httplib and JSON support. "
169 "Install vcpkg dependencies or use bundled libraries.");
170#else
171 // TODO: Implement history-aware prompting.
172 if (history.empty()) {
173 return absl::InvalidArgumentError("History cannot be empty.");
174 }
175 std::string prompt = prompt_builder_.BuildPromptFromHistory(history);
176
177 // Build request payload
178 nlohmann::json request_body = {
179 {"model", config_.model},
180 {"system", config_.system_prompt},
181 {"prompt", prompt},
182 {"stream", false},
183 {"options",
184 {{"temperature", config_.temperature},
185 {"num_predict", config_.max_tokens}}},
186 {"format", "json"} // Force JSON output
187 };
188
189 try {
190 httplib::Client cli(config_.base_url);
191 cli.set_read_timeout(60); // Longer timeout for inference
192
193 auto res = cli.Post("/api/generate", request_body.dump(), "application/json");
194
195 if (!res) {
196 return absl::UnavailableError(
197 "Failed to connect to Ollama. Is 'ollama serve' running?\n"
198 "Start with: ollama serve");
199 }
200
201 if (res->status != 200) {
202 return absl::InternalError(absl::StrFormat(
203 "Ollama API error: HTTP %d\nResponse: %s",
204 res->status, res->body));
205 }
206
207 // Parse Ollama's wrapper JSON
208 nlohmann::json ollama_wrapper;
209 try {
210 ollama_wrapper = nlohmann::json::parse(res->body);
211 } catch (const nlohmann::json::exception& e) {
212 return absl::InternalError(absl::StrFormat(
213 "Failed to parse Ollama response: %s\nBody: %s",
214 e.what(), res->body));
215 }
216
217 // Extract the LLM's response from Ollama's "response" field
218 if (!ollama_wrapper.contains("response") || !ollama_wrapper["response"].is_string()) {
219 return absl::InvalidArgumentError(
220 "Ollama response missing 'response' field");
221 }
222
223 std::string llm_output = ollama_wrapper["response"].get<std::string>();
224
225 // Debug: Print raw LLM output when verbose mode is enabled
226 const char* verbose_env = std::getenv("Z3ED_VERBOSE");
227 if (verbose_env && std::string(verbose_env) == "1") {
228 std::cout << "\n" << "\033[35m" << "🔍 Raw LLM Response:" << "\033[0m" << "\n"
229 << "\033[2m" << llm_output << "\033[0m" << "\n\n";
230 }
231
232 // Parse the LLM's JSON response (the agent structure)
233 nlohmann::json response_json;
234 try {
235 response_json = nlohmann::json::parse(llm_output);
236 } catch (const nlohmann::json::exception& e) {
237 // Sometimes the LLM includes extra text - try to extract JSON object
238 size_t start = llm_output.find('{');
239 size_t end = llm_output.rfind('}');
240
241 if (start != std::string::npos && end != std::string::npos && end > start) {
242 std::string json_only = llm_output.substr(start, end - start + 1);
243 try {
244 response_json = nlohmann::json::parse(json_only);
245 } catch (const nlohmann::json::exception&) {
246 return absl::InvalidArgumentError(
247 "LLM did not return valid JSON. Response:\n" + llm_output);
248 }
249 } else {
250 return absl::InvalidArgumentError(
251 "LLM did not return a JSON object. Response:\n" + llm_output);
252 }
253 }
254
255 AgentResponse agent_response;
256 if (response_json.contains("text_response") &&
257 response_json["text_response"].is_string()) {
258 agent_response.text_response =
259 response_json["text_response"].get<std::string>();
260 }
261 if (response_json.contains("reasoning") &&
262 response_json["reasoning"].is_string()) {
263 agent_response.reasoning = response_json["reasoning"].get<std::string>();
264 }
265 if (response_json.contains("tool_calls") &&
266 response_json["tool_calls"].is_array()) {
267 for (const auto& call : response_json["tool_calls"]) {
268 if (call.contains("tool_name") && call["tool_name"].is_string()) {
269 ToolCall tool_call;
270 tool_call.tool_name = call["tool_name"].get<std::string>();
271 if (call.contains("args") && call["args"].is_object()) {
272 for (auto& [key, value] : call["args"].items()) {
273 if (value.is_string()) {
274 tool_call.args[key] = value.get<std::string>();
275 }
276 }
277 }
278 agent_response.tool_calls.push_back(tool_call);
279 }
280 }
281 }
282 if (response_json.contains("commands") &&
283 response_json["commands"].is_array()) {
284 for (const auto& cmd : response_json["commands"]) {
285 if (cmd.is_string()) {
286 agent_response.commands.push_back(cmd.get<std::string>());
287 }
288 }
289 }
290
291 return agent_response;
292
293 } catch (const std::exception& e) {
294 return absl::InternalError(
295 absl::StrCat("Ollama request failed: ", e.what()));
296 }
297#endif
298}
299
300} // namespace cli
301} // namespace yaze
The Rom class is used to load, save, and modify Rom data.
Definition rom.h:71
void SetRomContext(Rom *rom) override
absl::StatusOr< AgentResponse > GenerateResponse(const std::string &prompt) override
absl::StatusOr< std::vector< std::string > > ListAvailableModels()
absl::StatusOr< std::string > ParseOllamaResponse(const std::string &json_response)
OllamaAIService(const OllamaConfig &config)
std::string BuildSystemInstructionWithExamples()
std::string BuildPromptFromHistory(const std::vector< agent::ChatMessage > &history)
std::string BuildSystemInstruction()
absl::Status LoadResourceCatalogue(const std::string &yaml_path)
Main namespace for the application.
std::vector< std::string > commands
Definition common.h:26
std::string reasoning
Definition common.h:29
std::vector< ToolCall > tool_calls
Definition common.h:23
std::string text_response
Definition common.h:20
std::map< std::string, std::string > args
Definition common.h:14
std::string tool_name
Definition common.h:13