diff --git a/docs/z3ed/README.md b/docs/z3ed/README.md index e3e86a96..d90718b0 100644 --- a/docs/z3ed/README.md +++ b/docs/z3ed/README.md @@ -134,6 +134,7 @@ The `z3ed` CLI is the foundation for an AI-driven Model-Code-Program (MCP) loop, - `agent chat`: Opens an interactive terminal chat (TUI) with the AI agent. - `agent simple-chat`: A lightweight, non-TUI chat mode for scripting and automation. - `agent test ...`: Commands for running and managing automated GUI tests. +- `agent learn ...`: **NEW**: Manage learned knowledge (preferences, ROM patterns, project context, conversation memory). ### Resource Commands @@ -163,11 +164,82 @@ Accessible from **Debug → Agent Chat** inside YAZE. Provides the same conversa Z3ED supports multiple AI providers. Configuration is resolved with command-line flags taking precedence over environment variables. - `--ai_provider=`: Selects the AI provider (`mock`, `ollama`, `gemini`). -- `--ai_model=`: Specifies the model name (e.g., `qwen2.5-coder:7b`, `gemini-1.5-flash`). +- `--ai_model=`: Specifies the model name (e.g., `qwen2.5-coder:7b`, `gemini-2.5-flash`). - `--gemini_api_key=`: Your Gemini API key. - `--ollama_host=`: The URL for your Ollama server (default: `http://localhost:11434`). -## 8. CLI Output & Help System +### System Prompt Versions + +Z3ED includes multiple system prompt versions for different use cases: + +- **v1 (default)**: Original reactive prompt with basic tool calling +- **v2**: Enhanced with better JSON formatting and error handling +- **v3 (latest)**: Proactive prompt with intelligent tool chaining and implicit iteration - **RECOMMENDED** + +To use v3 prompt: Set environment variable `Z3ED_PROMPT_VERSION=v3` or it will be auto-selected for Gemini 2.0+ models. + +## 8. Learn Command - Knowledge Management + +The learn command enables the AI agent to remember preferences, patterns, and context across sessions. + +### Basic Usage + +```bash +# Store a preference +z3ed agent learn --preference "default_palette=2" + +# Get a preference +z3ed agent learn --get-preference default_palette + +# List all preferences +z3ed agent learn --list-preferences + +# View statistics +z3ed agent learn --stats + +# Export all learned data +z3ed agent learn --export my_learned_data.json + +# Import learned data +z3ed agent learn --import my_learned_data.json +``` + +### Project Context + +Store project-specific information that the agent can reference: + +```bash +# Save project context +z3ed agent learn --project "myrom" --context "Vanilla+ difficulty hack, focus on dungeon redesign" + +# List projects +z3ed agent learn --list-projects + +# Get project details +z3ed agent learn --get-project "myrom" +``` + +### Conversation Memory + +The agent automatically stores summaries of conversations for future reference: + +```bash +# View recent memories +z3ed agent learn --recent-memories 10 + +# Search memories by topic +z3ed agent learn --search-memories "room 5" +``` + +### Storage Location + +All learned data is stored in `~/.yaze/agent/`: +- `preferences.json`: User preferences +- `patterns.json`: Learned ROM patterns +- `projects.json`: Project contexts +- `memories.json`: Conversation summaries + +## 9. CLI Output & Help System The `z3ed` CLI features a modernized output system designed to be clean for users and informative for developers. @@ -205,7 +277,7 @@ The help system is organized by category for easy navigation. - **Main Help**: `z3ed --help` or `z3ed -h` shows a high-level overview of command categories. - **Category Help**: `z3ed help ` provides detailed information for a specific group of commands (e.g., `agent`, `patch`, `rom`). -## 9. Collaborative Sessions & Multimodal Vision +## 10. Collaborative Sessions & Multimodal Vision ### Overview @@ -784,7 +856,7 @@ The AI response appears in your chat history and can reference specific details - **Agent Editor Docs**: `src/app/editor/agent/README.md` - **Integration Guide**: `docs/z3ed/YAZE_SERVER_V2_INTEGRATION.md` -## 10. Roadmap & Implementation Status +## 11. Roadmap & Implementation Status **Last Updated**: October 4, 2025 @@ -815,7 +887,13 @@ The AI response appears in your chat history and can reference specific details 4. **Collaboration UI Enhancements (1 day)**: Add UI elements for ROM sync, snapshot sharing, and proposal management in the Agent Chat widget. 5. **Windows Cross-Platform Testing (8-10h)**: Validate `z3ed` and the test harness on Windows. -## 11. Troubleshooting +### ✅ Recently Completed (v0.2.0-alpha) + +- **Enhanced System Prompt (v3)**: Proactive tool chaining with implicit iteration to minimize back-and-forth +- **Learn Command**: Full implementation with preferences, ROM patterns, project context, and conversation memory +- **gRPC Windows Build Optimization**: Documented vcpkg approach and optimization strategies + +## 12. Troubleshooting - **"Build with -DZ3ED_AI=ON" warning**: AI features are disabled. Rebuild with the flag to enable them. - **"gRPC not available" error**: GUI testing is disabled. Rebuild with `-DYAZE_WITH_GRPC=ON`. diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 593ed90e..1bde73c2 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -110,7 +110,7 @@ absl::Status Agent::Run(const std::vector& arg_vec) { return agent::HandleGuiCommand(subcommand_args); } if (subcommand == "learn") { - return agent::HandleLearnCommand(); + return agent::HandleLearnCommand(subcommand_args); } if (subcommand == "list") { return agent::HandleListCommand(); diff --git a/src/cli/handlers/agent/commands.h b/src/cli/handlers/agent/commands.h index a69eebfc..7397ff63 100644 --- a/src/cli/handlers/agent/commands.h +++ b/src/cli/handlers/agent/commands.h @@ -20,7 +20,7 @@ absl::Status HandleDiffCommand(Rom& rom, absl::Status HandleAcceptCommand(const std::vector& args, Rom& rom); absl::Status HandleTestCommand(const std::vector& args); absl::Status HandleGuiCommand(const std::vector& args); -absl::Status HandleLearnCommand(); +absl::Status HandleLearnCommand(const std::vector& args = {}); absl::Status HandleListCommand(); absl::Status HandleCommitCommand(Rom& rom); absl::Status HandleRevertCommand(Rom& rom); diff --git a/src/cli/handlers/agent/general_commands.cc b/src/cli/handlers/agent/general_commands.cc index 8e480426..5b1eca99 100644 --- a/src/cli/handlers/agent/general_commands.cc +++ b/src/cli/handlers/agent/general_commands.cc @@ -385,9 +385,170 @@ absl::Status HandleDiffCommand(Rom& rom, const std::vector& args) { return absl::OkStatus(); } -absl::Status HandleLearnCommand() { - std::cout << "Agent learn not yet implemented." << std::endl; - return absl::OkStatus(); +absl::Status HandleLearnCommand(const std::vector& args) { + using namespace yaze::cli::agent; + + static LearnedKnowledgeService learn_service; + static bool initialized = false; + + if (!initialized) { + auto status = learn_service.Initialize(); + if (!status.ok()) { + std::cerr << "Failed to initialize learned knowledge service: " + << status.message() << std::endl; + return status; + } + initialized = true; + } + + if (args.empty()) { + // Show usage + std::cout << "\nUsage: z3ed agent learn [options]\n\n"; + std::cout << "Options:\n"; + std::cout << " --preference = Set a preference\n"; + std::cout << " --get-preference Get a preference value\n"; + std::cout << " --list-preferences List all preferences\n"; + std::cout << " --pattern --data Learn a ROM pattern\n"; + std::cout << " --query-patterns Query learned patterns\n"; + std::cout << " --project --context Save project context\n"; + std::cout << " --get-project Get project context\n"; + std::cout << " --list-projects List all projects\n"; + std::cout << " --memory --summary Store conversation memory\n"; + std::cout << " --search-memories Search memories\n"; + std::cout << " --recent-memories [limit] Show recent memories\n"; + std::cout << " --export Export all data to JSON\n"; + std::cout << " --import Import data from JSON\n"; + std::cout << " --stats Show statistics\n"; + std::cout << " --clear Clear all learned data\n"; + return absl::OkStatus(); + } + + // Parse arguments + std::string command = args[0]; + + if (command == "--preference" && args.size() >= 2) { + std::string pref = args[1]; + size_t eq_pos = pref.find('='); + if (eq_pos == std::string::npos) { + return absl::InvalidArgumentError("Preference must be in format key=value"); + } + std::string key = pref.substr(0, eq_pos); + std::string value = pref.substr(eq_pos + 1); + auto status = learn_service.SetPreference(key, value); + if (status.ok()) { + std::cout << "✓ Preference '" << key << "' set to '" << value << "'\n"; + } + return status; + } + + if (command == "--get-preference" && args.size() >= 2) { + auto value = learn_service.GetPreference(args[1]); + if (value) { + std::cout << args[1] << " = " << *value << "\n"; + } else { + std::cout << "Preference '" << args[1] << "' not found\n"; + } + return absl::OkStatus(); + } + + if (command == "--list-preferences") { + auto prefs = learn_service.GetAllPreferences(); + if (prefs.empty()) { + std::cout << "No preferences stored.\n"; + } else { + std::cout << "\n=== Stored Preferences ===\n"; + for (const auto& [key, value] : prefs) { + std::cout << " " << key << " = " << value << "\n"; + } + } + return absl::OkStatus(); + } + + if (command == "--stats") { + auto stats = learn_service.GetStats(); + std::cout << "\n=== Learned Knowledge Statistics ===\n"; + std::cout << " Preferences: " << stats.preference_count << "\n"; + std::cout << " ROM Patterns: " << stats.pattern_count << "\n"; + std::cout << " Projects: " << stats.project_count << "\n"; + std::cout << " Memories: " << stats.memory_count << "\n"; + std::cout << " First learned: " << absl::FormatTime(absl::FromUnixMillis(stats.first_learned_at)) << "\n"; + std::cout << " Last updated: " << absl::FormatTime(absl::FromUnixMillis(stats.last_updated_at)) << "\n"; + return absl::OkStatus(); + } + + if (command == "--export" && args.size() >= 2) { + auto json = learn_service.ExportToJSON(); + if (!json.ok()) { + return json.status(); + } + std::ofstream file(args[1]); + if (!file.is_open()) { + return absl::InternalError("Failed to open file for writing"); + } + file << *json; + std::cout << "✓ Exported learned data to " << args[1] << "\n"; + return absl::OkStatus(); + } + + if (command == "--import" && args.size() >= 2) { + std::ifstream file(args[1]); + if (!file.is_open()) { + return absl::NotFoundError("File not found: " + args[1]); + } + std::stringstream buffer; + buffer << file.rdbuf(); + auto status = learn_service.ImportFromJSON(buffer.str()); + if (status.ok()) { + std::cout << "✓ Imported learned data from " << args[1] << "\n"; + } + return status; + } + + if (command == "--clear") { + auto status = learn_service.ClearAll(); + if (status.ok()) { + std::cout << "✓ All learned data cleared\n"; + } + return status; + } + + if (command == "--list-projects") { + auto projects = learn_service.GetAllProjects(); + if (projects.empty()) { + std::cout << "No projects stored.\n"; + } else { + std::cout << "\n=== Stored Projects ===\n"; + for (const auto& proj : projects) { + std::cout << " " << proj.project_name << "\n"; + std::cout << " ROM Hash: " << proj.rom_hash.substr(0, 16) << "...\n"; + std::cout << " Last Accessed: " << absl::FormatTime(absl::FromUnixMillis(proj.last_accessed)) << "\n"; + } + } + return absl::OkStatus(); + } + + if (command == "--recent-memories") { + int limit = 10; + if (args.size() >= 2) { + limit = std::stoi(args[1]); + } + auto memories = learn_service.GetRecentMemories(limit); + if (memories.empty()) { + std::cout << "No memories stored.\n"; + } else { + std::cout << "\n=== Recent Memories ===\n"; + for (const auto& mem : memories) { + std::cout << " Topic: " << mem.topic << "\n"; + std::cout << " Summary: " << mem.summary << "\n"; + std::cout << " Facts: " << mem.key_facts.size() << " key facts\n"; + std::cout << " Created: " << absl::FormatTime(absl::FromUnixMillis(mem.created_at)) << "\n"; + std::cout << "\n"; + } + } + return absl::OkStatus(); + } + + return absl::InvalidArgumentError("Unknown learn command. Use 'z3ed agent learn' for usage."); } absl::Status HandleListCommand() { diff --git a/src/cli/service/agent/learned_knowledge_service.cc b/src/cli/service/agent/learned_knowledge_service.cc new file mode 100644 index 00000000..247e5039 --- /dev/null +++ b/src/cli/service/agent/learned_knowledge_service.cc @@ -0,0 +1,673 @@ +#include "cli/service/agent/learned_knowledge_service.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" + +#ifdef _WIN32 +#include +#define mkdir(path, mode) _mkdir(path) +#else +#include +#include +#endif + +namespace yaze { +namespace cli { +namespace agent { + +namespace { + +int64_t CurrentTimestamp() { + return absl::ToUnixMillis(absl::Now()); +} + +std::string GenerateRandomID() { + static int counter = 0; + auto now = std::chrono::system_clock::now().time_since_epoch().count(); + return absl::StrFormat("%lld_%d", now, counter++); +} + +bool FileExists(const std::filesystem::path& path) { + return std::filesystem::exists(path); +} + +absl::Status EnsureDirectoryExists(const std::filesystem::path& path) { + if (!std::filesystem::exists(path)) { + std::error_code ec; + if (!std::filesystem::create_directories(path, ec)) { + return absl::InternalError( + absl::StrCat("Failed to create directory: ", path.string(), + " - ", ec.message())); + } + } + return absl::OkStatus(); +} + +} // namespace + +LearnedKnowledgeService::LearnedKnowledgeService() + : LearnedKnowledgeService(std::filesystem::path(getenv("HOME") ? getenv("HOME") : ".") / ".yaze" / "agent") {} + +LearnedKnowledgeService::LearnedKnowledgeService( + const std::filesystem::path& data_dir) + : data_dir_(data_dir), + prefs_file_(data_dir / "preferences.json"), + patterns_file_(data_dir / "patterns.json"), + projects_file_(data_dir / "projects.json"), + memories_file_(data_dir / "memories.json") {} + +absl::Status LearnedKnowledgeService::Initialize() { + if (initialized_) { + return absl::OkStatus(); + } + + // Ensure data directory exists + auto status = EnsureDirectoryExists(data_dir_); + if (!status.ok()) { + return status; + } + + // Load existing data + LoadPreferences(); // Ignore errors for empty files + LoadPatterns(); + LoadProjects(); + LoadMemories(); + + initialized_ = true; + return absl::OkStatus(); +} + +absl::Status LearnedKnowledgeService::SaveAll() { + auto status = SavePreferences(); + if (!status.ok()) return status; + + status = SavePatterns(); + if (!status.ok()) return status; + + status = SaveProjects(); + if (!status.ok()) return status; + + status = SaveMemories(); + if (!status.ok()) return status; + + return absl::OkStatus(); +} + +// === Preference Management === + +absl::Status LearnedKnowledgeService::SetPreference(const std::string& key, + const std::string& value) { + if (!initialized_) { + return absl::FailedPreconditionError("Service not initialized"); + } + + preferences_[key] = value; + return SavePreferences(); +} + +std::optional LearnedKnowledgeService::GetPreference( + const std::string& key) const { + auto it = preferences_.find(key); + if (it != preferences_.end()) { + return it->second; + } + return std::nullopt; +} + +std::map LearnedKnowledgeService::GetAllPreferences() const { + return preferences_; +} + +absl::Status LearnedKnowledgeService::RemovePreference(const std::string& key) { + if (!initialized_) { + return absl::FailedPreconditionError("Service not initialized"); + } + + preferences_.erase(key); + return SavePreferences(); +} + +// === ROM Pattern Learning === + +absl::Status LearnedKnowledgeService::LearnPattern(const std::string& type, + const std::string& rom_hash, + const std::string& data, + float confidence) { + if (!initialized_) { + return absl::FailedPreconditionError("Service not initialized"); + } + + ROMPattern pattern; + pattern.pattern_type = type; + pattern.rom_hash = rom_hash; + pattern.pattern_data = data; + pattern.confidence = confidence; + pattern.learned_at = CurrentTimestamp(); + pattern.access_count = 1; + + patterns_.push_back(pattern); + return SavePatterns(); +} + +std::vector +LearnedKnowledgeService::QueryPatterns(const std::string& type, + const std::string& rom_hash) const { + std::vector results; + + for (const auto& pattern : patterns_) { + bool type_match = type.empty() || pattern.pattern_type == type; + bool hash_match = rom_hash.empty() || pattern.rom_hash == rom_hash; + + if (type_match && hash_match) { + results.push_back(pattern); + } + } + + return results; +} + +absl::Status LearnedKnowledgeService::UpdatePatternConfidence( + const std::string& type, + const std::string& rom_hash, + float new_confidence) { + bool found = false; + + for (auto& pattern : patterns_) { + if (pattern.pattern_type == type && pattern.rom_hash == rom_hash) { + pattern.confidence = new_confidence; + pattern.access_count++; + found = true; + } + } + + if (!found) { + return absl::NotFoundError("Pattern not found"); + } + + return SavePatterns(); +} + +// === Project Context === + +absl::Status LearnedKnowledgeService::SaveProjectContext( + const std::string& project_name, + const std::string& rom_hash, + const std::string& context) { + if (!initialized_) { + return absl::FailedPreconditionError("Service not initialized"); + } + + // Update existing or create new + bool found = false; + for (auto& project : projects_) { + if (project.project_name == project_name) { + project.rom_hash = rom_hash; + project.context_data = context; + project.last_accessed = CurrentTimestamp(); + found = true; + break; + } + } + + if (!found) { + ProjectContext project; + project.project_name = project_name; + project.rom_hash = rom_hash; + project.context_data = context; + project.last_accessed = CurrentTimestamp(); + projects_.push_back(project); + } + + return SaveProjects(); +} + +std::optional +LearnedKnowledgeService::GetProjectContext(const std::string& project_name) const { + for (const auto& project : projects_) { + if (project.project_name == project_name) { + return project; + } + } + return std::nullopt; +} + +std::vector +LearnedKnowledgeService::GetAllProjects() const { + return projects_; +} + +// === Conversation Memory === + +absl::Status LearnedKnowledgeService::StoreConversationSummary( + const std::string& topic, + const std::string& summary, + const std::vector& key_facts) { + if (!initialized_) { + return absl::FailedPreconditionError("Service not initialized"); + } + + ConversationMemory memory; + memory.id = GenerateRandomID(); + memory.topic = topic; + memory.summary = summary; + memory.key_facts = key_facts; + memory.created_at = CurrentTimestamp(); + memory.access_count = 1; + + memories_.push_back(memory); + + // Keep only last 100 memories + if (memories_.size() > 100) { + memories_.erase(memories_.begin()); + } + + return SaveMemories(); +} + +std::vector +LearnedKnowledgeService::SearchMemories(const std::string& query) const { + std::vector results; + + std::string query_lower = query; + std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower); + + for (const auto& memory : memories_) { + std::string topic_lower = memory.topic; + std::string summary_lower = memory.summary; + std::transform(topic_lower.begin(), topic_lower.end(), topic_lower.begin(), ::tolower); + std::transform(summary_lower.begin(), summary_lower.end(), summary_lower.begin(), ::tolower); + + if (topic_lower.find(query_lower) != std::string::npos || + summary_lower.find(query_lower) != std::string::npos) { + results.push_back(memory); + } + } + + return results; +} + +std::vector +LearnedKnowledgeService::GetRecentMemories(int limit) const { + std::vector recent = memories_; + + // Sort by created_at descending + std::sort(recent.begin(), recent.end(), + [](const ConversationMemory& a, const ConversationMemory& b) { + return a.created_at > b.created_at; + }); + + if (recent.size() > static_cast(limit)) { + recent.resize(limit); + } + + return recent; +} + +// === Import/Export === + +#ifdef YAZE_WITH_JSON +absl::StatusOr LearnedKnowledgeService::ExportToJSON() const { + nlohmann::json export_data; + + // Export preferences + export_data["preferences"] = preferences_; + + // Export patterns + export_data["patterns"] = nlohmann::json::array(); + for (const auto& pattern : patterns_) { + nlohmann::json p; + p["type"] = pattern.pattern_type; + p["rom_hash"] = pattern.rom_hash; + p["data"] = pattern.pattern_data; + p["confidence"] = pattern.confidence; + p["learned_at"] = pattern.learned_at; + p["access_count"] = pattern.access_count; + export_data["patterns"].push_back(p); + } + + // Export projects + export_data["projects"] = nlohmann::json::array(); + for (const auto& project : projects_) { + nlohmann::json p; + p["name"] = project.project_name; + p["rom_hash"] = project.rom_hash; + p["context"] = project.context_data; + p["last_accessed"] = project.last_accessed; + export_data["projects"].push_back(p); + } + + // Export memories + export_data["memories"] = nlohmann::json::array(); + for (const auto& memory : memories_) { + nlohmann::json m; + m["id"] = memory.id; + m["topic"] = memory.topic; + m["summary"] = memory.summary; + m["key_facts"] = memory.key_facts; + m["created_at"] = memory.created_at; + m["access_count"] = memory.access_count; + export_data["memories"].push_back(m); + } + + return export_data.dump(2); +} + +absl::Status LearnedKnowledgeService::ImportFromJSON(const std::string& json_data) { + try { + auto data = nlohmann::json::parse(json_data); + + // Import preferences + if (data.contains("preferences")) { + for (const auto& [key, value] : data["preferences"].items()) { + preferences_[key] = value.get(); + } + } + + // Import patterns + if (data.contains("patterns")) { + for (const auto& p : data["patterns"]) { + ROMPattern pattern; + pattern.pattern_type = p["type"]; + pattern.rom_hash = p["rom_hash"]; + pattern.pattern_data = p["data"]; + pattern.confidence = p["confidence"]; + pattern.learned_at = p["learned_at"]; + pattern.access_count = p["access_count"]; + patterns_.push_back(pattern); + } + } + + // Import projects + if (data.contains("projects")) { + for (const auto& p : data["projects"]) { + ProjectContext project; + project.project_name = p["name"]; + project.rom_hash = p["rom_hash"]; + project.context_data = p["context"]; + project.last_accessed = p["last_accessed"]; + projects_.push_back(project); + } + } + + // Import memories + if (data.contains("memories")) { + for (const auto& m : data["memories"]) { + ConversationMemory memory; + memory.id = m["id"]; + memory.topic = m["topic"]; + memory.summary = m["summary"]; + memory.key_facts = m["key_facts"].get>(); + memory.created_at = m["created_at"]; + memory.access_count = m["access_count"]; + memories_.push_back(memory); + } + } + + return SaveAll(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("JSON parse error: ", e.what())); + } +} +#else +absl::StatusOr LearnedKnowledgeService::ExportToJSON() const { + return absl::UnimplementedError("JSON support not enabled. Build with -DYAZE_WITH_JSON=ON"); +} + +absl::Status LearnedKnowledgeService::ImportFromJSON(const std::string&) { + return absl::UnimplementedError("JSON support not enabled. Build with -DYAZE_WITH_JSON=ON"); +} +#endif + +absl::Status LearnedKnowledgeService::ClearAll() { + preferences_.clear(); + patterns_.clear(); + projects_.clear(); + memories_.clear(); + return SaveAll(); +} + +// === Statistics === + +LearnedKnowledgeService::Stats LearnedKnowledgeService::GetStats() const { + Stats stats; + stats.preference_count = preferences_.size(); + stats.pattern_count = patterns_.size(); + stats.project_count = projects_.size(); + stats.memory_count = memories_.size(); + + // Find earliest learned_at + int64_t earliest = CurrentTimestamp(); + for (const auto& pattern : patterns_) { + if (pattern.learned_at < earliest) { + earliest = pattern.learned_at; + } + } + for (const auto& memory : memories_) { + if (memory.created_at < earliest) { + earliest = memory.created_at; + } + } + stats.first_learned_at = earliest; + + // Last updated is now + stats.last_updated_at = CurrentTimestamp(); + + return stats; +} + +// === Internal Helpers === + +#ifdef YAZE_WITH_JSON +absl::Status LearnedKnowledgeService::LoadPreferences() { + if (!FileExists(prefs_file_)) { + return absl::OkStatus(); // No file yet, empty preferences + } + + try { + std::ifstream file(prefs_file_); + if (!file.is_open()) { + return absl::InternalError("Failed to open preferences file"); + } + + nlohmann::json data; + file >> data; + + for (const auto& [key, value] : data.items()) { + preferences_[key] = value.get(); + } + + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to load preferences: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::SavePreferences() { + try { + nlohmann::json data = preferences_; + + std::ofstream file(prefs_file_); + if (!file.is_open()) { + return absl::InternalError("Failed to open preferences file for writing"); + } + + file << data.dump(2); + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to save preferences: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::LoadPatterns() { + if (!FileExists(patterns_file_)) { + return absl::OkStatus(); + } + + try { + std::ifstream file(patterns_file_); + nlohmann::json data; + file >> data; + + for (const auto& p : data) { + ROMPattern pattern; + pattern.pattern_type = p["type"]; + pattern.rom_hash = p["rom_hash"]; + pattern.pattern_data = p["data"]; + pattern.confidence = p["confidence"]; + pattern.learned_at = p["learned_at"]; + pattern.access_count = p.value("access_count", 0); + patterns_.push_back(pattern); + } + + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to load patterns: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::SavePatterns() { + try { + nlohmann::json data = nlohmann::json::array(); + + for (const auto& pattern : patterns_) { + nlohmann::json p; + p["type"] = pattern.pattern_type; + p["rom_hash"] = pattern.rom_hash; + p["data"] = pattern.pattern_data; + p["confidence"] = pattern.confidence; + p["learned_at"] = pattern.learned_at; + p["access_count"] = pattern.access_count; + data.push_back(p); + } + + std::ofstream file(patterns_file_); + file << data.dump(2); + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to save patterns: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::LoadProjects() { + if (!FileExists(projects_file_)) { + return absl::OkStatus(); + } + + try { + std::ifstream file(projects_file_); + nlohmann::json data; + file >> data; + + for (const auto& p : data) { + ProjectContext project; + project.project_name = p["name"]; + project.rom_hash = p["rom_hash"]; + project.context_data = p["context"]; + project.last_accessed = p["last_accessed"]; + projects_.push_back(project); + } + + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to load projects: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::SaveProjects() { + try { + nlohmann::json data = nlohmann::json::array(); + + for (const auto& project : projects_) { + nlohmann::json p; + p["name"] = project.project_name; + p["rom_hash"] = project.rom_hash; + p["context"] = project.context_data; + p["last_accessed"] = project.last_accessed; + data.push_back(p); + } + + std::ofstream file(projects_file_); + file << data.dump(2); + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to save projects: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::SaveMemories() { + try { + nlohmann::json data = nlohmann::json::array(); + + for (const auto& memory : memories_) { + nlohmann::json m; + m["id"] = memory.id; + m["topic"] = memory.topic; + m["summary"] = memory.summary; + m["key_facts"] = memory.key_facts; + m["created_at"] = memory.created_at; + m["access_count"] = memory.access_count; + data.push_back(m); + } + + std::ofstream file(memories_file_); + file << data.dump(2); + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to save memories: ", e.what())); + } +} + +absl::Status LearnedKnowledgeService::LoadMemories() { + if (!FileExists(memories_file_)) { + return absl::OkStatus(); + } + + try { + std::ifstream file(memories_file_); + nlohmann::json data; + file >> data; + + for (const auto& m : data) { + ConversationMemory memory; + memory.id = m["id"]; + memory.topic = m["topic"]; + memory.summary = m["summary"]; + memory.key_facts = m["key_facts"].get>(); + memory.created_at = m["created_at"]; + memory.access_count = m.value("access_count", 0); + memories_.push_back(memory); + } + + return absl::OkStatus(); + } catch (const std::exception& e) { + return absl::InternalError(absl::StrCat("Failed to load memories: ", e.what())); + } +} + +#else +// Stub implementations when JSON is not available +absl::Status LearnedKnowledgeService::LoadPreferences() { return absl::OkStatus(); } +absl::Status LearnedKnowledgeService::SavePreferences() { return absl::UnimplementedError("JSON support required"); } +absl::Status LearnedKnowledgeService::LoadPatterns() { return absl::OkStatus(); } +absl::Status LearnedKnowledgeService::SavePatterns() { return absl::UnimplementedError("JSON support required"); } +absl::Status LearnedKnowledgeService::LoadProjects() { return absl::OkStatus(); } +absl::Status LearnedKnowledgeService::SaveProjects() { return absl::UnimplementedError("JSON support required"); } +absl::Status LearnedKnowledgeService::LoadMemories() { return absl::OkStatus(); } +absl::Status LearnedKnowledgeService::SaveMemories() { return absl::UnimplementedError("JSON support required"); } +#endif + +std::string LearnedKnowledgeService::GenerateID() const { + return GenerateRandomID(); +} + +} // namespace agent +} // namespace cli +} // namespace yaze diff --git a/src/cli/service/agent/learned_knowledge_service.h b/src/cli/service/agent/learned_knowledge_service.h new file mode 100644 index 00000000..a03bd67a --- /dev/null +++ b/src/cli/service/agent/learned_knowledge_service.h @@ -0,0 +1,225 @@ +#ifndef YAZE_CLI_SERVICE_AGENT_LEARNED_KNOWLEDGE_SERVICE_H_ +#define YAZE_CLI_SERVICE_AGENT_LEARNED_KNOWLEDGE_SERVICE_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" + +#ifdef YAZE_WITH_JSON +#include "nlohmann/json.hpp" +#endif + +namespace yaze { +namespace cli { +namespace agent { + +/** + * @class LearnedKnowledgeService + * @brief Manages persistent learned information across agent sessions + * + * Stores: + * - User preferences (default palettes, favorite tools, etc.) + * - ROM patterns (frequently accessed rooms, sprite placements, etc.) + * - Project context (ROM-specific notes, goals, custom names) + * - Conversation memory (summaries of past discussions) + */ +class LearnedKnowledgeService { + public: + LearnedKnowledgeService(); + explicit LearnedKnowledgeService(const std::filesystem::path& data_dir); + + // Initialize the service and load existing data + absl::Status Initialize(); + + // Save all data to disk + absl::Status SaveAll(); + + // === Preference Management === + + /** + * Set a user preference + * @param key Preference key (e.g., "default_palette", "preferred_tool") + * @param value Preference value + */ + absl::Status SetPreference(const std::string& key, const std::string& value); + + /** + * Get a user preference + * @param key Preference key + * @return Value if exists, nullopt otherwise + */ + std::optional GetPreference(const std::string& key) const; + + /** + * List all preferences + */ + std::map GetAllPreferences() const; + + /** + * Remove a preference + */ + absl::Status RemovePreference(const std::string& key); + + // === ROM Pattern Learning === + + struct ROMPattern { + std::string pattern_type; // e.g., "sprite_location", "tile_usage" + std::string rom_hash; + std::string pattern_data; // JSON encoded + float confidence = 1.0f; // 0.0 to 1.0 + int64_t learned_at = 0; + int access_count = 0; + }; + + /** + * Learn a pattern from the current ROM + * @param type Pattern type (e.g., "sprite_distribution", "room_access_frequency") + * @param rom_hash SHA256 hash of the ROM + * @param data Pattern-specific data (JSON) + */ + absl::Status LearnPattern(const std::string& type, + const std::string& rom_hash, + const std::string& data, + float confidence = 1.0f); + + /** + * Query patterns for a specific ROM + * @param type Pattern type to query + * @param rom_hash ROM hash to filter by (empty = all ROMs) + * @return Vector of matching patterns + */ + std::vector QueryPatterns(const std::string& type, + const std::string& rom_hash = "") const; + + /** + * Update pattern confidence/access count + */ + absl::Status UpdatePatternConfidence(const std::string& type, + const std::string& rom_hash, + float new_confidence); + + // === Project Context === + + struct ProjectContext { + std::string project_name; + std::string rom_hash; + std::string context_data; // JSON encoded: description, goals, custom labels + int64_t last_accessed = 0; + }; + + /** + * Save context for a project/ROM + */ + absl::Status SaveProjectContext(const std::string& project_name, + const std::string& rom_hash, + const std::string& context); + + /** + * Get project context + */ + std::optional GetProjectContext(const std::string& project_name) const; + + /** + * List all projects + */ + std::vector GetAllProjects() const; + + // === Conversation Memory === + + struct ConversationMemory { + std::string id; + std::string topic; + std::string summary; + std::vector key_facts; + int64_t created_at = 0; + int access_count = 0; + }; + + /** + * Store a conversation summary + * @param topic Topic/theme of the conversation + * @param summary Brief summary + * @param key_facts Important facts extracted + */ + absl::Status StoreConversationSummary(const std::string& topic, + const std::string& summary, + const std::vector& key_facts); + + /** + * Search conversation memories by topic/keyword + */ + std::vector SearchMemories(const std::string& query) const; + + /** + * Get most recent conversation memories + */ + std::vector GetRecentMemories(int limit = 10) const; + + // === Import/Export === + + /** + * Export all learned data to JSON + */ + absl::StatusOr ExportToJSON() const; + + /** + * Import learned data from JSON + */ + absl::Status ImportFromJSON(const std::string& json_data); + + /** + * Clear all learned data + */ + absl::Status ClearAll(); + + // === Statistics === + + struct Stats { + int preference_count = 0; + int pattern_count = 0; + int project_count = 0; + int memory_count = 0; + int64_t first_learned_at = 0; + int64_t last_updated_at = 0; + }; + + Stats GetStats() const; + + private: + std::filesystem::path data_dir_; + std::filesystem::path prefs_file_; + std::filesystem::path patterns_file_; + std::filesystem::path projects_file_; + std::filesystem::path memories_file_; + + std::map preferences_; + std::vector patterns_; + std::vector projects_; + std::vector memories_; + + bool initialized_ = false; + + // Internal helpers + absl::Status LoadPreferences(); + absl::Status LoadPatterns(); + absl::Status LoadProjects(); + absl::Status LoadMemories(); + + absl::Status SavePreferences(); + absl::Status SavePatterns(); + absl::Status SaveProjects(); + absl:Status SaveMemories(); + + std::string GenerateID() const; +}; + +} // namespace agent +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_SERVICE_AGENT_LEARNED_KNOWLEDGE_SERVICE_H_