Files
gott/Endpoints/EmbeddingDebugEndpoint.cs
T
MelekiandClaude Opus 4.6 3f2baa1639 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>
2026-04-01 08:50:41 +02:00

199 lines
7.1 KiB
C#

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."
});
}
}