Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Services/AgentMemoryService.cs

290 lines
6.6 KiB
C#

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<MemoryEntry> _entries = new List<MemoryEntry>();
private readonly object _lock = new object();
private string? _currentWorkFolder;
public int Count
{
get
{
lock (_lock)
{
return _entries.Count;
}
}
}
public IReadOnlyList<MemoryEntry> 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<MemoryEntry> GetRelevant(string query, int maxCount = 10)
{
if (string.IsNullOrWhiteSpace(query))
{
return new List<MemoryEntry>();
}
lock (_lock)
{
HashSet<string> queryTokens = Tokenize(query);
return (from x in _entries.Select(delegate(MemoryEntry e)
{
HashSet<string> second = Tokenize(e.Content);
int num = queryTokens.Intersect<string>(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<MemoryEntry> 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<MemoryEntry> entries = _entries.Where((MemoryEntry e) => e.WorkFolder == null).ToList();
SaveToFile(GetFilePath(null), entries);
if (!string.IsNullOrEmpty(_currentWorkFolder))
{
List<MemoryEntry> entries2 = _entries.Where((MemoryEntry e) => e.WorkFolder != null).ToList();
SaveToFile(GetFilePath(_currentWorkFolder), entries2);
}
}
private void SaveToFile(string path, List<MemoryEntry> 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<MemoryEntry> list = JsonSerializer.Deserialize<List<MemoryEntry>>(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<string> hashSet = Tokenize(a);
HashSet<string> hashSet2 = Tokenize(b);
if (hashSet.Count == 0 || hashSet2.Count == 0)
{
return 0.0;
}
int num = hashSet.Intersect<string>(hashSet2, StringComparer.OrdinalIgnoreCase).Count();
int num2 = hashSet.Union<string>(hashSet2, StringComparer.OrdinalIgnoreCase).Count();
return (num2 > 0) ? ((double)num / (double)num2) : 0.0;
}
private static HashSet<string> Tokenize(string text)
{
HashSet<string> hashSet = new HashSet<string>(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;
}
}