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.UseIdentity,
|
||||||
a.UseUserInfo,
|
a.UseUserInfo,
|
||||||
a.UseMemory,
|
a.UseMemory,
|
||||||
|
a.UseAutoContext,
|
||||||
a.UseWebSearch,
|
a.UseWebSearch,
|
||||||
a.WebSearchVersion,
|
a.WebSearchVersion,
|
||||||
a.AllowUserChoice,
|
a.AllowUserChoice,
|
||||||
@@ -119,6 +120,7 @@ public static class AgentEndpoint
|
|||||||
UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(),
|
UseIdentity = !body.TryGetProperty("useIdentity", out var ui) || ui.GetBoolean(),
|
||||||
UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(),
|
UseUserInfo = !body.TryGetProperty("useUserInfo", out var uu) || uu.GetBoolean(),
|
||||||
UseMemory = !body.TryGetProperty("useMemory", out var um) || um.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()),
|
UseWebSearch = body.TryGetProperty("webSearchVersion", out var wsv) && !string.IsNullOrEmpty(wsv.GetString()),
|
||||||
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "",
|
WebSearchVersion = body.TryGetProperty("webSearchVersion", out var wsv2) ? wsv2.GetString() ?? "" : "",
|
||||||
AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(),
|
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("useIdentity", out var ui)) agent.UseIdentity = ui.GetBoolean();
|
||||||
if (body.TryGetProperty("useUserInfo", out var uu)) agent.UseUserInfo = uu.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("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))
|
if (body.TryGetProperty("webSearchVersion", out var wsv))
|
||||||
{
|
{
|
||||||
agent.WebSearchVersion = wsv.GetString() ?? "";
|
agent.WebSearchVersion = wsv.GetString() ?? "";
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ public static class ChatEndpoint
|
|||||||
|
|
||||||
try
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class AgentRecord
|
|||||||
public bool UseIdentity { get; set; } = true;
|
public bool UseIdentity { get; set; } = true;
|
||||||
public bool UseUserInfo { get; set; } = true;
|
public bool UseUserInfo { get; set; } = true;
|
||||||
public bool UseMemory { get; set; } = true;
|
public bool UseMemory { get; set; } = true;
|
||||||
|
public bool UseAutoContext { get; set; } = true;
|
||||||
public bool UseWebSearch { get; set; }
|
public bool UseWebSearch { get; set; }
|
||||||
public string WebSearchVersion { get; set; } = "";
|
public string WebSearchVersion { get; set; } = "";
|
||||||
public bool AllowUserChoice { get; set; } = true;
|
public bool AllowUserChoice { get; set; } = true;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public class AnthropicService
|
|||||||
SubAgentContext finalizer = null,
|
SubAgentContext finalizer = null,
|
||||||
bool allowUserChoice = true,
|
bool allowUserChoice = true,
|
||||||
bool planMode = false,
|
bool planMode = false,
|
||||||
|
bool useAutoContext = true,
|
||||||
CancellationToken workCt = default,
|
CancellationToken workCt = default,
|
||||||
CancellationToken hardCt = default,
|
CancellationToken hardCt = default,
|
||||||
CancellationToken sseCt = default,
|
CancellationToken sseCt = default,
|
||||||
@@ -76,6 +77,46 @@ public class AnthropicService
|
|||||||
|
|
||||||
try
|
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)
|
while (iterations < maxIterations)
|
||||||
{
|
{
|
||||||
// Hard-Stop: Sofortiger Abbruch aus while-Loop (Doppel-Klick Stop)
|
// Hard-Stop: Sofortiger Abbruch aus while-Loop (Doppel-Klick Stop)
|
||||||
@@ -838,7 +879,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
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 (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1044,7 +1085,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
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 (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1345,7 +1386,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
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 (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1468,6 +1509,35 @@ public class AnthropicService
|
|||||||
: result;
|
: 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>
|
/// <summary>
|
||||||
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
|
/// Extrahiert Web-Search-Sources aus einem web_search_tool_result-Block.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ public class DatabaseService
|
|||||||
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
|
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_user_info", "INTEGER NOT NULL DEFAULT 1");
|
||||||
TryAddColumn(conn, "agents", "use_memory", "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, "messages", "deleted_on", "TEXT");
|
||||||
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
|
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
|
||||||
@@ -472,8 +473,8 @@ public class DatabaseService
|
|||||||
agent.CreatedAt = now;
|
agent.CreatedAt = now;
|
||||||
agent.UpdatedAt = now;
|
agent.UpdatedAt = now;
|
||||||
await conn.ExecuteAsync(@"
|
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)
|
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, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
|
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);
|
", agent);
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
@@ -507,7 +508,7 @@ public class DatabaseService
|
|||||||
UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description,
|
UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description,
|
||||||
model = @Model, tool_choice = @ToolChoice,
|
model = @Model, tool_choice = @ToolChoice,
|
||||||
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
|
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,
|
allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction,
|
||||||
avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY,
|
avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY,
|
||||||
is_public = @IsPublic, updated_at = @UpdatedAt
|
is_public = @IsPublic, updated_at = @UpdatedAt
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -21,12 +22,14 @@ public class EmbeddingBackgroundService : BackgroundService
|
|||||||
readonly string _dbConnectionString;
|
readonly string _dbConnectionString;
|
||||||
readonly ConcurrentQueue<EmbeddingJob> _queue = new();
|
readonly ConcurrentQueue<EmbeddingJob> _queue = new();
|
||||||
readonly SemaphoreSlim _signal = new(0);
|
readonly SemaphoreSlim _signal = new(0);
|
||||||
|
readonly string _dataPath;
|
||||||
|
|
||||||
public EmbeddingBackgroundService(VectorSearchService vectorSearch, EmbeddingService embedding, string dbPath)
|
public EmbeddingBackgroundService(VectorSearchService vectorSearch, EmbeddingService embedding, string dbPath)
|
||||||
{
|
{
|
||||||
_vectorSearch = vectorSearch;
|
_vectorSearch = vectorSearch;
|
||||||
_embedding = embedding;
|
_embedding = embedding;
|
||||||
_dbConnectionString = $"Data Source={dbPath}";
|
_dbConnectionString = $"Data Source={dbPath}";
|
||||||
|
_dataPath = Path.GetDirectoryName(dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Enqueue eine neue Message zum Embedden.</summary>
|
/// <summary>Enqueue eine neue Message zum Embedden.</summary>
|
||||||
@@ -50,6 +53,9 @@ public class EmbeddingBackgroundService : BackgroundService
|
|||||||
// Backfill bestehender Messages
|
// Backfill bestehender Messages
|
||||||
await BackfillAsync(ct);
|
await BackfillAsync(ct);
|
||||||
|
|
||||||
|
// Memory-Dateien embedden
|
||||||
|
await BackfillMemoryFilesAsync(ct);
|
||||||
|
|
||||||
// Live-Embedding neuer Messages
|
// Live-Embedding neuer Messages
|
||||||
Console.WriteLine("[EmbeddingBG] Waiting for new messages...");
|
Console.WriteLine("[EmbeddingBG] Waiting for new messages...");
|
||||||
while (!ct.IsCancellationRequested)
|
while (!ct.IsCancellationRequested)
|
||||||
@@ -62,12 +68,21 @@ public class EmbeddingBackgroundService : BackgroundService
|
|||||||
if (ct.IsCancellationRequested) break;
|
if (ct.IsCancellationRequested) break;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _vectorSearch.EmbedMessageAsync(job.MessageId, job.ChainId, job.SessionId,
|
if (job.Role.StartsWith("memory:"))
|
||||||
job.AgentId, job.Role, job.Text);
|
{
|
||||||
|
// 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)
|
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.");
|
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);
|
record EmbeddingJob(long MessageId, string ChainId, string SessionId, string AgentId, string Role, string Text);
|
||||||
|
|
||||||
class BackfillRow
|
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)
|
switch (name)
|
||||||
{
|
{
|
||||||
@@ -628,6 +628,13 @@ public class ToolService
|
|||||||
memoryService.AppendMemoryFile(agentId, path, content);
|
memoryService.AppendMemoryFile(agentId, path, content);
|
||||||
else
|
else
|
||||||
memoryService.WriteMemoryFile(agentId, path, content);
|
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)";
|
return $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)";
|
||||||
}
|
}
|
||||||
case "memory_list":
|
case "memory_list":
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
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_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_agent ON message_embeddings(agent_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_emb_message ON message_embeddings(message_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.");
|
Console.WriteLine("[VectorSearch] Schema initialized.");
|
||||||
}
|
}
|
||||||
@@ -188,6 +200,135 @@ public class VectorSearchService
|
|||||||
return new HashSet<long>(ids);
|
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 class EmbeddingRow
|
||||||
{
|
{
|
||||||
public long MessageId { get; set; }
|
public long MessageId { get; set; }
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
- [x] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen
|
- [x] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen
|
||||||
|
|
||||||
## Ideas / Backlog
|
## 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.
|
- [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
|
## Features aus Devy übernehmen
|
||||||
|
|||||||
@@ -227,6 +227,10 @@
|
|||||||
<input type="checkbox" id="agentUseMemory">
|
<input type="checkbox" id="agentUseMemory">
|
||||||
Zugriff auf Erinnerungen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
|
Zugriff auf Erinnerungen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
|
||||||
</label>
|
</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>
|
<label>Web-Suche</label>
|
||||||
<select id="agentWebSearchVersion">
|
<select id="agentWebSearchVersion">
|
||||||
<option value="">Deaktiviert</option>
|
<option value="">Deaktiviert</option>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ registerPage('agent', {
|
|||||||
const useIdentityEl = document.getElementById('agentUseIdentity');
|
const useIdentityEl = document.getElementById('agentUseIdentity');
|
||||||
const useUserInfoEl = document.getElementById('agentUseUserInfo');
|
const useUserInfoEl = document.getElementById('agentUseUserInfo');
|
||||||
const useMemoryEl = document.getElementById('agentUseMemory');
|
const useMemoryEl = document.getElementById('agentUseMemory');
|
||||||
|
const useAutoContextEl = document.getElementById('agentUseAutoContext');
|
||||||
const webSearchVersionEl = document.getElementById('agentWebSearchVersion');
|
const webSearchVersionEl = document.getElementById('agentWebSearchVersion');
|
||||||
const avatarPreview = document.getElementById('avatarPreview');
|
const avatarPreview = document.getElementById('avatarPreview');
|
||||||
const saveBtn = document.getElementById('agentSaveBtn');
|
const saveBtn = document.getElementById('agentSaveBtn');
|
||||||
@@ -64,6 +65,7 @@ registerPage('agent', {
|
|||||||
useIdentityEl.checked = agent.useIdentity !== false;
|
useIdentityEl.checked = agent.useIdentity !== false;
|
||||||
useUserInfoEl.checked = agent.useUserInfo !== false;
|
useUserInfoEl.checked = agent.useUserInfo !== false;
|
||||||
useMemoryEl.checked = agent.useMemory !== false;
|
useMemoryEl.checked = agent.useMemory !== false;
|
||||||
|
useAutoContextEl.checked = agent.useAutoContext !== false;
|
||||||
webSearchVersionEl.value = agent.webSearchVersion || '';
|
webSearchVersionEl.value = agent.webSearchVersion || '';
|
||||||
isPublicEl.checked = agent.isPublic || false;
|
isPublicEl.checked = agent.isPublic || false;
|
||||||
deleteBtn.hidden = false;
|
deleteBtn.hidden = false;
|
||||||
@@ -105,6 +107,7 @@ registerPage('agent', {
|
|||||||
useIdentity: useIdentityEl.checked,
|
useIdentity: useIdentityEl.checked,
|
||||||
useUserInfo: useUserInfoEl.checked,
|
useUserInfo: useUserInfoEl.checked,
|
||||||
useMemory: useMemoryEl.checked,
|
useMemory: useMemoryEl.checked,
|
||||||
|
useAutoContext: useAutoContextEl.checked,
|
||||||
webSearchVersion: webSearchVersionEl.value,
|
webSearchVersion: webSearchVersionEl.value,
|
||||||
avatar: _agentAvatarEditor.getImage(),
|
avatar: _agentAvatarEditor.getImage(),
|
||||||
avatarScale: _agentAvatarEditor.getScale(),
|
avatarScale: _agentAvatarEditor.getScale(),
|
||||||
|
|||||||
Reference in new issue
Block a user