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을 확인했습니다.
219 lines
11 KiB
C#
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}");
|
|
}
|
|
}
|