kiku-agent 1.3.0 → 1.3.2
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 +7 -7
- package/package.json +4 -7
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +298 -0
- package/server/agent/eval/modules/evaluation/scoring.js +351 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4796 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -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/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- 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/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +127 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +251 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +594 -0
- package/server/agent/tests/cliAutomation.test.js +121 -0
- package/server/agent/tests/cloudProviders.test.js +234 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +417 -0
- package/server/agent/tests/contextIntelligence.test.js +162 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +215 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +312 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +192 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +319 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/modelManager.test.js +109 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +99 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/providerPersistence.test.js +39 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/securityAuditRemediation.test.js +214 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +232 -0
- package/server/agent/tests/sidebarControls.test.js +101 -0
- package/server/agent/tests/statefulMemory.test.js +270 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +126 -0
- package/server/agent/tests/textToolParsing.test.js +139 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +152 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +520 -0
- package/server/agent/tests/webChatInspection.test.js +300 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +261 -0
- package/server/agent/tests/webSources.test.js +247 -0
- 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/apiRouter.js +7 -11
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/tests/connection.test.js +156 -0
- package/server/routes/agent.js +55 -0
- package/server/supabase_schema.sql +63 -0
|
@@ -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
|
+
}
|