Overhaul docs retrieval and web search quality
Replace the abandoned llms-txt-mcp/Chroma docs backend with an in-repo MCP service: SQLite WAL + FTS5 + sentence-transformer embeddings, transactional source replacement, persisted state across restarts, singleflight refresh with conditional requests, hybrid lexical/semantic ranking with exact-duplicate collapse, source/host filters, and explicit-by-default content retrieval. Add docs_rebuild and a docs-rebuild CLI command. Add deterministic llms-full.txt snapshot generation for machine-local menus with hash-validated provenance manifests; lifecycle commands promote a local menu to its snapshot only when the manifest validates. Switch public source profiles to content-bearing llms-full.txt feeds. Improve web search: bounded provider fallback with per-attempt diagnostics and cancellation, an optional Brave Search API provider, strict SearXNG engine selection, capped link/media extraction, and a real engine=browser renderer that routes every request through the existing SSRF vetting while blocking WebSockets, non-GET traffic, and private destinations. Extend release checks with offline unit suites and isolated candidate container tests for both images.
This commit is contained in:
63
docker/web-search/overrides/diagnostics.mjs
Normal file
63
docker/web-search/overrides/diagnostics.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const DEFAULT_TIMEOUT_MS = 15_000;
|
||||
const MAX_ERROR_LENGTH = 240;
|
||||
|
||||
export function classifyProviderError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const lower = message.toLowerCase();
|
||||
let category = "provider_error";
|
||||
if (lower.includes("timed out") || lower.includes("timeout")) category = "timeout";
|
||||
else if (lower.includes("429") || lower.includes("rate limit")) category = "rate_limited";
|
||||
else if (lower.includes("captcha") || lower.includes("challenge")) category = "blocked";
|
||||
else if (lower.includes("403") || lower.includes("401") || lower.includes("denied")) category = "forbidden";
|
||||
else if (lower.includes("network") || lower.includes("fetch") || lower.includes("socket")) category = "network";
|
||||
return { category, message: message.replace(/\s+/g, " ").slice(0, MAX_ERROR_LENGTH) };
|
||||
}
|
||||
|
||||
export async function attemptProvider(provider, query, limit, lang, options = {}) {
|
||||
const timeoutMs = Math.max(10, Math.min(options.timeoutMs || DEFAULT_TIMEOUT_MS, 60_000));
|
||||
const now = options.now || (() => performance.now());
|
||||
const started = now();
|
||||
if (provider.configured === false) {
|
||||
return {
|
||||
items: [],
|
||||
diagnostic: { provider: provider.name, status: "unavailable", duration_ms: 0, result_count: 0 }
|
||||
};
|
||||
}
|
||||
let timer;
|
||||
const controller = new AbortController();
|
||||
try {
|
||||
const items = await Promise.race([
|
||||
provider.search(query, limit, lang, controller.signal),
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
controller.abort(new Error(`provider timed out after ${timeoutMs}ms`));
|
||||
reject(new Error(`provider timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
})
|
||||
]);
|
||||
const bounded = Array.isArray(items) ? items.slice(0, limit) : [];
|
||||
return {
|
||||
items: bounded,
|
||||
diagnostic: {
|
||||
provider: provider.name,
|
||||
status: bounded.length ? "success" : "empty",
|
||||
duration_ms: Math.max(0, Math.round(now() - started)),
|
||||
result_count: bounded.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
items: [],
|
||||
diagnostic: {
|
||||
provider: provider.name,
|
||||
status: "error",
|
||||
duration_ms: Math.max(0, Math.round(now() - started)),
|
||||
result_count: 0,
|
||||
error: classifyProviderError(error)
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
controller.abort();
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user