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:
1 parent
add111ffe1
commit
148d1459a3
11 files changed
+305
-12
No files matched your search
@@ -64,6 +64,7 @@ public static class AgentEndpoint
|
||||
a.UseIdentity,
|
||||
a.UseUserInfo,
|
||||
a.UseMemory,
|
||||
a.UseAutoContext,
|
||||
a.UseWebSearch,
|
||||
a.WebSearchVersion,
|
||||
a.AllowUserChoice,
|
||||
@@ -119,6 +120,7 @@ public static class AgentEndpoint
|
||||
UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(),
|
||||
UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(),
|
||||
UseMemory = !body.TryGetProperty("useMemory", out var um) || um.GetBoolean(),
|
||||
UseAutoContext = !body.TryGetProperty("useAutoContext", out var uac) || uac.GetBoolean(),
|
||||
UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()),
|
||||
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "",
|
||||
AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(),
|
||||
@@ -160,6 +162,7 @@ public static class AgentEndpoint
|
||||
if (body.TryGetProperty("useIdentity", out var ui)) agent.UseIdentity = ui.GetBoolean();
|
||||
if (body.TryGetProperty("useUserInfo", out var uu)) agent.UseUserInfo = uu.GetBoolean();
|
||||
if (body.TryGetProperty("useMemory", out var um)) agent.UseMemory = um.GetBoolean();
|
||||
if (body.TryGetProperty("useAutoContext", out var uac)) agent.UseAutoContext = uac.GetBoolean();
|
||||
if (body.TryGetProperty("webSearchVersion", out var wsv))
|
||||
{
|
||||
agent.WebSearchVersion = wsv.GetString() ?? "";
|
||||
|
||||
@@ -208,7 +208,7 @@ public static class ChatEndpoint
|
||||
|
||||
try
|
||||
{
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ public class AgentRecord
|
||||
public bool UseIdentity { get; set; } = true;
|
||||
public bool UseUserInfo { get; set; } = true;
|
||||
public bool UseMemory { get; set; } = true;
|
||||
public bool UseAutoContext { get; set; } = true;
|
||||
public bool UseWebSearch { get; set; }
|
||||
public string WebSearchVersion { get; set; } = "";
|
||||
public bool AllowUserChoice { get; set; } = true;
|
||||
|
||||
@@ -55,6 +55,7 @@ public class AnthropicService
|
||||
SubAgentContext finalizer = null,
|
||||
bool allowUserChoice = true,
|
||||
bool planMode = false,
|
||||
bool useAutoContext = true,
|
||||
CancellationToken workCt = default,
|
||||
CancellationToken hardCt = default,
|
||||
CancellationToken sseCt = default,
|
||||
@@ -76,6 +77,46 @@ public class AnthropicService
|
||||
|
||||
try
|
||||
{
|
||||
// ── Automatische Kontext-Injektion via Embedding-Suche ──
|
||||
if (useAutoContext && _vectorSearch != null && systemPrompt != null && messages.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Letzte User-Message als Query extrahieren
|
||||
var lastUserText = ExtractLastUserText(messages);
|
||||
if (!string.IsNullOrWhiteSpace(lastUserText))
|
||||
{
|
||||
var contextResults = await _vectorSearch.SearchAsync(lastUserText, agentId, topN: 5, minScore: 0.45f);
|
||||
// Nur Ergebnisse aus anderen Sessions (nicht aktuelle Chain) und nicht zu kurze
|
||||
contextResults.RemoveAll(r => r.ChainId == chainId || (r.TextPreview?.Length ?? 0) < 20);
|
||||
// Memory-Dateien durchsuchen
|
||||
var memoryResults = await _vectorSearch.SearchMemoryFilesAsync(lastUserText, agentId, topN: 3, minScore: 0.45f);
|
||||
|
||||
if (contextResults.Count > 0 || memoryResults.Count > 0)
|
||||
{
|
||||
var ctxSb = new StringBuilder();
|
||||
ctxSb.AppendLine("\n<recalled_context>");
|
||||
ctxSb.AppendLine("Die folgende Information wurde automatisch abgerufen und könnte relevant sein:");
|
||||
foreach (var r in contextResults)
|
||||
{
|
||||
var date = DateTime.TryParse(r.CreatedAt, out var dt) ? dt.ToString("yyyy-MM-dd HH:mm") : r.CreatedAt;
|
||||
ctxSb.AppendLine($"- [Gespräch, {r.Role}, {date}]: {r.TextPreview}");
|
||||
}
|
||||
foreach (var r in memoryResults)
|
||||
{
|
||||
ctxSb.AppendLine($"- [Memory, {r.FilePath}]: {r.TextPreview}");
|
||||
}
|
||||
ctxSb.AppendLine("</recalled_context>");
|
||||
systemPrompt.Add(new { type = "text", text = ctxSb.ToString() });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[AutoContext] Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
while (iterations < maxIterations)
|
||||
{
|
||||
// Hard-Stop: Sofortiger Abbruch aus while-Loop (Doppel-Klick Stop)
|
||||
@@ -838,7 +879,7 @@ public class AnthropicService
|
||||
|
||||
string toolResult;
|
||||
bool isError = false;
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch); }
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||
|
||||
@@ -1044,7 +1085,7 @@ public class AnthropicService
|
||||
|
||||
string toolResult;
|
||||
bool isError = false;
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch); }
|
||||
try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||
|
||||
@@ -1345,7 +1386,7 @@ public class AnthropicService
|
||||
|
||||
string toolResult;
|
||||
bool isError = false;
|
||||
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch); }
|
||||
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); }
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||
|
||||
@@ -1468,6 +1509,35 @@ public class AnthropicService
|
||||
: result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extrahiert den Text der letzten User-Message aus der Message-Liste.
|
||||
/// </summary>
|
||||
private static string ExtractLastUserText(List<object> messages)
|
||||
{
|
||||
for (int i = messages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(messages[i]);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
if (!el.TryGetProperty("role", out var role) || role.GetString() != "user") continue;
|
||||
if (!el.TryGetProperty("content", out var content)) continue;
|
||||
|
||||
if (content.ValueKind == JsonValueKind.String)
|
||||
return content.GetString();
|
||||
|
||||
if (content.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var block in content.EnumerateArray())
|
||||
{
|
||||
var type = block.TryGetProperty("type", out var t) ? t.GetString() : "";
|
||||
if (type == "text" && block.TryGetProperty("text", out var txt))
|
||||
return txt.GetString();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
|
||||
/// </summary>
|
||||
|
||||
@@ -149,6 +149,7 @@ public class DatabaseService
|
||||
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
|
||||
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
||||
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
||||
TryAddColumn(conn, "agents", "use_auto_context", "INTEGER NOT NULL DEFAULT 1");
|
||||
|
||||
TryAddColumn(conn, "messages", "deleted_on", "TEXT");
|
||||
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
|
||||
@@ -472,8 +473,8 @@ public class DatabaseService
|
||||
agent.CreatedAt = now;
|
||||
agent.UpdatedAt = now;
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
|
||||
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
|
||||
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
|
||||
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
|
||||
", agent);
|
||||
return agent;
|
||||
}
|
||||
@@ -507,7 +508,7 @@ public class DatabaseService
|
||||
UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description,
|
||||
model = @Model, tool_choice = @ToolChoice,
|
||||
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
|
||||
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion,
|
||||
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_auto_context = @UseAutoContext, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion,
|
||||
allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction,
|
||||
avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY,
|
||||
is_public = @IsPublic, updated_at = @UpdatedAt
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@@ -21,12 +22,14 @@ public class EmbeddingBackgroundService : BackgroundService
|
||||
readonly string _dbConnectionString;
|
||||
readonly ConcurrentQueue<EmbeddingJob> _queue = new();
|
||||
readonly SemaphoreSlim _signal = new(0);
|
||||
readonly string _dataPath;
|
||||
|
||||
public EmbeddingBackgroundService(VectorSearchService vectorSearch, EmbeddingService embedding, string dbPath)
|
||||
{
|
||||
_vectorSearch = vectorSearch;
|
||||
_embedding = embedding;
|
||||
_dbConnectionString = $"Data Source={dbPath}";
|
||||
_dataPath = Path.GetDirectoryName(dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Enqueue eine neue Message zum Embedden.</summary>
|
||||
@@ -50,6 +53,9 @@ public class EmbeddingBackgroundService : BackgroundService
|
||||
// Backfill bestehender Messages
|
||||
await BackfillAsync(ct);
|
||||
|
||||
// Memory-Dateien embedden
|
||||
await BackfillMemoryFilesAsync(ct);
|
||||
|
||||
// Live-Embedding neuer Messages
|
||||
Console.WriteLine("[EmbeddingBG] Waiting for new messages...");
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -62,12 +68,21 @@ public class EmbeddingBackgroundService : BackgroundService
|
||||
if (ct.IsCancellationRequested) break;
|
||||
try
|
||||
{
|
||||
await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId,
|
||||
job.AgentId, job.Role, job.Text);
|
||||
if (job.Role.StartsWith("memory:"))
|
||||
{
|
||||
// Memory-File-Embedding
|
||||
var filePath = job.Role.Substring("memory:".Length);
|
||||
await _vectorSearch.EmbedMemoryFileAsync(job.AgentId, filePath, job.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId,
|
||||
job.AgentId, job.Role, job.Text);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[EmbeddingBG] Error embedding message {job.MessageId}: {ex.Message}");
|
||||
Console.WriteLine($"[EmbeddingBG] Error embedding job: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,6 +148,54 @@ public class EmbeddingBackgroundService : BackgroundService
|
||||
Console.WriteLine($"[EmbeddingBG] Backfill complete: {embedded} new embeddings, {skipped} skipped, {total} total messages.");
|
||||
}
|
||||
|
||||
async Task BackfillMemoryFilesAsync(CancellationToken ct)
|
||||
{
|
||||
Console.WriteLine("[EmbeddingBG] Starting memory file backfill...");
|
||||
|
||||
var agentsDir = Path.Combine(_dataPath, "agents");
|
||||
if (!Directory.Exists(agentsDir))
|
||||
{
|
||||
Console.WriteLine("[EmbeddingBG] No agents directory found, skipping memory backfill.");
|
||||
return;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
foreach (var agentDir in Directory.GetDirectories(agentsDir))
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
var agentId = Path.GetFileName(agentDir);
|
||||
|
||||
// Alle .md Dateien im Agent-Verzeichnis (rekursiv)
|
||||
var mdFiles = Directory.GetFiles(agentDir, "*.md", SearchOption.AllDirectories);
|
||||
foreach (var mdFile in mdFiles)
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
try
|
||||
{
|
||||
var text = await File.ReadAllTextAsync(mdFile, ct);
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length < 10) continue;
|
||||
|
||||
var relativePath = Path.GetRelativePath(agentDir, mdFile);
|
||||
await _vectorSearch.EmbedMemoryFileAsync(agentId, relativePath, text);
|
||||
total++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[EmbeddingBG] Error embedding memory file {mdFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[EmbeddingBG] Memory file backfill complete: {total} files embedded.");
|
||||
}
|
||||
|
||||
/// <summary>Embeddet eine einzelne Memory-Datei (nach memory_write).</summary>
|
||||
public void EnqueueMemoryFile(string agentId, string filePath, string text)
|
||||
{
|
||||
_queue.Enqueue(new EmbeddingJob(0, "", "", agentId, "memory:" + filePath, text));
|
||||
_signal.Release();
|
||||
}
|
||||
|
||||
record EmbeddingJob(long MessageId, string ChainId, string SessionId, string AgentId, string Role, string Text);
|
||||
|
||||
class BackfillRow
|
||||
|
||||
@@ -354,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, VectorSearchService vectorSearch = 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, EmbeddingBackgroundService embeddingBg = null)
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
@@ -628,6 +628,13 @@ public class ToolService
|
||||
memoryService.AppendMemoryFile(agentId, path, content);
|
||||
else
|
||||
memoryService.WriteMemoryFile(agentId, path, content);
|
||||
// Memory-Datei nach Schreiben embedden
|
||||
if (embeddingBg != null)
|
||||
{
|
||||
var fullText = memoryService.ReadMemoryFile(agentId, path);
|
||||
if (!string.IsNullOrEmpty(fullText))
|
||||
embeddingBg.EnqueueMemoryFile(agentId, path, fullText);
|
||||
}
|
||||
return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)";
|
||||
}
|
||||
case "memory_list":
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
- [x] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen
|
||||
|
||||
## Ideas / Backlog
|
||||
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
|
||||
- [x] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
|
||||
- [x] ~~Claude (Sub-)Agents~~ – Nicht umsetzbar: Claude Code Sub-Agents sind ein CLI-Feature, nicht über die Messages API verfügbar. Eigenes Sub-Agent-System (Preparer/Review/Finalizer) deckt den Use-Case bereits ab.
|
||||
|
||||
## Features aus Devy übernehmen
|
||||
|
||||
@@ -227,6 +227,10 @@
|
||||
<input type="checkbox" id="agentUseMemory">
|
||||
Zugriff auf Erinnerungen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="agentUseAutoContext" checked>
|
||||
Automatischer Kontext <span class="checkbox-hint">(Relevante Erinnerungen werden automatisch abgerufen)</span>
|
||||
</label>
|
||||
<label>Web-Suche</label>
|
||||
<select id="agentWebSearchVersion">
|
||||
<option value="">Deaktiviert</option>
|
||||
|
||||
@@ -24,6 +24,7 @@ registerPage('agent', {
|
||||
const useIdentityEl = document.getElementById('agentUseIdentity');
|
||||
const useUserInfoEl = document.getElementById('agentUseUserInfo');
|
||||
const useMemoryEl = document.getElementById('agentUseMemory');
|
||||
const useAutoContextEl = document.getElementById('agentUseAutoContext');
|
||||
const webSearchVersionEl = document.getElementById('agentWebSearchVersion');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
const saveBtn = document.getElementById('agentSaveBtn');
|
||||
@@ -64,6 +65,7 @@ registerPage('agent', {
|
||||
useIdentityEl.checked = agent.useIdentity !== false;
|
||||
useUserInfoEl.checked = agent.useUserInfo !== false;
|
||||
useMemoryEl.checked = agent.useMemory !== false;
|
||||
useAutoContextEl.checked = agent.useAutoContext !== false;
|
||||
webSearchVersionEl.value = agent.webSearchVersion || '';
|
||||
isPublicEl.checked = agent.isPublic || false;
|
||||
deleteBtn.hidden = false;
|
||||
@@ -105,6 +107,7 @@ registerPage('agent', {
|
||||
useIdentity: useIdentityEl.checked,
|
||||
useUserInfo: useUserInfoEl.checked,
|
||||
useMemory: useMemoryEl.checked,
|
||||
useAutoContext: useAutoContextEl.checked,
|
||||
webSearchVersion: webSearchVersionEl.value,
|
||||
avatar: _agentAvatarEditor.getImage(),
|
||||
avatarScale: _agentAvatarEditor.getScale(),
|
||||
|
||||
Reference in new issue
Block a user