feat(asset): implement asset path resolution and streamline file loading

- Introduced a new utility function `FindAsset` in `PlatformPaths` to locate asset files across multiple standard directories, enhancing flexibility in asset management.
- Updated various components to utilize `FindAsset` for loading configuration and prompt files, replacing hardcoded search paths with a more robust solution.
- Improved error handling and logging for asset loading failures, ensuring clearer feedback during runtime.
- Refactored existing code in `gemini_ai_service`, `prompt_builder`, and `platform_paths` to leverage the new asset resolution mechanism, promoting code consistency and maintainability.
This commit is contained in:
scawful
2025-10-10 19:12:16 -04:00
parent bfbba1de13
commit f91aff0b00
5 changed files with 293 additions and 182 deletions

View File

@@ -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 <filesystem>
#include <fstream>
#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<std::string> 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<std::string> 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 "[]";
}

View File

@@ -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<fs::path> BuildCatalogueSearchPaths(const std::string& explicit_path) {
std::vector<fs::path> 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<std::string> 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<std::string> 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<std::string> 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<char>(file)),
std::istreambuf_iterator<char>());
@@ -743,15 +699,10 @@ std::string PromptBuilder::BuildContextSection(const RomContext& context) {
}
std::string PromptBuilder::BuildSystemInstruction() {
// Try to load from file first
const std::vector<std::string> 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<char>(file)),
std::istreambuf_iterator<char>());
@@ -876,3 +827,4 @@ std::vector<FewShotExample> PromptBuilder::GetExamplesForCategory(
} // namespace cli
} // namespace yaze

View File

@@ -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()

View File

@@ -11,44 +11,63 @@
#else
#include <unistd.h>
#include <pwd.h>
#include <climits> // For PATH_MAX
#ifdef __APPLE__
#include <mach-o/dyld.h> // 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<std::filesystem::path> PlatformPaths::GetAppDataDirectory() {
@@ -70,23 +89,11 @@ absl::StatusOr<std::filesystem::path> 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<std::filesystem::path> PlatformPaths::FindAsset(
const std::string& relative_path) {
std::vector<std::filesystem::path> 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

View File

@@ -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<std::filesystem::path> FindAsset(
const std::string& relative_path);
};
} // namespace util