feat: AutoContext-Suche grundlegend verbessern + Embedding-Debug-Tool
Hybrid-Suche (Vektor + FTS5-Keyword), Recency-Boost, Query-Erweiterung (letzte 2-3 User-Messages), Chunk-Overlapping, FullText-Speicherung, konfigurierbares Embedding-Modell/MaxSeqLen, deduplizierter AutoContext- Code (Streaming + CLI shared), Admin-Diagnose-Seite mit Re-Embed-Funktion. Threshold von 0.45 auf 0.35 gesenkt (Hybrid-Suche ist präziser). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
be6d33691a
commit
3f2baa1639
14 files changed
+1103
-61
No files matched your search
@@ -254,7 +254,7 @@ public static class ChatEndpoint
|
||||
|
||||
if (anthropic.IsCliAvailable)
|
||||
{
|
||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, workCt: workCt, sseCt: sseCt);
|
||||
await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, workCt: workCt, sseCt: sseCt);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Extensions;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
public static class EmbeddingDebugEndpoint
|
||||
{
|
||||
public static void MapEmbeddingDebugEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/admin/embedding-debug", HandleDebugSearch);
|
||||
app.MapGet("/api/admin/embedding-debug/stats", HandleStats);
|
||||
app.MapPost("/api/admin/embedding-debug/re-embed", HandleReEmbed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnose-Suche: Zeigt Top-N mit Scores, inkl. Ergebnisse unter Schwellenwert.
|
||||
/// Query-Params: q (query), agentId, topN (default 20), chainId (zum Simulieren des Filters)
|
||||
/// </summary>
|
||||
private static async Task HandleDebugSearch(HttpContext ctx,
|
||||
VectorSearchService vectorSearch, EmbeddingService embeddingService)
|
||||
{
|
||||
if (!ctx.IsAdmin())
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!embeddingService.IsAvailable)
|
||||
{
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Embedding model not available" });
|
||||
return;
|
||||
}
|
||||
|
||||
var query = ctx.Request.Query["q"].ToString();
|
||||
var agentId = ctx.Request.Query["agentId"].ToString();
|
||||
var chainId = ctx.Request.Query["chainId"].ToString();
|
||||
var topN = 20;
|
||||
if (int.TryParse(ctx.Request.Query["topN"].ToString(), out var parsedTopN) && parsedTopN > 0)
|
||||
topN = Math.Min(parsedTopN, 100);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Query parameter 'q' is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Suche mit SEHR niedrigem Schwellenwert, damit wir alles sehen
|
||||
var allResults = await vectorSearch.SearchAsync(query,
|
||||
string.IsNullOrEmpty(agentId) ? null : agentId,
|
||||
topN: topN,
|
||||
minScore: 0.1f);
|
||||
|
||||
// Memory-Dateien auch durchsuchen
|
||||
var memoryResults = new List<VectorSearchService.MemorySearchResult>();
|
||||
if (!string.IsNullOrEmpty(agentId))
|
||||
{
|
||||
memoryResults = await vectorSearch.SearchMemoryFilesAsync(query, agentId, topN: 10, minScore: 0.1f);
|
||||
}
|
||||
|
||||
// Ergebnisse annotieren: Was würde der AutoContext-Filter machen?
|
||||
// Neuer Threshold ist 0.35 durch Hybrid-Suche
|
||||
var annotatedMessages = allResults.Select(r => new
|
||||
{
|
||||
r.MessageId,
|
||||
r.ChainId,
|
||||
r.SessionId,
|
||||
r.AgentId,
|
||||
r.Role,
|
||||
r.TextPreview,
|
||||
FullText = r.FullText ?? r.TextPreview,
|
||||
Score = Math.Round(r.Score, 4),
|
||||
r.CreatedAt,
|
||||
WouldPassThreshold = r.Score >= 0.35f,
|
||||
WouldBeFilteredByChain = !string.IsNullOrEmpty(chainId) && r.ChainId == chainId,
|
||||
WouldBeFilteredByLength = (r.TextPreview?.Length ?? 0) < 20,
|
||||
WouldAppearInAutoContext = r.Score >= 0.35f
|
||||
&& (string.IsNullOrEmpty(chainId) || r.ChainId != chainId)
|
||||
&& (r.TextPreview?.Length ?? 0) >= 20
|
||||
}).ToList();
|
||||
|
||||
var annotatedMemory = memoryResults.Select(r => new
|
||||
{
|
||||
r.AgentId,
|
||||
r.FilePath,
|
||||
r.ChunkIndex,
|
||||
r.TextPreview,
|
||||
FullText = r.FullText ?? r.TextPreview,
|
||||
Score = Math.Round(r.Score, 4),
|
||||
r.UpdatedAt,
|
||||
WouldPassThreshold = r.Score >= 0.35f
|
||||
}).ToList();
|
||||
|
||||
// Zusammenfassung
|
||||
var summary = new
|
||||
{
|
||||
Query = query,
|
||||
AgentId = agentId,
|
||||
ChainIdFilter = chainId,
|
||||
TotalMessageResults = annotatedMessages.Count,
|
||||
AboveThreshold035 = annotatedMessages.Count(r => r.WouldPassThreshold),
|
||||
BelowThreshold = annotatedMessages.Count(r => !r.WouldPassThreshold),
|
||||
WouldAppearInAutoContext = annotatedMessages.Count(r => r.WouldAppearInAutoContext),
|
||||
FilteredByChain = annotatedMessages.Count(r => r.WouldBeFilteredByChain),
|
||||
FilteredByLength = annotatedMessages.Count(r => r.WouldBeFilteredByLength),
|
||||
TotalMemoryResults = annotatedMemory.Count,
|
||||
MemoryAboveThreshold = annotatedMemory.Count(r => r.WouldPassThreshold)
|
||||
};
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
summary,
|
||||
messages = annotatedMessages,
|
||||
memory = annotatedMemory
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistiken: Anzahl Embeddings pro Agent, Gesamtzahl, etc.
|
||||
/// </summary>
|
||||
private static async Task HandleStats(HttpContext ctx,
|
||||
VectorSearchService vectorSearch, DatabaseService db)
|
||||
{
|
||||
if (!ctx.IsAdmin())
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||
return;
|
||||
}
|
||||
|
||||
using var conn = db.GetConnection();
|
||||
|
||||
var totalMessages = await Dapper.SqlMapper.ExecuteScalarAsync<int>(conn,
|
||||
"SELECT COUNT(*) FROM message_embeddings");
|
||||
var totalMemory = await Dapper.SqlMapper.ExecuteScalarAsync<int>(conn,
|
||||
"SELECT COUNT(*) FROM memory_file_embeddings");
|
||||
|
||||
var perAgentDynamic = await Dapper.SqlMapper.QueryAsync(conn,
|
||||
@"SELECT me.agent_id,
|
||||
a.name,
|
||||
COUNT(*) AS message_count,
|
||||
(SELECT COUNT(*) FROM memory_file_embeddings mfe WHERE mfe.agent_id = me.agent_id) AS memory_chunks
|
||||
FROM message_embeddings me
|
||||
LEFT JOIN agents a ON a.id = me.agent_id
|
||||
GROUP BY me.agent_id
|
||||
ORDER BY COUNT(*) DESC");
|
||||
|
||||
var perAgent = perAgentDynamic.Select(r => new
|
||||
{
|
||||
AgentId = (string)r.agent_id,
|
||||
AgentName = (string)r.name,
|
||||
MessageCount = (long)r.message_count,
|
||||
MemoryChunks = (long)r.memory_chunks
|
||||
}).ToList();
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
totalMessages,
|
||||
totalMemory,
|
||||
perAgent
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggert Re-Embedding aller Messages und Memory-Dateien.
|
||||
/// POST /api/admin/embedding-debug/re-embed
|
||||
/// </summary>
|
||||
private static async Task HandleReEmbed(HttpContext ctx,
|
||||
EmbeddingBackgroundService embeddingBg, EmbeddingService embeddingService)
|
||||
{
|
||||
if (!ctx.IsAdmin())
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Admin access required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!embeddingService.IsAvailable)
|
||||
{
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Embedding model not available" });
|
||||
return;
|
||||
}
|
||||
|
||||
embeddingBg.TriggerReEmbed();
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
status = "Re-embedding triggered",
|
||||
model = embeddingService.ModelName,
|
||||
info = "Background service will re-embed all messages and memory files. Check stats for progress."
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user