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>
207 lines
11 KiB
JavaScript
207 lines
11 KiB
JavaScript
// ── Embedding Debug Page ──
|
|
|
|
registerPage('embedding-debug', {
|
|
async init() {
|
|
if (!currentUser || !currentUser.isAdmin) {
|
|
navigateTo('dashboard');
|
|
return;
|
|
}
|
|
|
|
var queryInput = document.getElementById('embDebugQuery');
|
|
var agentSelect = document.getElementById('embDebugAgent');
|
|
var topNInput = document.getElementById('embDebugTopN');
|
|
var searchBtn = document.getElementById('embDebugSearchBtn');
|
|
var statsBtn = document.getElementById('embDebugStatsBtn');
|
|
var reEmbedBtn = document.getElementById('embDebugReEmbedBtn');
|
|
var summaryEl = document.getElementById('embDebugSummary');
|
|
var resultsEl = document.getElementById('embDebugResults');
|
|
|
|
// Agents laden
|
|
try {
|
|
var resp = await fetch('/api/agents/available');
|
|
var agents = await resp.json();
|
|
(agents || []).forEach(function(a) {
|
|
var opt = document.createElement('option');
|
|
opt.value = a.agentId;
|
|
opt.textContent = a.name;
|
|
agentSelect.appendChild(opt);
|
|
});
|
|
} catch (e) { /* ignore */ }
|
|
|
|
searchBtn.addEventListener('click', doSearch);
|
|
queryInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') doSearch();
|
|
});
|
|
statsBtn.addEventListener('click', doStats);
|
|
|
|
if (reEmbedBtn) {
|
|
reEmbedBtn.addEventListener('click', async function() {
|
|
if (!confirm('Alle Embeddings löschen und neu generieren?\n\nDas kann bei vielen Messages einige Minuten dauern.')) return;
|
|
reEmbedBtn.disabled = true;
|
|
reEmbedBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft...';
|
|
try {
|
|
var resp = await fetch('/api/admin/embedding-debug/re-embed', { method: 'POST' });
|
|
var data = await resp.json();
|
|
summaryEl.innerHTML = '<div class="success-box">' + esc(data.status || data.error) + (data.model ? ' (Modell: ' + esc(data.model) + ')' : '') + '</div>';
|
|
} catch (e) {
|
|
summaryEl.innerHTML = '<div class="error-box">Fehler: ' + esc(e.message) + '</div>';
|
|
} finally {
|
|
reEmbedBtn.disabled = false;
|
|
reEmbedBtn.innerHTML = '<i class="fa fa-refresh"></i> Re-Embed All';
|
|
}
|
|
});
|
|
}
|
|
|
|
async function doSearch() {
|
|
var query = queryInput.value.trim();
|
|
if (!query) return;
|
|
searchBtn.disabled = true;
|
|
searchBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Suche...';
|
|
summaryEl.innerHTML = '';
|
|
resultsEl.innerHTML = '';
|
|
|
|
try {
|
|
var params = new URLSearchParams({
|
|
q: query,
|
|
agentId: agentSelect.value,
|
|
topN: topNInput.value || '20'
|
|
});
|
|
var resp = await fetch('/api/admin/embedding-debug?' + params);
|
|
var data = await resp.json();
|
|
|
|
if (data.error) {
|
|
summaryEl.innerHTML = '<div class="error-box">' + esc(data.error) + '</div>';
|
|
return;
|
|
}
|
|
|
|
renderSummary(data.summary);
|
|
renderResults(data.messages, data.memory);
|
|
} catch (e) {
|
|
summaryEl.innerHTML = '<div class="error-box">Fehler: ' + esc(e.message) + '</div>';
|
|
} finally {
|
|
searchBtn.disabled = false;
|
|
searchBtn.innerHTML = '<i class="fa fa-search"></i> Suchen';
|
|
}
|
|
}
|
|
|
|
async function doStats() {
|
|
statsBtn.disabled = true;
|
|
statsBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
|
summaryEl.innerHTML = '';
|
|
resultsEl.innerHTML = '';
|
|
|
|
try {
|
|
var resp = await fetch('/api/admin/embedding-debug/stats');
|
|
var data = await resp.json();
|
|
renderStats(data);
|
|
} catch (e) {
|
|
summaryEl.innerHTML = '<div class="error-box">Fehler: ' + esc(e.message) + '</div>';
|
|
} finally {
|
|
statsBtn.disabled = false;
|
|
statsBtn.innerHTML = '<i class="fa fa-chart-bar"></i> Statistiken';
|
|
}
|
|
}
|
|
|
|
function renderSummary(s) {
|
|
summaryEl.innerHTML =
|
|
'<div class="emb-summary">' +
|
|
'<div class="emb-summary-grid">' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + s.totalMessageResults + '</span><span class="emb-stat-label">Treffer gesamt</span></div>' +
|
|
'<div class="emb-stat good"><span class="emb-stat-num">' + s.aboveThreshold035 + '</span><span class="emb-stat-label">≥ 0.35 (Threshold)</span></div>' +
|
|
'<div class="emb-stat warn"><span class="emb-stat-num">' + s.belowThreshold + '</span><span class="emb-stat-label">< 0.35</span></div>' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + s.wouldAppearInAutoContext + '</span><span class="emb-stat-label">→ AutoContext</span></div>' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + s.filteredByChain + '</span><span class="emb-stat-label">Chain-gefiltert</span></div>' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + s.filteredByLength + '</span><span class="emb-stat-label">Zu kurz (<20)</span></div>' +
|
|
'</div>' +
|
|
(s.totalMemoryResults > 0 ?
|
|
'<div class="emb-summary-grid" style="margin-top:0.5rem">' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + s.totalMemoryResults + '</span><span class="emb-stat-label">Memory-Treffer</span></div>' +
|
|
'<div class="emb-stat good"><span class="emb-stat-num">' + s.memoryAboveThreshold + '</span><span class="emb-stat-label">Memory ≥ 0.35</span></div>' +
|
|
'</div>' : '') +
|
|
'</div>';
|
|
}
|
|
|
|
function renderResults(messages, memory) {
|
|
var html = '';
|
|
|
|
if (messages && messages.length > 0) {
|
|
html += '<h3 style="margin:1rem 0 0.5rem">Message-Ergebnisse</h3>';
|
|
html += '<div class="emb-results">';
|
|
messages.forEach(function(r, i) {
|
|
var scoreClass = r.score >= 0.35 ? 'score-good' : (r.score >= 0.2 ? 'score-medium' : 'score-low');
|
|
var filterTags = '';
|
|
if (r.wouldBeFilteredByChain) filterTags += '<span class="emb-tag filter">Chain</span>';
|
|
if (r.wouldBeFilteredByLength) filterTags += '<span class="emb-tag filter">Kurz</span>';
|
|
if (r.wouldAppearInAutoContext) filterTags += '<span class="emb-tag pass">AutoContext</span>';
|
|
else if (r.wouldPassThreshold) filterTags += '<span class="emb-tag threshold">Threshold OK</span>';
|
|
|
|
var hasFullText = r.fullText && r.fullText.length > (r.textPreview || '').length;
|
|
html += '<div class="emb-result-item">' +
|
|
'<div class="emb-result-header">' +
|
|
'<span class="emb-rank">#' + (i + 1) + '</span>' +
|
|
'<span class="emb-score ' + scoreClass + '">' + r.score.toFixed(4) + '</span>' +
|
|
'<span class="emb-role">' + esc(r.role) + '</span>' +
|
|
'<span class="emb-date">' + esc(r.createdAt ? r.createdAt.substring(0, 16) : '') + '</span>' +
|
|
filterTags +
|
|
(hasFullText ? '<span class="emb-tag" style="cursor:pointer;background:var(--bg-secondary)" onclick="this.closest(\'.emb-result-item\').querySelector(\'.emb-fulltext\').style.display=this.closest(\'.emb-result-item\').querySelector(\'.emb-fulltext\').style.display===\'none\'?\'block\':\'none\'">Full</span>' : '') +
|
|
'</div>' +
|
|
'<div class="emb-result-text">' + esc(r.textPreview || '(kein Preview)') + '</div>' +
|
|
(hasFullText ? '<div class="emb-fulltext" style="display:none;margin-top:0.3rem;padding:0.5rem;background:var(--bg-secondary);border-radius:4px;font-size:0.8rem;white-space:pre-wrap;max-height:300px;overflow-y:auto">' + esc(r.fullText) + '</div>' : '') +
|
|
'<div class="emb-result-meta">MsgID: ' + r.messageId + ' | Session: ' + esc(r.sessionId || '').substring(0, 8) + ' | Chain: ' + esc(r.chainId || '').substring(0, 8) + '</div>' +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (memory && memory.length > 0) {
|
|
html += '<h3 style="margin:1rem 0 0.5rem">Memory-Ergebnisse</h3>';
|
|
html += '<div class="emb-results">';
|
|
memory.forEach(function(r, i) {
|
|
var scoreClass = r.score >= 0.35 ? 'score-good' : (r.score >= 0.2 ? 'score-medium' : 'score-low');
|
|
var hasFullText = r.fullText && r.fullText.length > (r.textPreview || '').length;
|
|
html += '<div class="emb-result-item">' +
|
|
'<div class="emb-result-header">' +
|
|
'<span class="emb-rank">#' + (i + 1) + '</span>' +
|
|
'<span class="emb-score ' + scoreClass + '">' + r.score.toFixed(4) + '</span>' +
|
|
'<span class="emb-role">memory</span>' +
|
|
'<span class="emb-date">' + esc(r.filePath || '') + '</span>' +
|
|
(r.score >= 0.35 ? '<span class="emb-tag pass">AutoContext</span>' : '') +
|
|
(hasFullText ? '<span class="emb-tag" style="cursor:pointer;background:var(--bg-secondary)" onclick="this.closest(\'.emb-result-item\').querySelector(\'.emb-fulltext\').style.display=this.closest(\'.emb-result-item\').querySelector(\'.emb-fulltext\').style.display===\'none\'?\'block\':\'none\'">Full</span>' : '') +
|
|
'</div>' +
|
|
'<div class="emb-result-text">' + esc(r.textPreview || '(kein Preview)') + '</div>' +
|
|
(hasFullText ? '<div class="emb-fulltext" style="display:none;margin-top:0.3rem;padding:0.5rem;background:var(--bg-secondary);border-radius:4px;font-size:0.8rem;white-space:pre-wrap;max-height:300px;overflow-y:auto">' + esc(r.fullText) + '</div>' : '') +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (!html) {
|
|
html = '<p style="color:var(--text-muted)">Keine Ergebnisse gefunden (auch nicht unter Threshold).</p>';
|
|
}
|
|
|
|
resultsEl.innerHTML = html;
|
|
}
|
|
|
|
function renderStats(data) {
|
|
var html = '<h3 style="margin-bottom:0.5rem">Embedding-Statistiken</h3>';
|
|
html += '<div class="emb-summary-grid">' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + (data.totalMessages || 0).toLocaleString() + '</span><span class="emb-stat-label">Message-Embeddings</span></div>' +
|
|
'<div class="emb-stat"><span class="emb-stat-num">' + (data.totalMemory || 0).toLocaleString() + '</span><span class="emb-stat-label">Memory-Chunks</span></div>' +
|
|
'</div>';
|
|
|
|
if (data.perAgent && data.perAgent.length > 0) {
|
|
html += '<h3 style="margin:1rem 0 0.5rem">Pro Agent</h3>';
|
|
html += '<table class="db-table"><thead><tr><th>Agent</th><th>Messages</th><th>Memory</th></tr></thead><tbody>';
|
|
data.perAgent.forEach(function(a) {
|
|
html += '<tr><td>' + esc(a.agentName || a.agentId || '(kein Agent)') + '</td>' +
|
|
'<td class="db-table-num">' + (a.messageCount || 0).toLocaleString() + '</td>' +
|
|
'<td class="db-table-num">' + (a.memoryChunks || 0).toLocaleString() + '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
}
|
|
|
|
resultsEl.innerHTML = html;
|
|
}
|
|
}
|
|
});
|