Auto-Context: Semantische Kontext-Injektion, Memory-File-Embeddings, Agent-Toggle

- Automatische Embedding-basierte Kontext-Injektion vor jeder API-Anfrage
  (Top 5 Chat-Ergebnisse + Top 3 Memory-File-Treffer als recalled_context)
- Memory-Dateien (.md) werden embedded (Backfill + Live nach memory_write)
- Neue Agent-Option 'useAutoContext' (Checkbox, Default: aktiviert)
- VectorSearchService: SearchMemoryFilesAsync, EmbedMemoryFileAsync, Chunking
- EmbeddingBackgroundService: Memory-File-Backfill + EnqueueMemoryFile
- ToolService: memory_write löst Re-Embedding aus
- embeddingBg Parameter an ExecuteTool durchgereicht
This commit is contained in:
Meleki committed 2026-03-11 09:45:50 +01:00
1 parent add111ffe1
commit 148d1459a3
11 files changed
+305 -12

No files matched your search

+141
View File
@@ -1,4 +1,5 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -49,6 +50,17 @@ public class VectorSearchService
CREATE INDEX IF NOT EXISTS idx_emb_session ON message_embeddings(session_id);
CREATE INDEX IF NOT EXISTS idx_emb_agent ON message_embeddings(agent_id);
CREATE INDEX IF NOT EXISTS idx_emb_message ON message_embeddings(message_id);
CREATE TABLE IF NOT EXISTS memory_file_embeddings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
file_path TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
text_preview TEXT,
vector BLOB NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memfile_agent ON memory_file_embeddings(agent_id);
");
Console.WriteLine("[VectorSearch] Schema initialized.");
}
@@ -188,6 +200,135 @@ public class VectorSearchService
return new HashSet<long>(ids);
}
/// <summary>
/// Generiert und speichert ein Embedding für eine Memory-Datei (oder aktualisiert es).
/// </summary>
public async Task EmbedMemoryFileAsync(string agentId, string filePath, string text)
{
if (!_embedding.IsAvailable || string.IsNullOrWhiteSpace(text)) return;
if (text.Length < 10) return;
// Chunks erstellen: Memory-Dateien können lang sein → in 500-Zeichen-Chunks aufteilen
var chunks = ChunkText(text, 500);
using var conn = CreateConnection();
// Alte Embeddings für diese Datei löschen (bei Update)
await conn.ExecuteAsync(
"DELETE FROM memory_file_embeddings WHERE agent_id = @agentId AND file_path = @filePath",
new { agentId, filePath });
for (int i = 0; i < chunks.Count; i++)
{
float[] vector;
await _embeddingLock.WaitAsync();
try { vector = _embedding.GenerateEmbedding(chunks[i]); }
finally { _embeddingLock.Release(); }
if (vector.Length == 0) continue;
var preview = chunks[i].Length > 200 ? chunks[i].Substring(0, 200) : chunks[i];
await conn.ExecuteAsync(@"
INSERT INTO memory_file_embeddings
(agent_id, file_path, chunk_index, text_preview, vector, updated_at)
VALUES
(@agentId, @filePath, @chunkIndex, @preview, @vector, @updatedAt)",
new
{
agentId,
filePath,
chunkIndex = i,
preview,
vector = EmbeddingService.ToBytes(vector),
updatedAt = DateTime.UtcNow.ToString("o")
});
}
}
/// <summary>
/// Sucht über Memory-Dateien eines Agents.
/// </summary>
public async Task<List<MemorySearchResult>> SearchMemoryFilesAsync(string query, string agentId, int topN = 5, float minScore = 0.4f)
{
if (!_embedding.IsAvailable || string.IsNullOrEmpty(agentId)) return new List<MemorySearchResult>();
float[] queryVec;
await _embeddingLock.WaitAsync();
try { queryVec = _embedding.GenerateEmbedding(query); }
finally { _embeddingLock.Release(); }
if (queryVec.Length == 0) return new List<MemorySearchResult>();
using var conn = CreateConnection();
var rows = await conn.QueryAsync<MemoryFileRow>(
"SELECT agent_id, file_path, chunk_index, text_preview, vector, updated_at FROM memory_file_embeddings WHERE agent_id = @agentId",
new { agentId });
var results = new List<MemorySearchResult>();
foreach (var row in rows)
{
var vec = EmbeddingService.FromBytes(row.Vector);
var score = EmbeddingService.CosineSimilarity(queryVec, vec);
if (score >= minScore)
{
results.Add(new MemorySearchResult
{
AgentId = row.AgentId,
FilePath = row.FilePath,
ChunkIndex = row.ChunkIndex,
TextPreview = row.TextPreview,
Score = score,
UpdatedAt = row.UpdatedAt
});
}
}
return results.OrderByDescending(r => r.Score).Take(topN).ToList();
}
static List<string> ChunkText(string text, int chunkSize)
{
var chunks = new List<string>();
if (string.IsNullOrEmpty(text)) return chunks;
// Versuche an Absätzen zu trennen
var paragraphs = text.Split(new[] { "\n\n", "\r\n\r\n" }, StringSplitOptions.RemoveEmptyEntries);
var current = new StringBuilder();
foreach (var para in paragraphs)
{
if (current.Length + para.Length > chunkSize && current.Length > 0)
{
chunks.Add(current.ToString().Trim());
current.Clear();
}
if (current.Length > 0) current.AppendLine();
current.Append(para);
}
if (current.Length > 0)
chunks.Add(current.ToString().Trim());
return chunks;
}
public class MemoryFileRow
{
public string AgentId { get; set; }
public string FilePath { get; set; }
public int ChunkIndex { get; set; }
public string TextPreview { get; set; }
public byte[] Vector { get; set; }
public string UpdatedAt { get; set; }
}
public class MemorySearchResult
{
public string AgentId { get; set; }
public string FilePath { get; set; }
public int ChunkIndex { get; set; }
public string TextPreview { get; set; }
public float Score { get; set; }
public string UpdatedAt { get; set; }
}
public class EmbeddingRow
{
public long MessageId { get; set; }