feat: Semantische Suche über Chat-History (memory_search)

- Lokales Embedding-Model (all-MiniLM-L6-v2, ONNX, CPU)
- EmbeddingService: WordPiece-Tokenizer + Mean Pooling (384 Dim)
- VectorSearchService: SQLite-basierte Vektor-Speicherung + Cosine Similarity
- EmbeddingBackgroundService: Backfill bestehender Messages + Live-Queue
- Neues Tool memory_search: Semantische Suche über alle Sessions (inkl. archivierte)
- ~9000 Messages embedded, Suche über komplette Chat-History
This commit is contained in:
Meleki committed 2026-03-11 09:22:11 +01:00
1 parent baf53b7dac
commit add111ffe1
9 files changed
+727 -15

No files matched your search

+47 -3
View File
@@ -33,7 +33,7 @@ public class ToolService
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
{
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
"memory_read", "memory_write", "memory_list",
"memory_read", "memory_write", "memory_list", "memory_search",
"remote_execute", AskUserChoiceToolName
};
@@ -54,7 +54,7 @@ public class ToolService
return filtered.ToArray();
}
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false)
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false)
{
var allTools = new List<object>();
if (includeUserChoice)
@@ -63,6 +63,8 @@ public class ToolService
allTools.AddRange(GetFileTools());
if (includeMemory)
allTools.AddRange(GetMemoryToolDefinitions());
if (includeMemorySearch)
allTools.Add(GetMemorySearchTool());
if (includeRemote)
allTools.Add(GetRemoteExecuteTool());
return allTools.ToArray();
@@ -264,6 +266,25 @@ public class ToolService
};
}
private object GetMemorySearchTool()
{
return new
{
name = "memory_search",
description = "Semantische Suche über alle vergangenen Chat-Nachrichten (inklusive archivierter Sessions). Findet relevante Informationen aus früheren Gesprächen anhand von Bedeutung, nicht exakten Wörtern. Nutze dies wenn du Kontext aus vergangenen Interaktionen brauchst.",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["query"] = new { type = "string", description = "Suchanfrage – beschreibe wonach du suchst (z.B. 'Druckerproblem letzte Woche', 'Server-Migration Konfiguration')" },
["top_n"] = new { type = "integer", description = "Anzahl der Ergebnisse (Default: 10, Max: 30)" }
},
required = new[] { "query" }
}
};
}
private object GetRemoteExecuteTool()
{
return new
@@ -333,7 +354,7 @@ public class ToolService
};
}
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null)
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null, VectorSearchService vectorSearch = null)
{
switch (name)
{
@@ -617,6 +638,29 @@ public class ToolService
var files = memoryService.ListMemoryFiles(agentId, subPath);
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
}
case "memory_search":
{
if (vectorSearch == null)
return "Fehler: Semantische Suche nicht verfügbar";
var query = input.GetProperty("query").GetString() ?? "";
var topN = input.TryGetProperty("top_n", out var tn) ? tn.GetInt32() : 10;
if (topN > 30) topN = 30;
if (topN < 1) topN = 1;
var results = await vectorSearch.SearchAsync(query, agentId, topN);
if (results.Count == 0)
return "Keine relevanten Ergebnisse gefunden.";
var sb = new StringBuilder();
sb.AppendLine($"Gefunden: {results.Count} relevante Einträge\n");
for (int i = 0; i < results.Count; i++)
{
var r = results[i];
var date = DateTime.TryParse(r.CreatedAt, out var dt) ? dt.ToString("yyyy-MM-dd HH:mm") : r.CreatedAt;
sb.AppendLine($"[{i + 1}] Score: {r.Score:F2} | {r.Role} | {date} | Session: {r.SessionId}");
sb.AppendLine(r.TextPreview);
sb.AppendLine();
}
return sb.ToString().TrimEnd();
}
case "remote_execute":
{
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))