kiku-agent 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -407
- package/package.json +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +532 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/routes/agent.js +55 -0
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createSearchResult } from '../core/types.js';
|
|
2
|
+
import { safeFetchWithPinning } from '../security/ssrf.js';
|
|
3
|
+
import { loadCheerio, decodeEntities, normalizeText } from '../extract.js';
|
|
4
|
+
|
|
5
|
+
function cleanHtml(raw) {
|
|
6
|
+
if (!raw || typeof raw !== 'string') return '';
|
|
7
|
+
return normalizeText(decodeEntities(raw.replace(/<[^>]+>/g, ' ')));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function parseResultsWithCheerio(html, maxResults) {
|
|
11
|
+
const cheerioLoad = await loadCheerio();
|
|
12
|
+
if (!cheerioLoad) return [];
|
|
13
|
+
const $ = cheerioLoad(html);
|
|
14
|
+
const results = [];
|
|
15
|
+
$('div.result, div.web-result').each((_, el) => {
|
|
16
|
+
if (results.length >= maxResults) return;
|
|
17
|
+
const $el = $(el);
|
|
18
|
+
const anchor = $el.find('a.result__a').first();
|
|
19
|
+
let link = anchor.attr('href') || '';
|
|
20
|
+
if (link.includes('uddg=')) {
|
|
21
|
+
try {
|
|
22
|
+
const decoded = new URL('https://duckduckgo.com' + link).searchParams.get('uddg');
|
|
23
|
+
if (decoded) link = decoded;
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
const title = normalizeText(decodeEntities(anchor.text()));
|
|
27
|
+
const snippet = normalizeText(decodeEntities($el.find('.result__snippet').first().text()));
|
|
28
|
+
if (link && title) results.push({ link, title, snippet });
|
|
29
|
+
});
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseResultsWithRegex(html, maxResults) {
|
|
34
|
+
const results = [];
|
|
35
|
+
const resultRegex = /<div[^>]*class="[^"]*result[^"]*"[^>]*>[\s\S]*?<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
|
|
38
|
+
results.push({ link: match[1], title: cleanHtml(match[2]), snippet: cleanHtml(match[3]) });
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function searchDuckDuckGo(query, options = {}) {
|
|
44
|
+
const maxResults = Number(options.limit) || 10;
|
|
45
|
+
const timeoutMs = Number(options.timeoutMs) || 8000;
|
|
46
|
+
const results = [];
|
|
47
|
+
const seenLinks = new Set();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const lang = options.language || 'en';
|
|
51
|
+
const kl = lang === 'th' ? 'th-th' : (lang === 'zh' ? 'zh-cn' : 'en-us');
|
|
52
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=${kl}`;
|
|
53
|
+
|
|
54
|
+
const res = await safeFetchWithPinning(searchUrl, {
|
|
55
|
+
headers: {
|
|
56
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
57
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
58
|
+
'Accept-Language': 'th,en-US;q=0.9,en;q=0.8,zh-CN;q=0.7'
|
|
59
|
+
},
|
|
60
|
+
timeoutMs,
|
|
61
|
+
signal: options.signal
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const html = await res.text();
|
|
69
|
+
const cheerioParsed = await parseResultsWithCheerio(html, maxResults);
|
|
70
|
+
const parsed = cheerioParsed.length > 0 ? cheerioParsed : parseResultsWithRegex(html, maxResults);
|
|
71
|
+
|
|
72
|
+
for (const item of parsed) {
|
|
73
|
+
if (results.length >= maxResults) break;
|
|
74
|
+
let rawLink = item.link;
|
|
75
|
+
if (rawLink.includes('uddg=')) {
|
|
76
|
+
try {
|
|
77
|
+
const decoded = new URL('https://duckduckgo.com' + rawLink).searchParams.get('uddg');
|
|
78
|
+
if (decoded) rawLink = decoded;
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (rawLink && item.title && !seenLinks.has(rawLink)) {
|
|
83
|
+
seenLinks.add(rawLink);
|
|
84
|
+
results.push(createSearchResult({
|
|
85
|
+
link: rawLink,
|
|
86
|
+
title: item.title,
|
|
87
|
+
snippet: item.snippet,
|
|
88
|
+
engine: 'duckduckgo',
|
|
89
|
+
score: 1.0 - (results.length * 0.05)
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createSearchResult } from '../core/types.js';
|
|
2
|
+
import { safeFetchWithPinning } from '../security/ssrf.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export async function searchJina(query, options = {}) {
|
|
6
|
+
const maxResults = Number(options.limit) || 10;
|
|
7
|
+
const timeoutMs = Number(options.timeoutMs) || 10000;
|
|
8
|
+
const results = [];
|
|
9
|
+
const seenLinks = new Set();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const searchUrl = `https://s.jina.ai/${encodeURIComponent(query)}`;
|
|
13
|
+
const res = await safeFetchWithPinning(searchUrl, {
|
|
14
|
+
headers: {
|
|
15
|
+
'Accept': 'application/json, text/plain, */*',
|
|
16
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
17
|
+
'X-No-Cache': 'false'
|
|
18
|
+
},
|
|
19
|
+
timeoutMs,
|
|
20
|
+
signal: options.signal
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const contentType = res.headers.get('content-type') || '';
|
|
28
|
+
if (contentType.includes('application/json')) {
|
|
29
|
+
const json = await res.json();
|
|
30
|
+
const items = Array.isArray(json?.data) ? json.data : (Array.isArray(json?.results) ? json.results : []);
|
|
31
|
+
|
|
32
|
+
for (const item of items.slice(0, maxResults)) {
|
|
33
|
+
const link = item?.url || item?.link;
|
|
34
|
+
if (link && !seenLinks.has(link)) {
|
|
35
|
+
seenLinks.add(link);
|
|
36
|
+
results.push(createSearchResult({
|
|
37
|
+
link,
|
|
38
|
+
title: item.title || query,
|
|
39
|
+
snippet: item.description || item.content || '',
|
|
40
|
+
engine: 'jina',
|
|
41
|
+
score: 1.0 - (results.length * 0.05),
|
|
42
|
+
metadata: {
|
|
43
|
+
publishedTime: item.publishedTime || null
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
|
|
52
|
+
const blockRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)([\s\S]*?)(?=\n\[|$)/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = blockRegex.exec(text)) !== null && results.length < maxResults) {
|
|
55
|
+
const title = match[1].trim();
|
|
56
|
+
const link = match[2].trim();
|
|
57
|
+
const snippet = match[3].replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
58
|
+
|
|
59
|
+
if (link && !seenLinks.has(link)) {
|
|
60
|
+
seenLinks.add(link);
|
|
61
|
+
results.push(createSearchResult({
|
|
62
|
+
link,
|
|
63
|
+
title,
|
|
64
|
+
snippet,
|
|
65
|
+
engine: 'jina',
|
|
66
|
+
score: 1.0 - (results.length * 0.05)
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createSearchResult } from '../core/types.js';
|
|
2
|
+
import { safeFetchWithPinning } from '../security/ssrf.js';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export async function searchSearXNG(query, options = {}) {
|
|
6
|
+
const baseUrl = options.baseUrl || process.env.SEARXNG_URL;
|
|
7
|
+
if (!baseUrl) {
|
|
8
|
+
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const maxResults = Number(options.limit) || 10;
|
|
13
|
+
const timeoutMs = Number(options.timeoutMs) || 8000;
|
|
14
|
+
const results = [];
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const cleanBase = baseUrl.replace(/\/+$/, '');
|
|
18
|
+
const searchUrl = `${cleanBase}/search?q=${encodeURIComponent(query)}&format=json&language=${options.language || 'auto'}`;
|
|
19
|
+
|
|
20
|
+
const res = await safeFetchWithPinning(searchUrl, {
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
23
|
+
'Accept': 'application/json, text/plain, */*'
|
|
24
|
+
},
|
|
25
|
+
timeoutMs,
|
|
26
|
+
signal: options.signal
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const json = await res.json();
|
|
34
|
+
const rawResults = Array.isArray(json?.results) ? json.results : [];
|
|
35
|
+
|
|
36
|
+
for (const item of rawResults.slice(0, maxResults)) {
|
|
37
|
+
if (item?.url && item?.title) {
|
|
38
|
+
results.push(createSearchResult({
|
|
39
|
+
link: item.url,
|
|
40
|
+
title: item.title,
|
|
41
|
+
snippet: item.content || item.snippet || '',
|
|
42
|
+
engine: 'searxng',
|
|
43
|
+
score: Number(item.score) || (1.0 - results.length * 0.05),
|
|
44
|
+
metadata: {
|
|
45
|
+
engines: item.engines || [],
|
|
46
|
+
category: item.category || 'general'
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const TRACKING_PARAMS = new Set([
|
|
2
|
+
'utm_source',
|
|
3
|
+
'utm_medium',
|
|
4
|
+
'utm_campaign',
|
|
5
|
+
'utm_term',
|
|
6
|
+
'utm_content',
|
|
7
|
+
'utm_id',
|
|
8
|
+
'fbclid',
|
|
9
|
+
'gclid',
|
|
10
|
+
'dclid',
|
|
11
|
+
'gclsrc',
|
|
12
|
+
'ref',
|
|
13
|
+
'ref_src',
|
|
14
|
+
'spm',
|
|
15
|
+
'spm_id_from',
|
|
16
|
+
'from',
|
|
17
|
+
'source',
|
|
18
|
+
'_ga',
|
|
19
|
+
'_gl',
|
|
20
|
+
'mc_eid',
|
|
21
|
+
'yclid',
|
|
22
|
+
'trk',
|
|
23
|
+
'wt_mc',
|
|
24
|
+
'ocid'
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export function canonicalizeUrl(rawUrl) {
|
|
29
|
+
if (!rawUrl || typeof rawUrl !== 'string') return '';
|
|
30
|
+
const trimmed = rawUrl.trim();
|
|
31
|
+
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(trimmed);
|
|
35
|
+
} catch {
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if (parsed.protocol === 'http:' && !parsed.hostname.includes('localhost') && !parsed.hostname.startsWith('127.')) {
|
|
41
|
+
parsed.protocol = 'https:';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
let hostname = parsed.hostname.toLowerCase();
|
|
46
|
+
if (hostname.startsWith('www.')) {
|
|
47
|
+
hostname = hostname.slice(4);
|
|
48
|
+
}
|
|
49
|
+
parsed.hostname = hostname;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if ((parsed.protocol === 'https:' && parsed.port === '443') || (parsed.protocol === 'http:' && parsed.port === '80')) {
|
|
53
|
+
parsed.port = '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if (parsed.pathname.length > 1 && parsed.pathname.endsWith('/')) {
|
|
58
|
+
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const searchParams = new URLSearchParams(parsed.search);
|
|
63
|
+
const remainingKeys = [];
|
|
64
|
+
for (const [key] of searchParams.entries()) {
|
|
65
|
+
const lowerKey = key.toLowerCase();
|
|
66
|
+
if (TRACKING_PARAMS.has(lowerKey) || lowerKey.startsWith('utm_')) {
|
|
67
|
+
searchParams.delete(key);
|
|
68
|
+
} else {
|
|
69
|
+
if (!remainingKeys.includes(key)) remainingKeys.push(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
remainingKeys.sort();
|
|
74
|
+
const sortedParams = new URLSearchParams();
|
|
75
|
+
for (const key of remainingKeys) {
|
|
76
|
+
const vals = searchParams.getAll(key);
|
|
77
|
+
for (const val of vals) {
|
|
78
|
+
sortedParams.append(key, val);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sortedQueryString = sortedParams.toString();
|
|
83
|
+
parsed.search = sortedQueryString ? `?${sortedQueryString}` : '';
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
parsed.hash = '';
|
|
87
|
+
|
|
88
|
+
return parsed.href;
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { canonicalizeUrl } from './canonical.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_RRF_K = 60;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges and ranks search results across multiple engines using Reciprocal Rank Fusion (RRF).
|
|
7
|
+
* Formula: Score(d) = Sum_{e in E} ( 1 / (k + rank_e(d)) )
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<Array<import('./types.js').SearchResult>>} engineResultLists - Array of result arrays from engines
|
|
10
|
+
* @param {Object} [options]
|
|
11
|
+
* @param {number} [options.k=60] - Smoothing constant
|
|
12
|
+
* @param {number} [options.limit=10] - Max fused results to return
|
|
13
|
+
* @returns {Array<import('./types.js').SearchResult>} Fused, deduplicated and ranked results
|
|
14
|
+
*/
|
|
15
|
+
export function fuseSearchResults(engineResultLists = [], options = {}) {
|
|
16
|
+
const k = Number.isFinite(options.k) ? Math.max(1, options.k) : DEFAULT_RRF_K;
|
|
17
|
+
const limit = Number.isFinite(options.limit) ? Math.max(1, options.limit) : 10;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const aggregated = new Map();
|
|
21
|
+
|
|
22
|
+
for (const list of engineResultLists) {
|
|
23
|
+
if (!Array.isArray(list)) continue;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < list.length; i++) {
|
|
26
|
+
const item = list[i];
|
|
27
|
+
if (!item || !item.link) continue;
|
|
28
|
+
|
|
29
|
+
const canonicalLink = canonicalizeUrl(item.link);
|
|
30
|
+
if (!canonicalLink) continue;
|
|
31
|
+
|
|
32
|
+
const rank = i + 1;
|
|
33
|
+
const rrfContribution = 1 / (k + rank);
|
|
34
|
+
|
|
35
|
+
let existing = aggregated.get(canonicalLink);
|
|
36
|
+
if (!existing) {
|
|
37
|
+
existing = {
|
|
38
|
+
link: canonicalLink,
|
|
39
|
+
title: item.title || '',
|
|
40
|
+
snippet: item.snippet || '',
|
|
41
|
+
engine: item.engine || 'meta',
|
|
42
|
+
score: 0,
|
|
43
|
+
metadata: {
|
|
44
|
+
engines: [item.engine].filter(Boolean),
|
|
45
|
+
ranks: { [item.engine]: rank },
|
|
46
|
+
originalLinks: [item.link],
|
|
47
|
+
snippets: item.snippet ? [item.snippet] : []
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
aggregated.set(canonicalLink, existing);
|
|
51
|
+
} else {
|
|
52
|
+
if (item.engine && !existing.metadata.engines.includes(item.engine)) {
|
|
53
|
+
existing.metadata.engines.push(item.engine);
|
|
54
|
+
}
|
|
55
|
+
if (item.engine) {
|
|
56
|
+
existing.metadata.ranks[item.engine] = rank;
|
|
57
|
+
}
|
|
58
|
+
if (item.link && !existing.metadata.originalLinks.includes(item.link)) {
|
|
59
|
+
existing.metadata.originalLinks.push(item.link);
|
|
60
|
+
}
|
|
61
|
+
if (item.title && (!existing.title || item.title.length > existing.title.length)) {
|
|
62
|
+
existing.title = item.title;
|
|
63
|
+
}
|
|
64
|
+
if (item.snippet && !existing.metadata.snippets.includes(item.snippet)) {
|
|
65
|
+
existing.metadata.snippets.push(item.snippet);
|
|
66
|
+
|
|
67
|
+
if (item.snippet.length > existing.snippet.length) {
|
|
68
|
+
existing.snippet = item.snippet;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
existing.score += rrfContribution;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const results = Array.from(aggregated.values());
|
|
78
|
+
results.sort((a, b) => b.score - a.score);
|
|
79
|
+
|
|
80
|
+
return results.slice(0, limit);
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Search Result Data Contract
|
|
3
|
+
*
|
|
4
|
+
* @typedef {Object} SearchResult
|
|
5
|
+
* @property {string} link - Canonical destination URL
|
|
6
|
+
* @property {string} title - Sanitized result title
|
|
7
|
+
* @property {string} snippet - Extracted text snippet or abstract
|
|
8
|
+
* @property {'duckduckgo' | 'searxng' | 'jina' | 'openserp' | 'yacy' | string} engine - Origin search engine
|
|
9
|
+
* @property {number} [score] - Optional relevance / RRF score
|
|
10
|
+
* @property {Record<string, unknown>} [metadata] - Optional engine-specific metadata
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function createSearchResult({
|
|
14
|
+
link,
|
|
15
|
+
title = '',
|
|
16
|
+
snippet = '',
|
|
17
|
+
engine = 'duckduckgo',
|
|
18
|
+
score = 0,
|
|
19
|
+
metadata = {}
|
|
20
|
+
}) {
|
|
21
|
+
return {
|
|
22
|
+
link: String(link || '').trim(),
|
|
23
|
+
title: String(title || '').trim(),
|
|
24
|
+
snippet: String(snippet || '').trim(),
|
|
25
|
+
engine: String(engine || 'duckduckgo').toLowerCase(),
|
|
26
|
+
score: Number.isFinite(Number(score)) ? Number(score) : 0,
|
|
27
|
+
metadata: metadata && typeof metadata === 'object' ? metadata : {}
|
|
28
|
+
};
|
|
29
|
+
}
|