AX Agent 메모리 구조 1차 강화: 계층형 메모리 문서 로딩과 프롬프트 주입 추가
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- AgentMemoryService에 관리형/사용자/프로젝트/로컬 메모리 문서 탐색을 추가해 AXMEMORY.md, AXMEMORY.local.md, .ax/rules/*.md 계층을 로드하도록 확장함 - ChatWindow 시스템 프롬프트 메모리 섹션을 계층형 메모리와 기존 학습 메모리를 함께 조립하는 구조로 재편함 - 작업 폴더 메모리 로드 전에 Count를 먼저 검사하던 경로를 제거해 다른 폴더 메모리 누락 가능성을 줄임 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
This commit is contained in:
@@ -16,6 +16,11 @@ public class AgentMemoryService
|
||||
private static readonly string MemoryDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "memory");
|
||||
private static readonly string ManagedMemoryDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"AxCopilot", "memory");
|
||||
private const string MemoryFileName = "AXMEMORY.md";
|
||||
private const string LocalMemoryFileName = "AXMEMORY.local.md";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -24,6 +29,7 @@ public class AgentMemoryService
|
||||
};
|
||||
|
||||
private readonly List<MemoryEntry> _entries = new();
|
||||
private readonly List<MemoryInstructionDocument> _instructionDocuments = new();
|
||||
private readonly object _lock = new();
|
||||
private string? _currentWorkFolder;
|
||||
|
||||
@@ -33,12 +39,19 @@ public class AgentMemoryService
|
||||
/// <summary>모든 메모리 항목 (읽기 전용).</summary>
|
||||
public IReadOnlyList<MemoryEntry> All { get { lock (_lock) return _entries.ToList(); } }
|
||||
|
||||
/// <summary>현재 로드된 계층형 메모리 문서 (읽기 전용).</summary>
|
||||
public IReadOnlyList<MemoryInstructionDocument> InstructionDocuments
|
||||
{
|
||||
get { lock (_lock) return _instructionDocuments.ToList(); }
|
||||
}
|
||||
|
||||
/// <summary>작업 폴더별 메모리 + 전역 메모리를 로드합니다.</summary>
|
||||
public void Load(string? workFolder)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_entries.Clear();
|
||||
_instructionDocuments.Clear();
|
||||
_currentWorkFolder = workFolder;
|
||||
|
||||
// 전역 메모리
|
||||
@@ -51,6 +64,8 @@ public class AgentMemoryService
|
||||
var folderPath = GetFilePath(workFolder);
|
||||
LoadFromFile(folderPath);
|
||||
}
|
||||
|
||||
LoadInstructionDocuments(workFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +270,91 @@ public class AgentMemoryService
|
||||
return union > 0 ? (double)intersection / union : 0;
|
||||
}
|
||||
|
||||
private void LoadInstructionDocuments(string? workFolder)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddInstructionFileIfExists(Path.Combine(ManagedMemoryDir, MemoryFileName), "managed", "관리형 메모리", seen);
|
||||
AddInstructionRuleFilesIfExists(Path.Combine(ManagedMemoryDir, "rules"), "managed", "관리형 메모리", seen);
|
||||
|
||||
AddInstructionFileIfExists(Path.Combine(MemoryDir, MemoryFileName), "user", "사용자 메모리", seen);
|
||||
AddInstructionRuleFilesIfExists(Path.Combine(MemoryDir, "rules"), "user", "사용자 메모리", seen);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder))
|
||||
return;
|
||||
|
||||
foreach (var directory in EnumerateDirectoryChain(workFolder))
|
||||
{
|
||||
AddInstructionFileIfExists(Path.Combine(directory, MemoryFileName), "project", "프로젝트 메모리", seen);
|
||||
AddInstructionFileIfExists(Path.Combine(directory, ".ax", MemoryFileName), "project", "프로젝트 메모리", seen);
|
||||
AddInstructionRuleFilesIfExists(Path.Combine(directory, ".ax", "rules"), "project", "프로젝트 메모리", seen);
|
||||
AddInstructionFileIfExists(Path.Combine(directory, LocalMemoryFileName), "local", "로컬 메모리", seen);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInstructionRuleFilesIfExists(string rulesDirectory, string layer, string label, HashSet<string> seen)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(rulesDirectory))
|
||||
return;
|
||||
|
||||
foreach (var file in Directory.GetFiles(rulesDirectory, "*.md").OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
AddInstructionFileIfExists(file, layer, label, seen);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"메모리 rules 로드 실패 ({rulesDirectory}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInstructionFileIfExists(string path, string layer, string label, HashSet<string> seen)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return;
|
||||
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
if (!seen.Add(fullPath))
|
||||
return;
|
||||
|
||||
var content = File.ReadAllText(fullPath);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
return;
|
||||
|
||||
_instructionDocuments.Add(new MemoryInstructionDocument
|
||||
{
|
||||
Layer = layer,
|
||||
Label = label,
|
||||
Path = fullPath,
|
||||
Content = content.Trim()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"메모리 문서 로드 실패 ({path}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateDirectoryChain(string workFolder)
|
||||
{
|
||||
var current = Path.GetFullPath(workFolder);
|
||||
var stack = new Stack<string>();
|
||||
|
||||
while (!string.IsNullOrWhiteSpace(current))
|
||||
{
|
||||
stack.Push(current);
|
||||
var parent = Directory.GetParent(current)?.FullName;
|
||||
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, current, StringComparison.OrdinalIgnoreCase))
|
||||
break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
while (stack.Count > 0)
|
||||
yield return stack.Pop();
|
||||
}
|
||||
|
||||
/// <summary>텍스트를 토큰으로 분리합니다.</summary>
|
||||
private static HashSet<string> Tokenize(string text)
|
||||
{
|
||||
@@ -307,3 +407,19 @@ public class MemoryEntry
|
||||
[JsonPropertyName("workFolder")]
|
||||
public string? WorkFolder { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>CLAUDE.md 스타일의 계층형 메모리 문서.</summary>
|
||||
public class MemoryInstructionDocument
|
||||
{
|
||||
[JsonPropertyName("layer")]
|
||||
public string Layer { get; set; } = "project";
|
||||
|
||||
[JsonPropertyName("label")]
|
||||
public string Label { get; set; } = "프로젝트 메모리";
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user