using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// 에이전트 메모리 관리 도구. /// 프로젝트 규칙, 사용자 선호도, 학습 내용을 저장/검색/삭제합니다. /// public class MemoryTool : IAgentTool { public string Name => "memory"; public string Description => "프로젝트 규칙, 사용자 선호도, 학습 내용을 저장하고 검색합니다.\n" + "대화 간 지속되는 메모리로, 새 대화에서도 이전에 학습한 내용을 활용할 수 있습니다.\n" + "- action=\"save\": 학습 메모리 저장 (type, content 필수)\n" + "- action=\"save_scope\": 계층형 메모리 파일에 저장 (scope, content 필수)\n" + "- action=\"show_scope\": 계층형 메모리 파일 본문 조회 (scope 필수)\n" + "- action=\"search\": 관련 메모리 검색 (query 필수)\n" + "- action=\"list\": 현재 메모리 전체 목록 + 계층형 메모리 파일 목록\n" + "- action=\"delete\": 학습 메모리 삭제 (id 필수)\n" + "- action=\"delete_scope\": 계층형 메모리 파일에서 삭제 (scope, query 필수)\n" + "scope 종류: managed | user | project | local\n" + "type 종류: rule(프로젝트 규칙), preference(사용자 선호), fact(사실), correction(실수 교정)"; public ToolParameterSchema Parameters => new() { Properties = new() { ["action"] = new() { Type = "string", Description = "save | save_scope | show_scope | search | list | delete | delete_scope" }, ["type"] = new() { Type = "string", Description = "메모리 유형: rule | preference | fact | correction. save 시 필수." }, ["scope"] = new() { Type = "string", Description = "계층형 메모리 대상: managed | user | project | local. save_scope/delete_scope 시 필수." }, ["content"] = new() { Type = "string", Description = "저장할 내용. save 시 필수." }, ["query"] = new() { Type = "string", Description = "검색 쿼리. search 시 필수." }, ["id"] = new() { Type = "string", Description = "메모리 ID. delete 시 필수." }, }, Required = ["action"] }; public Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { // 설정 체크 var app = System.Windows.Application.Current as App; if (!(app?.SettingsService?.Settings.Llm.EnableAgentMemory ?? true)) return Task.FromResult(ToolResult.Ok("에이전트 메모리가 비활성 상태입니다. 설정에서 활성화하세요.")); var memoryService = app?.MemoryService; if (memoryService == null) return Task.FromResult(ToolResult.Fail("메모리 서비스를 사용할 수 없습니다.")); memoryService.Load(context.WorkFolder); if (!args.TryGetProperty("action", out var actionEl)) return Task.FromResult(ToolResult.Fail("action이 필요합니다.")); var action = actionEl.GetString() ?? ""; return Task.FromResult(action switch { "save" => ExecuteSave(args, memoryService, context), "save_scope" => ExecuteSaveScope(args, memoryService, context), "show_scope" => ExecuteShowScope(args, memoryService, context), "search" => ExecuteSearch(args, memoryService), "list" => ExecuteList(memoryService), "delete" => ExecuteDelete(args, memoryService), "delete_scope" => ExecuteDeleteScope(args, memoryService, context), _ => ToolResult.Fail($"알 수 없는 액션: {action}. save | save_scope | show_scope | search | list | delete | delete_scope 중 선택하세요."), }); } private static ToolResult ExecuteSave(JsonElement args, AgentMemoryService svc, AgentContext context) { var type = args.TryGetProperty("type", out var t) ? t.GetString() ?? "fact" : "fact"; var content = args.TryGetProperty("content", out var c) ? c.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(content)) return ToolResult.Fail("content가 필요합니다."); var validTypes = new[] { "rule", "preference", "fact", "correction" }; if (!validTypes.Contains(type)) return ToolResult.Fail($"잘못된 type: {type}. rule | preference | fact | correction 중 선택하세요."); var workFolder = string.IsNullOrEmpty(context.WorkFolder) ? null : context.WorkFolder; var entry = svc.Add(type, content, $"agent:{context.ActiveTab}", workFolder); return ToolResult.Ok($"메모리 저장됨 [{entry.Type}] (ID: {entry.Id}): {entry.Content}"); } private static ToolResult ExecuteSearch(JsonElement args, AgentMemoryService svc) { var query = args.TryGetProperty("query", out var q) ? q.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(query)) return ToolResult.Fail("query가 필요합니다."); var results = svc.GetRelevant(query, 10); var docs = svc.InstructionDocuments .Where(d => d.Content.Contains(query, StringComparison.OrdinalIgnoreCase) || d.Path.Contains(query, StringComparison.OrdinalIgnoreCase)) .Take(8) .ToList(); if (results.Count == 0 && docs.Count == 0) return ToolResult.Ok("관련 메모리가 없습니다."); var sb = new StringBuilder(); if (docs.Count > 0) { sb.AppendLine($"계층형 메모리 {docs.Count}개:"); foreach (var doc in docs) { var priority = doc.Priority > 0 ? $" (우선순위 {doc.Priority})" : ""; var suffix = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" — {doc.Description}"; var scopeHint = doc.Paths.Count > 0 ? $" (paths: {string.Join(", ", doc.Paths)})" : ""; sb.AppendLine($" [{doc.Label}] {doc.Path}{priority}{suffix}{scopeHint}"); } sb.AppendLine(); } if (results.Count > 0) { sb.AppendLine($"학습 메모리 {results.Count}개:"); foreach (var e in results) sb.AppendLine($" [{e.Type}] {e.Content} (사용 {e.UseCount}회, ID: {e.Id})"); } return ToolResult.Ok(sb.ToString()); } private static ToolResult ExecuteList(AgentMemoryService svc) { var all = svc.All; var docs = svc.InstructionDocuments; if (all.Count == 0 && docs.Count == 0) return ToolResult.Ok("저장된 메모리가 없습니다."); var sb = new StringBuilder(); if (docs.Count > 0) { sb.AppendLine($"계층형 메모리 파일 {docs.Count}개:"); foreach (var doc in docs) { var priority = doc.Priority > 0 ? $" (우선순위 {doc.Priority})" : ""; var suffix = string.IsNullOrWhiteSpace(doc.Description) ? "" : $" — {doc.Description}"; var scopeHint = doc.Paths.Count > 0 ? $" (paths: {string.Join(", ", doc.Paths)})" : ""; sb.AppendLine($" • [{doc.Label}] {doc.Path}{priority}{suffix}{scopeHint}"); } sb.AppendLine(); } if (all.Count > 0) { sb.AppendLine($"학습 메모리 {all.Count}개:"); foreach (var group in all.GroupBy(e => e.Type)) { sb.AppendLine($"\n[{group.Key}]"); foreach (var e in group.OrderByDescending(e => e.UseCount)) sb.AppendLine($" • {e.Content} (사용 {e.UseCount}회, ID: {e.Id})"); } } return ToolResult.Ok(sb.ToString()); } private static ToolResult ExecuteDelete(JsonElement args, AgentMemoryService svc) { var id = args.TryGetProperty("id", out var i) ? i.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(id)) return ToolResult.Fail("id가 필요합니다."); return svc.Remove(id) ? ToolResult.Ok($"메모리 삭제됨 (ID: {id})") : ToolResult.Fail($"해당 ID의 메모리를 찾을 수 없습니다: {id}"); } private static ToolResult ExecuteSaveScope(JsonElement args, AgentMemoryService svc, AgentContext context) { var scope = args.TryGetProperty("scope", out var s) ? s.GetString() ?? "" : ""; var content = args.TryGetProperty("content", out var c) ? c.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(scope)) return ToolResult.Fail("scope가 필요합니다. managed | user | project | local 중 선택하세요."); if (string.IsNullOrWhiteSpace(content)) return ToolResult.Fail("content가 필요합니다."); var result = svc.SaveInstruction(scope, content, context.WorkFolder); return result.Path.Length == 0 ? ToolResult.Fail(result.Message) : ToolResult.Ok($"{result.Message}\n경로: {result.Path}"); } private static ToolResult ExecuteDeleteScope(JsonElement args, AgentMemoryService svc, AgentContext context) { var scope = args.TryGetProperty("scope", out var s) ? s.GetString() ?? "" : ""; var query = args.TryGetProperty("query", out var q) ? q.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(scope)) return ToolResult.Fail("scope가 필요합니다. managed | user | project | local 중 선택하세요."); if (string.IsNullOrWhiteSpace(query)) return ToolResult.Fail("query가 필요합니다."); var result = svc.DeleteInstruction(scope, query, context.WorkFolder); return result.Changed ? ToolResult.Ok($"{result.Message}\n경로: {result.Path}") : ToolResult.Fail(result.Message); } private static ToolResult ExecuteShowScope(JsonElement args, AgentMemoryService svc, AgentContext context) { var scope = args.TryGetProperty("scope", out var s) ? s.GetString() ?? "" : ""; if (string.IsNullOrWhiteSpace(scope)) return ToolResult.Fail("scope가 필요합니다. managed | user | project | local 중 선택하세요."); var path = svc.GetWritableInstructionPath(scope, context.WorkFolder); if (string.IsNullOrWhiteSpace(path)) return ToolResult.Fail("해당 scope의 메모리 파일 경로를 결정할 수 없습니다."); var content = svc.ReadInstructionFile(scope, context.WorkFolder); if (content == null) return ToolResult.Ok($"메모리 파일이 아직 없습니다.\n경로: {path}"); return ToolResult.Ok($"메모리 파일 경로: {path}\n\n{content}"); } }