using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Windows; namespace AxCopilot.Services; public class AgentMemoryService { private static readonly string MemoryDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "memory"); private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { WriteIndented = true, PropertyNameCaseInsensitive = true }; private readonly List _entries = new List(); private readonly object _lock = new object(); private string? _currentWorkFolder; public int Count { get { lock (_lock) { return _entries.Count; } } } public IReadOnlyList All { get { lock (_lock) { return _entries.ToList(); } } } public void Load(string? workFolder) { lock (_lock) { _entries.Clear(); _currentWorkFolder = workFolder; string filePath = GetFilePath(null); LoadFromFile(filePath); if (!string.IsNullOrEmpty(workFolder)) { string filePath2 = GetFilePath(workFolder); LoadFromFile(filePath2); } } } public MemoryEntry Add(string type, string content, string? source = null, string? workFolder = null) { lock (_lock) { MemoryEntry memoryEntry = _entries.FirstOrDefault((MemoryEntry e) => e.Type == type && CalculateSimilarity(e.Content, content) > 0.8); if (memoryEntry != null) { memoryEntry.LastUsedAt = DateTime.Now; memoryEntry.UseCount++; Save(); return memoryEntry; } MemoryEntry memoryEntry2 = new MemoryEntry { Id = Guid.NewGuid().ToString("N").Substring(0, 12), Type = type, Content = content, Source = (source ?? ""), CreatedAt = DateTime.Now, LastUsedAt = DateTime.Now, UseCount = 1, Relevance = 1.0, WorkFolder = workFolder }; _entries.Add(memoryEntry2); Prune(); Save(); return memoryEntry2; } } public List GetRelevant(string query, int maxCount = 10) { if (string.IsNullOrWhiteSpace(query)) { return new List(); } lock (_lock) { HashSet queryTokens = Tokenize(query); return (from x in _entries.Select(delegate(MemoryEntry e) { HashSet second = Tokenize(e.Content); int num = queryTokens.Intersect(second, StringComparer.OrdinalIgnoreCase).Count(); double num2 = ((queryTokens.Count > 0) ? ((double)num / (double)queryTokens.Count) : 0.0); num2 += Math.Min((double)e.UseCount * 0.05, 0.3); if ((DateTime.Now - e.LastUsedAt).TotalDays < 7.0) { num2 += 0.1; } return (Entry: e, Score: num2); }) where x.Score > 0.1 orderby x.Score descending select x).Take(maxCount).Select(delegate((MemoryEntry Entry, double Score) x) { x.Entry.Relevance = x.Score; return x.Entry; }).ToList(); } } public void Touch(string id) { lock (_lock) { MemoryEntry memoryEntry = _entries.FirstOrDefault((MemoryEntry e) => e.Id == id); if (memoryEntry != null) { memoryEntry.UseCount++; memoryEntry.LastUsedAt = DateTime.Now; Save(); } } } public bool Remove(string id) { lock (_lock) { bool flag = _entries.RemoveAll((MemoryEntry e) => e.Id == id) > 0; if (flag) { Save(); } return flag; } } public void Clear() { lock (_lock) { _entries.Clear(); Save(); } } private void Prune() { int num = 100; try { num = ((!(Application.Current is App app)) ? ((int?)null) : app.SettingsService?.Settings.Llm.MaxMemoryEntries) ?? 100; } catch { } if (_entries.Count <= num) { return; } List list = (from e in _entries orderby e.UseCount, e.LastUsedAt select e).Take(_entries.Count - num).ToList(); foreach (MemoryEntry item in list) { _entries.Remove(item); } } private void Save() { Directory.CreateDirectory(MemoryDir); List entries = _entries.Where((MemoryEntry e) => e.WorkFolder == null).ToList(); SaveToFile(GetFilePath(null), entries); if (!string.IsNullOrEmpty(_currentWorkFolder)) { List entries2 = _entries.Where((MemoryEntry e) => e.WorkFolder != null).ToList(); SaveToFile(GetFilePath(_currentWorkFolder), entries2); } } private void SaveToFile(string path, List entries) { try { string plainText = JsonSerializer.Serialize(entries, JsonOptions); string contents = CryptoService.PortableEncrypt(plainText); string text = path + ".tmp"; File.WriteAllText(text, contents); File.Move(text, path, overwrite: true); } catch (Exception ex) { LogService.Warn("메모리 저장 실패: " + ex.Message); } } private void LoadFromFile(string path) { if (!File.Exists(path)) { return; } try { string @base = File.ReadAllText(path); string text = CryptoService.PortableDecrypt(@base); if (!string.IsNullOrEmpty(text)) { List list = JsonSerializer.Deserialize>(text, JsonOptions); if (list != null) { _entries.AddRange(list); } } } catch (Exception ex) { LogService.Warn("메모리 로드 실패 (" + path + "): " + ex.Message); } } private static string GetFilePath(string? workFolder) { if (string.IsNullOrEmpty(workFolder)) { return Path.Combine(MemoryDir, "_global.dat"); } string text = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(workFolder.ToLowerInvariant()))).Substring(0, 16); return Path.Combine(MemoryDir, text + ".dat"); } private static double CalculateSimilarity(string a, string b) { HashSet hashSet = Tokenize(a); HashSet hashSet2 = Tokenize(b); if (hashSet.Count == 0 || hashSet2.Count == 0) { return 0.0; } int num = hashSet.Intersect(hashSet2, StringComparer.OrdinalIgnoreCase).Count(); int num2 = hashSet.Union(hashSet2, StringComparer.OrdinalIgnoreCase).Count(); return (num2 > 0) ? ((double)num / (double)num2) : 0.0; } private static HashSet Tokenize(string text) { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); StringBuilder stringBuilder = new StringBuilder(); foreach (char c in text) { if (char.IsLetterOrDigit(c) || c == '_') { stringBuilder.Append(c); } else if (stringBuilder.Length > 1) { hashSet.Add(stringBuilder.ToString()); stringBuilder.Clear(); } else { stringBuilder.Clear(); } } if (stringBuilder.Length > 1) { hashSet.Add(stringBuilder.ToString()); } return hashSet; } }