From fde4a193b2f46aee787e390f13dadd4855855db6 Mon Sep 17 00:00:00 2001 From: Marcus Nimtz Date: Wed, 5 Aug 2026 10:04:29 +0200 Subject: [PATCH] =?UTF-8?q?v1.11.83=20=E2=80=94=20Admin-Diagnose-Endpoint?= =?UTF-8?q?=20f=C3=BCr=20Embedding-Modell-Labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/v1/ai/embedding-stats (admin-only, rein lesend, im Browser aufrufbar): zeigt gespeicherte Embedding-Modell-Labels + Anzahl vs. das aktuelle Query-Modell (Ping-Embed), plus totalEmbeddings/readyFiles/ohne-Embedding und ein mismatch-Flag + Hinweis. Grundlage, um den KI-Chat-0-Treffer-Fall (Label-Mismatch) ohne Log-Gewühl und ohne riskanten Reindex zu diagnostizieren. Lokal mit .NET 8 gebaut: 0 Fehler. VERSION 1.11.82 → 1.11.83. Co-Authored-By: Claude Opus 4.8 --- VERSION | 2 +- src/NimShare.Api/Controllers/AiController.cs | 65 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 89ce98e..cb797f8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.11.82 +1.11.83 diff --git a/src/NimShare.Api/Controllers/AiController.cs b/src/NimShare.Api/Controllers/AiController.cs index f39a1f0..8cb71c1 100644 --- a/src/NimShare.Api/Controllers/AiController.cs +++ b/src/NimShare.Api/Controllers/AiController.cs @@ -1011,6 +1011,71 @@ public async Task Reindex( return Ok(new { queued = ids.Count }); } + /// v1.11.83: Admin-Diagnose für den KI-Chat/Suche-"0 Treffer"-Fall. + /// Zeigt, mit welchem Embedding-Modell-Label die vorhandenen Vektoren + /// gespeichert sind vs. welches Label die aktuelle Query erzeugt — ein + /// Mismatch ist die Ursache für "Keine passenden Dateien" trotz Index + /// (siehe RetrieveHitsAsync). Rein lesend; als Admin im Browser aufrufbar: + /// GET /api/v1/ai/embedding-stats + [Authorize(Policy = "ApiUser")] + [HttpGet("embedding-stats")] + public async Task EmbeddingStats( + [FromServices] ICurrentUserService users, CancellationToken ct) + { + var me = await users.GetOrProvisionAsync(User, ct); + if (me.Role != UserRole.Admin) + return Problem(statusCode: 403, title: "Nur für Admins."); + + // Nur die Modell-Labels laden (nicht die Vektor-Bytes) und im Speicher + // gruppieren — leicht und ohne EF-GroupBy-Übersetzungsrisiko auf SQLite. + var labels = await _db.FileEmbeddings.Select(e => e.Model).ToListAsync(ct); + var storedModels = labels + .GroupBy(m => m) + .Select(g => new { model = g.Key, count = g.Count() }) + .OrderByDescending(x => x.count) + .ToList(); + + var totalEmbeddings = labels.Count; + var readyFiles = await _db.Files.CountAsync( + f => f.Status == StorageFileStatus.Ready, ct); + var filesWithoutEmbedding = await _db.Files.CountAsync( + f => f.Status == StorageFileStatus.Ready + && !_db.FileEmbeddings.Any(e => e.FileId == f.Id), ct); + + // Aktuelles Query-Modell live ermitteln (ein billiger Ping-Embed). + string? queryModel = null; + string? providerError = null; + try + { + var provider = await _ai.CreateProviderAsync(ct); + var res = await provider.EmbedAsync("ping", ct); + if (res is { } r) queryModel = r.Model; + else providerError = (provider as OpenAiProvider)?.LastError + ?? (provider as GeminiProvider)?.LastError + ?? "Provider lieferte kein Embedding."; + } + catch (Exception ex) { providerError = ex.Message; } + + var mismatch = queryModel != null && storedModels.Count > 0 + && storedModels.All(s => s.model != queryModel); + + return Ok(new + { + queryModel, + providerError, + storedModels, + totalEmbeddings, + readyFiles, + filesWithoutEmbedding, + mismatch, + hint = storedModels.Count == 0 + ? "Keine Embeddings vorhanden — Index ist leer." + : mismatch + ? "MISMATCH: gespeicherte Vektoren tragen ein anderes Modell-Label als die aktuelle Query → Relabel (wenn real dasselbe Modell) oder gezieltes Re-Embed nötig, KEIN Reindex." + : "Kein Label-Mismatch: Query- und Index-Label passen zusammen." + }); + } + // ── #4 Guided upload-request cover email ─────────────────────────────── public record GuidedUrReq(Guid LinkId, string RecipientEmail, string? Context);