diff --git a/src/cli/service/ai/gemini_ai_service.cc b/src/cli/service/ai/gemini_ai_service.cc index f311361b..59d35884 100644 --- a/src/cli/service/ai/gemini_ai_service.cc +++ b/src/cli/service/ai/gemini_ai_service.cc @@ -11,13 +11,13 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/strip.h" +#include "util/platform_paths.h" #ifdef YAZE_WITH_JSON #include #include #include "httplib.h" #include "nlohmann/json.hpp" -namespace fs = std::filesystem; // OpenSSL initialization for HTTPS support #ifdef CPPHTTPLIB_OPENSSL_SUPPORT @@ -76,34 +76,29 @@ GeminiAIService::GeminiAIService(const GeminiConfig& config) std::cerr << "[DEBUG] Building system instruction..." << std::endl; } - // Try to load version-specific system prompt file + // Try to load version-specific system prompt file using FindAsset std::string prompt_file; if (config_.prompt_version == "v3") { - prompt_file = "assets/agent/system_prompt_v3.txt"; + prompt_file = "agent/system_prompt_v3.txt"; } else if (config_.prompt_version == "v2") { - prompt_file = "assets/agent/system_prompt_v2.txt"; + prompt_file = "agent/system_prompt_v2.txt"; } else { - prompt_file = "assets/agent/system_prompt.txt"; + prompt_file = "agent/system_prompt.txt"; } - std::vector search_paths = { - prompt_file, - "../" + prompt_file, - "../../" + prompt_file - }; - + auto prompt_path = util::PlatformPaths::FindAsset(prompt_file); bool loaded = false; - for (const auto& path : search_paths) { - std::ifstream file(path); + + if (prompt_path.ok()) { + std::ifstream file(prompt_path->string()); if (file.good()) { std::stringstream buffer; buffer << file.rdbuf(); config_.system_instruction = buffer.str(); if (config_.verbose) { - std::cerr << "[DEBUG] Loaded prompt: " << path << std::endl; + std::cerr << "[DEBUG] Loaded prompt: " << prompt_path->string() << std::endl; } loaded = true; - break; } } @@ -148,38 +143,22 @@ std::string GeminiAIService::BuildFunctionCallSchemas() { return schemas; } - // Fallback: Search for function_schemas.json - const std::vector search_paths = { - "assets/agent/function_schemas.json", - "../assets/agent/function_schemas.json", - "../../assets/agent/function_schemas.json", - }; + // Fallback: Search for function_schemas.json using FindAsset + auto schema_path_or = util::PlatformPaths::FindAsset("agent/function_schemas.json"); - fs::path schema_path; - bool found = false; - - for (const auto& candidate : search_paths) { - fs::path resolved = fs::absolute(candidate); - if (fs::exists(resolved)) { - schema_path = resolved; - found = true; - break; - } - } - - if (!found) { - std::cerr << "⚠️ Function schemas file not found. Tried paths:" << std::endl; - for (const auto& path : search_paths) { - std::cerr << " - " << fs::absolute(path).string() << std::endl; + if (!schema_path_or.ok()) { + if (config_.verbose) { + std::cerr << "⚠️ Function schemas file not found: " + << schema_path_or.status().message() << std::endl; } return "[]"; // Return empty array as fallback } // Load and parse the JSON file - std::ifstream file(schema_path); + std::ifstream file(schema_path_or->string()); if (!file.is_open()) { std::cerr << "⚠️ Failed to open function schemas file: " - << schema_path.string() << std::endl; + << schema_path_or->string() << std::endl; return "[]"; } diff --git a/src/cli/service/ai/prompt_builder.cc b/src/cli/service/ai/prompt_builder.cc index 2ddaeeff..02d15177 100644 --- a/src/cli/service/ai/prompt_builder.cc +++ b/src/cli/service/ai/prompt_builder.cc @@ -11,6 +11,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "nlohmann/json.hpp" +#include "util/platform_paths.h" #include "yaml-cpp/yaml.h" namespace yaze { @@ -18,8 +19,6 @@ namespace cli { namespace { -namespace fs = std::filesystem; - bool IsYamlBool(const std::string& value) { const std::string lower = absl::AsciiStrToLower(value); return lower == "true" || lower == "false" || lower == "yes" || @@ -64,44 +63,6 @@ nlohmann::json YamlToJson(const YAML::Node& node) { } } -std::vector BuildCatalogueSearchPaths(const std::string& explicit_path) { - std::vector paths; - if (!explicit_path.empty()) { - paths.emplace_back(explicit_path); - } - - if (const char* env_path = std::getenv("YAZE_AGENT_CATALOGUE")) { - if (*env_path != '\0') { - paths.emplace_back(env_path); - } - } - - // Try to get executable directory for better path resolution - fs::path exe_dir; - try { - exe_dir = fs::current_path(); - } catch (...) { - exe_dir = "."; - } - - const std::vector defaults = { - "assets/agent/prompt_catalogue.yaml", - "../assets/agent/prompt_catalogue.yaml", - "../../assets/agent/prompt_catalogue.yaml", - "../../../assets/agent/prompt_catalogue.yaml", // From build/bin/ - "../../../../assets/agent/prompt_catalogue.yaml", // From build/bin/yaze.app/Contents/MacOS/ - "../Resources/assets/agent/prompt_catalogue.yaml", // macOS app bundle - "assets/z3ed/prompt_catalogue.yaml", - "../assets/z3ed/prompt_catalogue.yaml", - }; - - for (const auto& candidate : defaults) { - paths.emplace_back(candidate); - } - - return paths; -} - } // namespace PromptBuilder::PromptBuilder() = default; @@ -116,35 +77,35 @@ void PromptBuilder::ClearCatalogData() { absl::StatusOr PromptBuilder::ResolveCataloguePath( const std::string& yaml_path) const { - const auto search_paths = BuildCatalogueSearchPaths(yaml_path); - for (const auto& candidate : search_paths) { - fs::path resolved = candidate; - if (resolved.is_relative()) { - try { - resolved = fs::absolute(resolved); - } catch (const std::exception& e) { - // If we can't resolve the absolute path (e.g., cwd doesn't exist), - // just try the relative path as-is - continue; - } - } - - try { - if (fs::exists(resolved)) { - return resolved.string(); - } - } catch (const std::exception& e) { - // If checking existence fails, just continue to next path - continue; + // If an explicit path is provided, check it first + if (!yaml_path.empty()) { + std::error_code ec; + if (std::filesystem::exists(yaml_path, ec) && !ec) { + return yaml_path; } } - - return absl::NotFoundError( - absl::StrCat("Prompt catalogue not found. Checked paths: ", - absl::StrJoin(search_paths, ", ", - [](std::string* out, const fs::path& path) { - absl::StrAppend(out, path.string()); - }))); + + // Check environment variable override + if (const char* env_path = std::getenv("YAZE_AGENT_CATALOGUE")) { + if (*env_path != '\0') { + std::error_code ec; + if (std::filesystem::exists(env_path, ec) && !ec) { + return std::string(env_path); + } + } + } + + // Use PlatformPaths to find the asset in standard locations + // Try the requested path (default is prompt_catalogue.yaml) + std::string relative_path = yaml_path.empty() ? + "agent/prompt_catalogue.yaml" : yaml_path; + + auto result = util::PlatformPaths::FindAsset(relative_path); + if (result.ok()) { + return result->string(); + } + + return result.status(); } absl::Status PromptBuilder::LoadResourceCatalogue(const std::string& yaml_path) { @@ -548,15 +509,10 @@ std::string PromptBuilder::BuildFewShotExamplesSection() const { } std::string PromptBuilder::BuildConstraintsSection() const { - // Try to load from file first - const std::vector search_paths = { - "assets/agent/tool_calling_instructions.txt", - "../assets/agent/tool_calling_instructions.txt", - "../../assets/agent/tool_calling_instructions.txt", - }; - - for (const auto& path : search_paths) { - std::ifstream file(path); + // Try to load from file first using FindAsset + auto file_path = util::PlatformPaths::FindAsset("agent/tool_calling_instructions.txt"); + if (file_path.ok()) { + std::ifstream file(file_path->string()); if (file.is_open()) { std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); @@ -743,15 +699,10 @@ std::string PromptBuilder::BuildContextSection(const RomContext& context) { } std::string PromptBuilder::BuildSystemInstruction() { - // Try to load from file first - const std::vector search_paths = { - "assets/agent/system_prompt.txt", - "../assets/agent/system_prompt.txt", - "../../assets/agent/system_prompt.txt", - }; - - for (const auto& path : search_paths) { - std::ifstream file(path); + // Try to load from file first using FindAsset + auto file_path = util::PlatformPaths::FindAsset("agent/system_prompt.txt"); + if (file_path.ok()) { + std::ifstream file(file_path->string()); if (file.is_open()) { std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); @@ -876,3 +827,4 @@ std::vector PromptBuilder::GetExamplesForCategory( } // namespace cli } // namespace yaze + diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index 030a06d1..1704aa70 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -89,9 +89,11 @@ add_executable( cli/service/testing/test_suite_writer.cc ) -# ============================================================================ +target_compile_definitions(z3ed PRIVATE YAZE_ASSETS_PATH="${CMAKE_SOURCE_DIR}/assets") + +# ============================================================================ # AI Agent Support (Consolidated via Z3ED_AI flag) -# ============================================================================ +# ============================================================================ if(Z3ED_AI OR YAZE_WITH_JSON) target_compile_definitions(z3ed PRIVATE YAZE_WITH_JSON) message(STATUS "✓ z3ed AI agent enabled (Ollama + Gemini support)") @@ -100,9 +102,9 @@ if(Z3ED_AI OR YAZE_WITH_JSON) target_link_libraries(z3ed PRIVATE nlohmann_json::nlohmann_json) endif() -# ============================================================================ +# ============================================================================ # SSL/HTTPS Support (Optional - Required for Gemini API) -# ============================================================================ +# ============================================================================ # SSL is only enabled when AI features are active # Ollama (localhost) works without SSL, Gemini (HTTPS) requires it if((Z3ED_AI OR YAZE_WITH_JSON) AND (YAZE_WITH_GRPC OR Z3ED_AI)) @@ -158,9 +160,9 @@ else() ) endif() -# ============================================================================ +# ============================================================================ # Optional gRPC Support for CLI Agent Test Command -# ============================================================================ +# ============================================================================ if(YAZE_WITH_GRPC) message(STATUS "Adding gRPC support to z3ed CLI") @@ -183,4 +185,4 @@ if(YAZE_WITH_GRPC) libprotobuf) message(STATUS "✓ gRPC CLI automation integrated") -endif() +endif() \ No newline at end of file diff --git a/src/util/platform_paths.cc b/src/util/platform_paths.cc index dbbeeb91..11493014 100644 --- a/src/util/platform_paths.cc +++ b/src/util/platform_paths.cc @@ -11,44 +11,63 @@ #else #include #include +#include // For PATH_MAX +#ifdef __APPLE__ +#include // For _NSGetExecutablePath +#endif #endif namespace yaze { namespace util { std::filesystem::path PlatformPaths::GetHomeDirectory() { + try { #ifdef _WIN32 - // Windows: Use USERPROFILE environment variable - const char* userprofile = std::getenv("USERPROFILE"); - if (userprofile && *userprofile) { - return std::filesystem::path(userprofile); - } - - // Fallback to HOMEDRIVE + HOMEPATH - const char* homedrive = std::getenv("HOMEDRIVE"); - const char* homepath = std::getenv("HOMEPATH"); - if (homedrive && homepath) { - return std::filesystem::path(std::string(homedrive) + std::string(homepath)); - } - - // Last resort: use temp directory - return std::filesystem::temp_directory_path(); + // Windows: Use USERPROFILE environment variable + const char* userprofile = std::getenv("USERPROFILE"); + if (userprofile && *userprofile) { + return std::filesystem::path(userprofile); + } + + // Fallback to HOMEDRIVE + HOMEPATH + const char* homedrive = std::getenv("HOMEDRIVE"); + const char* homepath = std::getenv("HOMEPATH"); + if (homedrive && homepath) { + return std::filesystem::path(std::string(homedrive) + std::string(homepath)); + } + + // Last resort: use temp directory + std::error_code ec; + auto temp = std::filesystem::temp_directory_path(ec); + if (!ec) { + return temp; + } + return std::filesystem::path("."); #else - // Unix/macOS: Use HOME environment variable - const char* home = std::getenv("HOME"); - if (home && *home) { - return std::filesystem::path(home); - } - - // Fallback: try getpwuid - struct passwd* pw = getpwuid(getuid()); - if (pw && pw->pw_dir) { - return std::filesystem::path(pw->pw_dir); - } - - // Last resort: current directory - return std::filesystem::current_path(); + // Unix/macOS: Use HOME environment variable + const char* home = std::getenv("HOME"); + if (home && *home) { + return std::filesystem::path(home); + } + + // Fallback: try getpwuid + struct passwd* pw = getpwuid(getuid()); + if (pw && pw->pw_dir) { + return std::filesystem::path(pw->pw_dir); + } + + // Last resort: current directory (with error handling) + std::error_code ec; + auto cwd = std::filesystem::current_path(ec); + if (!ec) { + return cwd; + } + return std::filesystem::path("."); #endif + } catch (...) { + // If everything fails, return current directory placeholder + return std::filesystem::path("."); + } } absl::StatusOr PlatformPaths::GetAppDataDirectory() { @@ -70,23 +89,11 @@ absl::StatusOr PlatformPaths::GetAppDataDirectory() { return status; } return app_data; -#elif defined(__APPLE__) +#else + // Unix/macOS: Use ~/.yaze for simplicity and consistency + // This is simpler than XDG or Application Support for a dev tool std::filesystem::path home = GetHomeDirectory(); - std::filesystem::path app_data = home / "Library" / "Application Support" / "yaze"; - auto status = EnsureDirectoryExists(app_data); - if (!status.ok()) { - return status; - } - return app_data; -#else // Linux and other Unix-like systems - const char* xdg_config_home = std::getenv("XDG_CONFIG_HOME"); - std::filesystem::path config_dir; - if (xdg_config_home && *xdg_config_home) { - config_dir = std::filesystem::path(xdg_config_home); - } else { - config_dir = GetHomeDirectory() / ".config"; - } - std::filesystem::path app_data = config_dir / "yaze"; + std::filesystem::path app_data = home / ".yaze"; auto status = EnsureDirectoryExists(app_data); if (!status.ok()) { return status; @@ -174,5 +181,163 @@ std::string PlatformPaths::ToNativePath(const std::filesystem::path& path) { return path.string(); } +absl::StatusOr PlatformPaths::FindAsset( + const std::string& relative_path) { + std::vector search_paths; + + try { + // 1. Check compile-time YAZE_ASSETS_PATH if defined +#ifdef YAZE_ASSETS_PATH + try { + search_paths.push_back(std::filesystem::path(YAZE_ASSETS_PATH) / relative_path); + } catch (...) { + // Skip if path construction fails + } +#endif + + // 2. Current working directory + assets/ (cached to avoid repeated calls) + static std::filesystem::path cached_cwd; + static bool cwd_cached = false; + + if (!cwd_cached) { + std::error_code ec; + cached_cwd = std::filesystem::current_path(ec); + cwd_cached = true; // Only try once to avoid repeated slow calls + } + + if (!cached_cwd.empty()) { + try { + search_paths.push_back(cached_cwd / "assets" / relative_path); + } catch (...) { + // Skip if path construction fails + } + } + + // 3. Executable directory + assets/ (cached to avoid repeated OS calls) + static std::filesystem::path cached_exe_dir; + static bool exe_dir_cached = false; + + if (!exe_dir_cached) { + try { +#ifdef __APPLE__ + char exe_path[PATH_MAX]; + uint32_t size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &size) == 0) { + cached_exe_dir = std::filesystem::path(exe_path).parent_path(); + } +#elif defined(__linux__) + char exe_path[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1); + if (len != -1) { + exe_path[len] = '\0'; + cached_exe_dir = std::filesystem::path(exe_path).parent_path(); + } +#elif defined(_WIN32) + wchar_t exe_path[MAX_PATH]; + if (GetModuleFileNameW(NULL, exe_path, MAX_PATH) != 0) { + cached_exe_dir = std::filesystem::path(exe_path).parent_path(); + } +#endif + } catch (...) { + // Skip if exe path detection fails + } + exe_dir_cached = true; // Only try once + } + + if (!cached_exe_dir.empty()) { + try { + search_paths.push_back(cached_exe_dir / "assets" / relative_path); + // Also check parent (for build/bin/yaze case) + search_paths.push_back(cached_exe_dir.parent_path() / "assets" / relative_path); + } catch (...) { + // Skip if path construction fails + } + } + + // 4. Parent directories (for running from build subdirectories) + if (!cached_cwd.empty()) { + try { + auto parent = cached_cwd.parent_path(); + if (!parent.empty() && parent != cached_cwd) { + search_paths.push_back(parent / "assets" / relative_path); + auto grandparent = parent.parent_path(); + if (!grandparent.empty() && grandparent != parent) { + search_paths.push_back(grandparent / "assets" / relative_path); + } + } + } catch (...) { + // Skip if path operations fail + } + } + + // 5. User directory ~/.yaze/assets/ (cached home directory) + static std::filesystem::path cached_home; + static bool home_cached = false; + + if (!home_cached) { + try { + cached_home = GetHomeDirectory(); + } catch (...) { + // Skip if home lookup fails + } + home_cached = true; // Only try once + } + + if (!cached_home.empty() && cached_home != ".") { + try { + search_paths.push_back(cached_home / ".yaze" / "assets" / relative_path); + } catch (...) { + // Skip if path construction fails + } + } + + // 6. System-wide installation (Unix only) +#ifndef _WIN32 + try { + search_paths.push_back(std::filesystem::path("/usr/local/share/yaze/assets") / relative_path); + search_paths.push_back(std::filesystem::path("/usr/share/yaze/assets") / relative_path); + } catch (...) { + // Skip if path construction fails + } +#endif + + // Search all paths and return the first one that exists + // Limit search to prevent infinite loops on weird filesystems + const size_t max_paths_to_check = 20; + size_t checked = 0; + + for (const auto& candidate : search_paths) { + if (++checked > max_paths_to_check) { + break; // Safety limit + } + + try { + // Use std::filesystem::exists with error code to avoid exceptions + std::error_code exists_ec; + if (std::filesystem::exists(candidate, exists_ec) && !exists_ec) { + // Double-check it's a regular file or directory, not a broken symlink + auto status = std::filesystem::status(candidate, exists_ec); + if (!exists_ec && status.type() != std::filesystem::file_type::not_found) { + return candidate; + } + } + } catch (...) { + // Skip this candidate if checking fails + continue; + } + } + + // If not found, return a simple error + return absl::NotFoundError( + absl::StrCat("Asset not found: ", relative_path)); + + } catch (const std::exception& e) { + return absl::InternalError( + absl::StrCat("Exception while searching for asset: ", e.what())); + } catch (...) { + return absl::InternalError("Unknown exception while searching for asset"); + } +} + } // namespace util } // namespace yaze diff --git a/src/util/platform_paths.h b/src/util/platform_paths.h index e7d342f7..adea0189 100644 --- a/src/util/platform_paths.h +++ b/src/util/platform_paths.h @@ -35,8 +35,7 @@ class PlatformPaths { * and cache files. The directory is created if it doesn't exist. * * - Windows: `%APPDATA%\yaze` (e.g., C:\Users\user\AppData\Roaming\yaze) - * - macOS: `~/Library/Application Support/yaze` - * - Linux: `$XDG_CONFIG_HOME/yaze` or `~/.config/yaze` + * - macOS/Unix: `~/.yaze` * * @return StatusOr with path to the application data directory. */ @@ -46,13 +45,10 @@ class PlatformPaths { * @brief Get the user-specific configuration directory for YAZE. * * This is the standard location for storing user configuration files. - * It often maps to the same location as GetAppDataDirectory but provides - * a more semantically correct API for config files. * The directory is created if it doesn't exist. * * - Windows: `%APPDATA%\yaze` - * - macOS: `~/Library/Application Support/yaze` - * - Linux: `$XDG_CONFIG_HOME/yaze` or `~/.config/yaze` + * - macOS/Unix: `~/.yaze` * * @return StatusOr with path to the configuration directory. */ @@ -117,6 +113,23 @@ class PlatformPaths { * @return Native path string */ static std::string ToNativePath(const std::filesystem::path& path); + + /** + * @brief Find an asset file in multiple standard locations + * + * Searches for an asset file in the following order: + * 1. YAZE_ASSETS_PATH (if defined at compile time) + relative_path + * 2. Current working directory + assets/ + relative_path + * 3. Executable directory + assets/ + relative_path + * 4. Parent directory + assets/ + relative_path + * 5. ~/.yaze/assets/ + relative_path (user-installed assets) + * 6. /usr/local/share/yaze/assets/ + relative_path (system-wide on Unix) + * + * @param relative_path Path relative to assets directory (e.g., "agent/prompt_catalogue.yaml") + * @return StatusOr with absolute path to found asset file + */ + static absl::StatusOr FindAsset( + const std::string& relative_path); }; } // namespace util