Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/MemoryTool.cs
lacvet 917e61af20
Some checks failed
Release Gate / gate (push) Has been cancelled
메모리 계층 우선순위와 중복 병합 정책 정리
계층형 메모리 문서 로드 후 동일한 규칙 내용은 더 가까운 계층만 남기고 중복을 제거하도록 NormalizeInstructionDocuments 로직을 추가했습니다.

최종 메모리 문서는 managed, user, project, local 순서로 다시 정렬되고 우선순위 번호를 부여하며, MemoryTool의 list와 search 결과에서 그 우선순위를 함께 보여주도록 했습니다.

README와 DEVELOPMENT 문서에 2026-04-07 00:39 (KST) 기준 이력을 반영했고 Release 빌드 경고 0 오류 0을 확인했습니다.
2026-04-07 00:17:48 +09:00

219 lines
11 KiB
C#

using System.Text;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
/// <summary>
/// 에이전트 메모리 관리 도구.
/// 프로젝트 규칙, 사용자 선호도, 학습 내용을 저장/검색/삭제합니다.
/// </summary>
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<ToolResult> 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}");
}
}