opencode-rag-plugin 1.15.1 → 1.17.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 +52 -9
- package/dist/api.js +44 -24
- package/dist/chunker/base.d.ts +34 -3
- package/dist/chunker/base.js +70 -21
- package/dist/chunker/factory.d.ts +4 -1
- package/dist/chunker/factory.js +12 -1
- package/dist/chunker/grammar.js +3 -0
- package/dist/chunker/image.js +8 -8
- package/dist/chunker/pdf.js +11 -13
- package/dist/chunker/xml.d.ts +2 -0
- package/dist/chunker/xml.js +2 -0
- package/dist/cli/commands/index-command.js +0 -4
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.d.ts +12 -0
- package/dist/cli/commands/init-helpers.js +87 -2
- package/dist/cli/commands/init.js +20 -2
- package/dist/cli/commands/query.js +1 -0
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/setup.js +113 -0
- package/dist/cli/commands/status.js +64 -2
- package/dist/cli/commands/update.d.ts +4 -6
- package/dist/cli/commands/update.js +44 -58
- package/dist/cli/index.js +2 -1
- package/dist/content/image.js +24 -3
- package/dist/content/reader.d.ts +6 -1
- package/dist/content/reader.js +49 -4
- package/dist/core/config.d.ts +29 -1
- package/dist/core/config.js +105 -8
- package/dist/core/desc-cache.d.ts +31 -0
- package/dist/core/desc-cache.js +124 -0
- package/dist/core/interfaces.d.ts +58 -3
- package/dist/core/manifest.d.ts +24 -1
- package/dist/core/manifest.js +34 -3
- package/dist/core/resolve-api-key.js +4 -2
- package/dist/core/runtime-overrides.js +12 -8
- package/dist/core/setup-runtime.d.ts +23 -0
- package/dist/core/setup-runtime.js +231 -0
- package/dist/core/version-check.d.ts +85 -0
- package/dist/core/version-check.js +160 -0
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +5 -7
- package/dist/describer/describer.d.ts +2 -2
- package/dist/describer/describer.js +6 -8
- package/dist/describer/gemini.d.ts +2 -2
- package/dist/describer/gemini.js +5 -7
- package/dist/embedder/cohere.d.ts +1 -1
- package/dist/embedder/cohere.js +2 -2
- package/dist/embedder/factory.d.ts +3 -1
- package/dist/embedder/factory.js +8 -2
- package/dist/embedder/health.js +5 -1
- package/dist/embedder/http.d.ts +1 -1
- package/dist/embedder/http.js +74 -43
- package/dist/embedder/ollama.d.ts +1 -1
- package/dist/embedder/ollama.js +2 -2
- package/dist/embedder/openai.d.ts +1 -1
- package/dist/embedder/openai.js +2 -2
- package/dist/eval/compare-merge.d.ts +10 -0
- package/dist/eval/compare-merge.js +537 -0
- package/dist/eval/compare-rankings.d.ts +10 -0
- package/dist/eval/compare-rankings.js +245 -0
- package/dist/eval/dump-descriptions.d.ts +7 -0
- package/dist/eval/dump-descriptions.js +58 -0
- package/dist/eval/fast-index.d.ts +8 -0
- package/dist/eval/fast-index.js +283 -0
- package/dist/eval/run-branch-compare.d.ts +7 -0
- package/dist/eval/run-branch-compare.js +220 -0
- package/dist/eval/run-token-test.js +1 -0
- package/dist/eval/test-kw.d.ts +1 -0
- package/dist/eval/test-kw.js +22 -0
- package/dist/eval/update-descriptions.d.ts +7 -0
- package/dist/eval/update-descriptions.js +84 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/indexer/embed-stage.js +2 -1
- package/dist/indexer/git-diff.js +21 -9
- package/dist/indexer/pipeline.d.ts +6 -0
- package/dist/indexer/pipeline.js +304 -40
- package/dist/indexer/watch.js +1 -3
- package/dist/indexer/worker.d.ts +15 -2
- package/dist/indexer/worker.js +25 -12
- package/dist/mcp/handlers.d.ts +9 -0
- package/dist/mcp/handlers.js +23 -6
- package/dist/mcp/server.js +2 -0
- package/dist/opencode/create-read-tool.d.ts +2 -0
- package/dist/opencode/create-read-tool.js +8 -2
- package/dist/opencode/read-fallback.d.ts +1 -5
- package/dist/opencode/read-fallback.js +1 -18
- package/dist/opencode/read-format.js +5 -3
- package/dist/opencode/tools.js +5 -7
- package/dist/plugin.js +223 -71
- package/dist/retriever/keyword-index.d.ts +3 -2
- package/dist/retriever/keyword-index.js +25 -1
- package/dist/retriever/retriever.d.ts +4 -1
- package/dist/retriever/retriever.js +34 -56
- package/dist/tui.js +18 -1
- package/dist/vectorstore/lancedb.d.ts +35 -4
- package/dist/vectorstore/lancedb.js +146 -23
- package/dist/vectorstore/memory.d.ts +6 -1
- package/dist/vectorstore/memory.js +58 -0
- package/dist/watcher.d.ts +2 -0
- package/dist/watcher.js +5 -1
- package/dist/web/api.js +10 -2
- package/dist/web/server.js +18 -3
- package/package.json +8 -9
- package/scripts/postinstall-setup.js +82 -0
- package/dist/api.js.map +0 -1
- package/dist/chunker/base.js.map +0 -1
- package/dist/chunker/bash.js.map +0 -1
- package/dist/chunker/c.js.map +0 -1
- package/dist/chunker/cpp.js.map +0 -1
- package/dist/chunker/csharp.js.map +0 -1
- package/dist/chunker/css.js.map +0 -1
- package/dist/chunker/doc.js.map +0 -1
- package/dist/chunker/dockerfile.js.map +0 -1
- package/dist/chunker/docx.js.map +0 -1
- package/dist/chunker/excel.js.map +0 -1
- package/dist/chunker/factory.js.map +0 -1
- package/dist/chunker/fallback.js.map +0 -1
- package/dist/chunker/go.js.map +0 -1
- package/dist/chunker/grammar.js.map +0 -1
- package/dist/chunker/html.js.map +0 -1
- package/dist/chunker/image.js.map +0 -1
- package/dist/chunker/ini.js.map +0 -1
- package/dist/chunker/java.js.map +0 -1
- package/dist/chunker/javascript.js.map +0 -1
- package/dist/chunker/json.js.map +0 -1
- package/dist/chunker/kotlin.js.map +0 -1
- package/dist/chunker/loader.js.map +0 -1
- package/dist/chunker/markdown.js.map +0 -1
- package/dist/chunker/pdf.js.map +0 -1
- package/dist/chunker/php.js.map +0 -1
- package/dist/chunker/powershell.js.map +0 -1
- package/dist/chunker/python.js.map +0 -1
- package/dist/chunker/razor.js.map +0 -1
- package/dist/chunker/ruby.js.map +0 -1
- package/dist/chunker/rust.js.map +0 -1
- package/dist/chunker/sln.js.map +0 -1
- package/dist/chunker/sql.js.map +0 -1
- package/dist/chunker/ssl.js.map +0 -1
- package/dist/chunker/swift.js.map +0 -1
- package/dist/chunker/tex.js.map +0 -1
- package/dist/chunker/toml.js.map +0 -1
- package/dist/chunker/typescript.js.map +0 -1
- package/dist/chunker/uuid.js.map +0 -1
- package/dist/chunker/xml.js.map +0 -1
- package/dist/chunker/yaml.js.map +0 -1
- package/dist/cli/commands/clear.js.map +0 -1
- package/dist/cli/commands/describe-image.js.map +0 -1
- package/dist/cli/commands/dump.js.map +0 -1
- package/dist/cli/commands/eval.js.map +0 -1
- package/dist/cli/commands/index-command.js.map +0 -1
- package/dist/cli/commands/index.js.map +0 -1
- package/dist/cli/commands/init-helpers.js.map +0 -1
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/list.js.map +0 -1
- package/dist/cli/commands/mcp.js.map +0 -1
- package/dist/cli/commands/query.js.map +0 -1
- package/dist/cli/commands/show.js.map +0 -1
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/commands/ui.js.map +0 -1
- package/dist/cli/commands/update.js.map +0 -1
- package/dist/cli/format.js.map +0 -1
- package/dist/cli/helpers.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/progress.d.ts +0 -42
- package/dist/cli/progress.js +0 -137
- package/dist/cli/progress.js.map +0 -1
- package/dist/cli/types.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/content/doc.js.map +0 -1
- package/dist/content/docx.js.map +0 -1
- package/dist/content/excel.js.map +0 -1
- package/dist/content/image.js.map +0 -1
- package/dist/content/pdf.js.map +0 -1
- package/dist/content/reader.js.map +0 -1
- package/dist/content/types.js.map +0 -1
- package/dist/core/bootstrap.js.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/doc-progress.js.map +0 -1
- package/dist/core/fileLogger.js.map +0 -1
- package/dist/core/interfaces.js.map +0 -1
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/provider-defaults.js.map +0 -1
- package/dist/core/rag-injection-flag.js.map +0 -1
- package/dist/core/resolve-api-key.js.map +0 -1
- package/dist/core/runtime-overrides.js.map +0 -1
- package/dist/describer/anthropic.js.map +0 -1
- package/dist/describer/describer.js.map +0 -1
- package/dist/describer/factory.js.map +0 -1
- package/dist/describer/gemini.js.map +0 -1
- package/dist/describer/shared.js.map +0 -1
- package/dist/embedder/cohere.js.map +0 -1
- package/dist/embedder/factory.js.map +0 -1
- package/dist/embedder/health.js.map +0 -1
- package/dist/embedder/http.js.map +0 -1
- package/dist/embedder/ollama.js.map +0 -1
- package/dist/embedder/openai.js.map +0 -1
- package/dist/eval/index.js.map +0 -1
- package/dist/eval/run-token-test.js.map +0 -1
- package/dist/eval/session-logger.js.map +0 -1
- package/dist/eval/storage.js.map +0 -1
- package/dist/eval/token-analysis.js.map +0 -1
- package/dist/eval/token-counter.js.map +0 -1
- package/dist/eval/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/indexer/description-stage.js.map +0 -1
- package/dist/indexer/embed-stage.js.map +0 -1
- package/dist/indexer/git-diff.js.map +0 -1
- package/dist/indexer/metadata.js.map +0 -1
- package/dist/indexer/pipeline.js.map +0 -1
- package/dist/indexer/stats.js.map +0 -1
- package/dist/indexer/watch.js.map +0 -1
- package/dist/indexer/worker.js.map +0 -1
- package/dist/indexer.js.map +0 -1
- package/dist/mcp/cli.js.map +0 -1
- package/dist/mcp/handlers.js.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/opencode/create-read-tool.js.map +0 -1
- package/dist/opencode/read-fallback.js.map +0 -1
- package/dist/opencode/read-format.js.map +0 -1
- package/dist/opencode/read-query.js.map +0 -1
- package/dist/opencode/tool-args.js.map +0 -1
- package/dist/opencode/tools.js.map +0 -1
- package/dist/plugin-entry.js.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/dist/retriever/context-optimizer.js.map +0 -1
- package/dist/retriever/keyword-index.js.map +0 -1
- package/dist/retriever/retriever.js.map +0 -1
- package/dist/tui.js.map +0 -1
- package/dist/updater.d.ts +0 -45
- package/dist/updater.js +0 -175
- package/dist/updater.js.map +0 -1
- package/dist/vectorstore/factory.js.map +0 -1
- package/dist/vectorstore/lancedb.js.map +0 -1
- package/dist/vectorstore/memory.js.map +0 -1
- package/dist/watcher.js.map +0 -1
- package/dist/web/api.js.map +0 -1
- package/dist/web/server.js.map +0 -1
- package/dist/web/static.js.map +0 -1
package/dist/plugin.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* automatic context injection and documentation mode.
|
|
5
5
|
*/
|
|
6
6
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
7
|
-
import { loadConfig, findConfigFile, DEFAULT_CONFIG, resolveLogConfig } from "./core/config.js";
|
|
7
|
+
import { loadConfig, findConfigFile, DEFAULT_CONFIG, resolveLogConfig, persistProbedDimension } from "./core/config.js";
|
|
8
8
|
import { createEmbedder } from "./embedder/factory.js";
|
|
9
9
|
import { createDescriptionProvider } from "./describer/factory.js";
|
|
10
10
|
import { createVectorStore } from "./vectorstore/factory.js";
|
|
@@ -22,12 +22,15 @@ import { loadDocProgress, markSubdirectoryDocumented } from "./core/doc-progress
|
|
|
22
22
|
import { loadManifest } from "./core/manifest.js";
|
|
23
23
|
import { createSessionLogger } from "./eval/session-logger.js";
|
|
24
24
|
import { countTokens } from "./eval/token-counter.js";
|
|
25
|
-
import { checkForUpdate } from "./
|
|
26
|
-
import {
|
|
25
|
+
import { checkForUpdate, getCurrentVersion } from "./core/version-check.js";
|
|
26
|
+
import { destroyAllPooledConnections } from "./embedder/http.js";
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
27
28
|
import path from "node:path";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { spawn, execSync } from "node:child_process";
|
|
30
31
|
import { tmpdir } from "node:os";
|
|
32
|
+
/** Maximum entries per session map before oldest entries are evicted. */
|
|
33
|
+
const MAX_SESSION_MAP_SIZE = 50;
|
|
31
34
|
/** Cache of loaded RAG configurations keyed by workspace directory. */
|
|
32
35
|
const configCache = new Map();
|
|
33
36
|
/** Active background indexer instances keyed by workspace directory. */
|
|
@@ -36,6 +39,55 @@ const backgroundIndexers = new Map();
|
|
|
36
39
|
const mcpServers = new Map();
|
|
37
40
|
/** Pending update notifications keyed by workspace directory. */
|
|
38
41
|
const pendingUpdateInfo = new Map();
|
|
42
|
+
/** Workspace directories that have already been prompted about an update this session. */
|
|
43
|
+
const notifiedUpdateDirs = new Set();
|
|
44
|
+
/** Guard flag to prevent re-entrant shutdown. */
|
|
45
|
+
let shutdownRegistered = false;
|
|
46
|
+
/** Close all active background indexers and MCP servers, then destroy idle sockets. */
|
|
47
|
+
async function shutdownPluginResources() {
|
|
48
|
+
for (const [dir, indexer] of backgroundIndexers) {
|
|
49
|
+
try {
|
|
50
|
+
await indexer.close();
|
|
51
|
+
}
|
|
52
|
+
catch { /* best-effort */ }
|
|
53
|
+
backgroundIndexers.delete(dir);
|
|
54
|
+
}
|
|
55
|
+
for (const [dir, server] of mcpServers) {
|
|
56
|
+
try {
|
|
57
|
+
await server.close();
|
|
58
|
+
}
|
|
59
|
+
catch { /* best-effort */ }
|
|
60
|
+
mcpServers.delete(dir);
|
|
61
|
+
}
|
|
62
|
+
configCache.clear();
|
|
63
|
+
pendingUpdateInfo.clear();
|
|
64
|
+
notifiedUpdateDirs.clear();
|
|
65
|
+
destroyAllPooledConnections();
|
|
66
|
+
}
|
|
67
|
+
/** Register a process.beforeExit handler so that resources are cleaned up
|
|
68
|
+
* when the OpenCode process exits (rather than only on plugin reload). */
|
|
69
|
+
function registerShutdownHandler() {
|
|
70
|
+
if (shutdownRegistered)
|
|
71
|
+
return;
|
|
72
|
+
shutdownRegistered = true;
|
|
73
|
+
process.once("beforeExit", () => {
|
|
74
|
+
void shutdownPluginResources();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
registerShutdownHandler();
|
|
78
|
+
/** Set a bounded Map entry, evicting the oldest key if the map exceeds maximum size. */
|
|
79
|
+
function boundedSet(map, key, value, maxSize) {
|
|
80
|
+
if (map.has(key)) {
|
|
81
|
+
map.set(key, value);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (map.size >= maxSize) {
|
|
85
|
+
const oldest = map.keys().next().value;
|
|
86
|
+
if (oldest !== undefined)
|
|
87
|
+
map.delete(oldest);
|
|
88
|
+
}
|
|
89
|
+
map.set(key, value);
|
|
90
|
+
}
|
|
39
91
|
/** Name of the semantic search tool as exposed to the LLM. */
|
|
40
92
|
const CONTEXT_TOOL_NAME = "search_semantic";
|
|
41
93
|
/** Marker string injected into context output to identify RAG-sourced content. */
|
|
@@ -230,10 +282,10 @@ function buildRetrievalQuery(hints) {
|
|
|
230
282
|
* Perform a retrieval query against the vector store with the given parameters.
|
|
231
283
|
* Returns an empty array for blank queries.
|
|
232
284
|
*/
|
|
233
|
-
async function retrieveContext(query, embedder, store, topK, retrieveFn = retrieve, minScore = 0, keywordIndex, keywordWeight, queryPrefix, explain = false) {
|
|
285
|
+
async function retrieveContext(query, embedder, store, topK, retrieveFn = retrieve, minScore = 0, keywordIndex, keywordWeight, queryPrefix, explain = false, hybridEnabled) {
|
|
234
286
|
if (query.trim().length === 0)
|
|
235
287
|
return [];
|
|
236
|
-
return retrieveFn(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight, queryPrefix, explain });
|
|
288
|
+
return retrieveFn(query, embedder, store, { topK, minScore, keywordIndex, keywordWeight, hybridEnabled, queryPrefix, explain });
|
|
237
289
|
}
|
|
238
290
|
/**
|
|
239
291
|
* Load results from one or two queries (primary + optional extra), optimize
|
|
@@ -243,9 +295,10 @@ async function retrieveContext(query, embedder, store, topK, retrieveFn = retrie
|
|
|
243
295
|
async function loadRetrievedResults(query, embedder, store, cfg, retrieveFn = retrieve, topK = cfg.retrieval.topK, extraQuery, keywordIndex, queryPrefix, explain = false) {
|
|
244
296
|
const minScore = cfg.retrieval.minScore;
|
|
245
297
|
const kw = cfg.retrieval.hybridSearch?.keywordWeight;
|
|
246
|
-
const
|
|
298
|
+
const hybridEnabled = cfg.retrieval.hybridSearch?.enabled;
|
|
299
|
+
const primaryResults = await retrieveContext(query, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain, hybridEnabled);
|
|
247
300
|
const extraResults = extraQuery
|
|
248
|
-
? await retrieveContext(extraQuery, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain)
|
|
301
|
+
? await retrieveContext(extraQuery, embedder, store, topK, retrieveFn, minScore, keywordIndex, kw, queryPrefix, explain, hybridEnabled)
|
|
249
302
|
: [];
|
|
250
303
|
const optCfg = cfg.retrieval.contextOptimization ?? DEFAULT_CONTEXT_OPTIMIZATION;
|
|
251
304
|
return optimizeContext([...primaryResults, ...extraResults], { topK, config: optCfg })
|
|
@@ -355,12 +408,20 @@ export function createRagHooks(options) {
|
|
|
355
408
|
let cachedOverrides = loadRuntimeOverrides(options.storePath);
|
|
356
409
|
let overridesLastCheck = 0;
|
|
357
410
|
const OVERRIDES_TTL_MS = Number(process.env.OPENCODE_RAG_OVERRIDES_TTL_MS) || 3600000;
|
|
411
|
+
let lastEffectiveCfg = null;
|
|
412
|
+
let lastEffectiveCfgTime = 0;
|
|
358
413
|
function getEffectiveCfg() {
|
|
359
414
|
if (Date.now() - overridesLastCheck > OVERRIDES_TTL_MS) {
|
|
360
415
|
cachedOverrides = loadRuntimeOverrides(options.storePath);
|
|
361
416
|
overridesLastCheck = Date.now();
|
|
417
|
+
lastEffectiveCfg = null;
|
|
362
418
|
}
|
|
363
|
-
|
|
419
|
+
if (lastEffectiveCfg && Date.now() - lastEffectiveCfgTime < 1000) {
|
|
420
|
+
return lastEffectiveCfg;
|
|
421
|
+
}
|
|
422
|
+
lastEffectiveCfg = applyRuntimeOverrides(options.cfg, cachedOverrides);
|
|
423
|
+
lastEffectiveCfgTime = Date.now();
|
|
424
|
+
return lastEffectiveCfg;
|
|
364
425
|
}
|
|
365
426
|
// Session-level caches for lazy retrieval
|
|
366
427
|
const sessionLastMessage = new Map();
|
|
@@ -538,6 +599,7 @@ export function createRagHooks(options) {
|
|
|
538
599
|
sessionLastMessage,
|
|
539
600
|
sessionRetrievalCache,
|
|
540
601
|
keywordIndex,
|
|
602
|
+
maxSessionCacheSize: MAX_SESSION_MAP_SIZE,
|
|
541
603
|
});
|
|
542
604
|
tools["read"] = readTool;
|
|
543
605
|
appendDebugLog(options.logFilePath, {
|
|
@@ -562,8 +624,8 @@ export function createRagHooks(options) {
|
|
|
562
624
|
if (msgInfo?.role === "assistant" &&
|
|
563
625
|
typeof msgInfo.sessionID === "string" &&
|
|
564
626
|
typeof msgInfo.id === "string") {
|
|
565
|
-
sessionAssistantMessageId
|
|
566
|
-
sessionAssistantText
|
|
627
|
+
boundedSet(sessionAssistantMessageId, msgInfo.sessionID, msgInfo.id, MAX_SESSION_MAP_SIZE);
|
|
628
|
+
boundedSet(sessionAssistantText, msgInfo.sessionID, "", MAX_SESSION_MAP_SIZE);
|
|
567
629
|
}
|
|
568
630
|
}
|
|
569
631
|
else if (event.type === "message.part.updated") {
|
|
@@ -577,7 +639,7 @@ export function createRagHooks(options) {
|
|
|
577
639
|
const assistantMsgId = sessionAssistantMessageId.get(part.sessionID);
|
|
578
640
|
if (assistantMsgId && part.messageID === assistantMsgId) {
|
|
579
641
|
const existing = sessionAssistantText.get(part.sessionID) ?? "";
|
|
580
|
-
sessionAssistantText
|
|
642
|
+
boundedSet(sessionAssistantText, part.sessionID, existing + delta, MAX_SESSION_MAP_SIZE);
|
|
581
643
|
}
|
|
582
644
|
}
|
|
583
645
|
}
|
|
@@ -592,48 +654,61 @@ export function createRagHooks(options) {
|
|
|
592
654
|
scope: "experimental.chat.system.transform",
|
|
593
655
|
message: "system guidance injected",
|
|
594
656
|
});
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
657
|
+
const cfg = getEffectiveCfg();
|
|
658
|
+
if (cfg.openCode.injectSystemPrompt !== false) {
|
|
659
|
+
const guidance = [
|
|
660
|
+
"MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
|
|
661
|
+
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
662
|
+
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
663
|
+
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
664
|
+
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
665
|
+
"",
|
|
666
|
+
"Decision tree — ALWAYS follow this order:",
|
|
667
|
+
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
668
|
+
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
669
|
+
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
670
|
+
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
671
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
672
|
+
"",
|
|
673
|
+
"Proactive triggers — you MUST call these tools when:",
|
|
674
|
+
"- User asks about code behavior, architecture, or implementation details",
|
|
675
|
+
"- User asks to edit, refactor, or fix code — call `find_usages` first",
|
|
676
|
+
"- User references files or functions you haven't read yet",
|
|
677
|
+
"- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
|
|
678
|
+
"- User refers to an image, screenshot, diagram, or visual asset",
|
|
679
|
+
"- Before answering ANY code-related question, retrieve context first",
|
|
680
|
+
"- Before reading ANY file, call `get_file_skeleton` to orient first",
|
|
681
|
+
"",
|
|
682
|
+
"Anti-patterns — NEVER do these:",
|
|
683
|
+
"- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
|
|
684
|
+
"- Editing a function without calling `find_usages` first (breaks call sites)",
|
|
685
|
+
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
686
|
+
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
687
|
+
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
688
|
+
];
|
|
689
|
+
output.system.unshift(guidance.join("\n"));
|
|
690
|
+
}
|
|
691
|
+
// Inject a one-time update prompt so the agent asks the user to install.
|
|
692
|
+
// Fires only once per workspace per session to avoid nagging on every turn.
|
|
627
693
|
const updateInfo = pendingUpdateInfo.get(options.worktree);
|
|
628
|
-
if (updateInfo) {
|
|
629
|
-
|
|
630
|
-
|
|
694
|
+
if (updateInfo && !notifiedUpdateDirs.has(options.worktree)) {
|
|
695
|
+
notifiedUpdateDirs.add(options.worktree);
|
|
696
|
+
pendingUpdateInfo.delete(options.worktree);
|
|
697
|
+
output.system.unshift(`A newer OpenCodeRAG version is available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}. ` +
|
|
698
|
+
`At the very start of your next response, briefly tell the user a new version is available and ask whether ` +
|
|
699
|
+
`they would like you to install it. If they agree, run \`opencode-rag update\` via the bash tool, then tell ` +
|
|
700
|
+
`them to restart OpenCode to load the new version. Do not mention this notice again.`);
|
|
631
701
|
}
|
|
632
702
|
// Inject documentation mode system prompt if enabled
|
|
633
703
|
const docMode = getEffectiveCfg().documentationMode;
|
|
634
704
|
if (docMode?.enabled && docMode.systemPrompt) {
|
|
635
705
|
output.system.unshift(docMode.systemPrompt);
|
|
636
706
|
}
|
|
707
|
+
// Inject wiki mode system prompt if enabled
|
|
708
|
+
const wikiMode = getEffectiveCfg().wikiMode;
|
|
709
|
+
if (wikiMode?.enabled && wikiMode.systemPrompt) {
|
|
710
|
+
output.system.unshift(wikiMode.systemPrompt);
|
|
711
|
+
}
|
|
637
712
|
},
|
|
638
713
|
async "chat.message"(input, output) {
|
|
639
714
|
try {
|
|
@@ -650,7 +725,7 @@ export function createRagHooks(options) {
|
|
|
650
725
|
});
|
|
651
726
|
if (text.length === 0)
|
|
652
727
|
return;
|
|
653
|
-
sessionLastMessage
|
|
728
|
+
boundedSet(sessionLastMessage, input.sessionID, text, MAX_SESSION_MAP_SIZE);
|
|
654
729
|
// Handle /doc slash command
|
|
655
730
|
if (text.startsWith("/doc")) {
|
|
656
731
|
const docMode = getEffectiveCfg().documentationMode;
|
|
@@ -735,6 +810,64 @@ export function createRagHooks(options) {
|
|
|
735
810
|
}
|
|
736
811
|
return;
|
|
737
812
|
}
|
|
813
|
+
// Handle /wiki slash command
|
|
814
|
+
if (text.startsWith("/wiki")) {
|
|
815
|
+
const wikiMode = getEffectiveCfg().wikiMode;
|
|
816
|
+
if (!wikiMode?.enabled) {
|
|
817
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
818
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
819
|
+
const first = parts[0];
|
|
820
|
+
if (typeof first.text === "string") {
|
|
821
|
+
parts[0] = { ...first, text: "Wiki mode is not enabled. Set `wikiMode.enabled` to `true` in opencode-rag.json." };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const arg = text.slice(5).trim().toLowerCase();
|
|
827
|
+
const wikiDir = path.join(options.worktree, ".opencode", "wiki");
|
|
828
|
+
const lines = [];
|
|
829
|
+
if (arg === "lint") {
|
|
830
|
+
lines.push("## Wiki Lint", "", "Run a health check on the wiki. Check for:", "- Orphan pages with no inbound [[links]]", "- Stale pages whose sourceRefs files changed since lastReviewed", "- Contradictions between pages", "- Important concepts mentioned but lacking their own page", "", "Append findings to .opencode/wiki/log.md and fix any issues found.");
|
|
831
|
+
}
|
|
832
|
+
else if (arg === "seed") {
|
|
833
|
+
lines.push("## Wiki Seed", "", "Generate the initial wiki from existing sources:", "1. Read README and top-level docs for project overview → create overview page", "2. Scan file structure with get_file_skeleton for major modules → create entity pages", "3. Identify common conventions from code patterns → create concept pages", "4. Create .opencode/wiki/index.md listing all pages", "5. Create .opencode/wiki/log.md with initial entry", "6. Keep pages focused — one concept per page, short prose, cross-referenced with [[links]]");
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
if (existsSync(wikiDir)) {
|
|
837
|
+
const entries = readdirSync(wikiDir);
|
|
838
|
+
const pages = entries.filter((e) => e.endsWith(".md") && e !== "log.md").length;
|
|
839
|
+
const hasIndex = entries.includes("index.md");
|
|
840
|
+
const hasLog = entries.includes("log.md");
|
|
841
|
+
const dirs = entries.filter((e) => !e.includes(".")).length;
|
|
842
|
+
let lastLogEntry = "";
|
|
843
|
+
const logPath = path.join(wikiDir, "log.md");
|
|
844
|
+
if (hasLog && existsSync(logPath)) {
|
|
845
|
+
const logContent = readFileSync(logPath, "utf-8");
|
|
846
|
+
const logLines = logContent.trim().split("\n").filter((l) => l.startsWith("## ["));
|
|
847
|
+
if (logLines.length > 0) {
|
|
848
|
+
lastLogEntry = logLines[logLines.length - 1] ?? "";
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
lines.push("## Wiki Status", "", `Wiki: \`.opencode/wiki/\``, `Pages: ~${pages} (.md files)`, `Subdirectories: \`${dirs}\``, `Index: ${hasIndex ? "yes" : "no"} | Log: ${hasLog ? "yes" : "no"}`);
|
|
852
|
+
if (lastLogEntry) {
|
|
853
|
+
lines.push(`Latest log: \`${lastLogEntry}\``);
|
|
854
|
+
}
|
|
855
|
+
lines.push("", "Commands:", "- `/wiki` — show this status", "- `/wiki lint` — health-check the wiki", "- `/wiki seed` — generate initial wiki pages from the codebase");
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
lines.push("## Wiki Status", "", "No wiki yet at `.opencode/wiki/`.", "Use `/wiki seed` to generate the initial wiki from existing sources.");
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const wikiMsg = lines.join("\n");
|
|
862
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
863
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
864
|
+
const first = parts[0];
|
|
865
|
+
if (typeof first.text === "string") {
|
|
866
|
+
parts[0] = { ...first, text: wikiMsg };
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
738
871
|
// Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
|
|
739
872
|
const pendingInjection = consumePendingRagInjection(options.storePath);
|
|
740
873
|
if (pendingInjection) {
|
|
@@ -1025,29 +1158,52 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1025
1158
|
}
|
|
1026
1159
|
mcpServers.delete(input.directory);
|
|
1027
1160
|
}
|
|
1161
|
+
// Clean up stale config cache and pending update info for this directory
|
|
1162
|
+
configCache.delete(input.directory);
|
|
1163
|
+
pendingUpdateInfo.delete(input.directory);
|
|
1164
|
+
notifiedUpdateDirs.delete(input.directory);
|
|
1165
|
+
// Clean up idle HTTP sockets from previous provider connections
|
|
1166
|
+
destroyAllPooledConnections();
|
|
1028
1167
|
appendDebugLog(logFilePath, {
|
|
1029
1168
|
scope: "plugin",
|
|
1030
1169
|
message: `OpenCode plugin enabled for ${input.directory}`,
|
|
1031
1170
|
}, logLevel);
|
|
1032
|
-
//
|
|
1171
|
+
// Use cached dimension from config if available (avoids blocking startup with an API call)
|
|
1172
|
+
// If not set, probe the embedding provider once and persist the result.
|
|
1033
1173
|
const embedder = createEmbedder(effectiveCfg);
|
|
1034
|
-
let vectorDimension =
|
|
1035
|
-
|
|
1036
|
-
const probe = await embedder.embed(["dimension-probe"], "query");
|
|
1037
|
-
if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
|
|
1038
|
-
vectorDimension = probe[0].length;
|
|
1039
|
-
}
|
|
1174
|
+
let vectorDimension = effectiveCfg.embedding.vectorDimension;
|
|
1175
|
+
if (vectorDimension && vectorDimension > 0) {
|
|
1040
1176
|
appendDebugLog(logFilePath, {
|
|
1041
1177
|
scope: "plugin",
|
|
1042
|
-
message: `Vector dimension: ${vectorDimension}`,
|
|
1178
|
+
message: `Vector dimension: ${vectorDimension} (cached in config)`,
|
|
1043
1179
|
}, logLevel);
|
|
1044
1180
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1181
|
+
else {
|
|
1182
|
+
vectorDimension = 384;
|
|
1183
|
+
try {
|
|
1184
|
+
const probe = await embedder.embed(["dimension-probe"], "query");
|
|
1185
|
+
if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
|
|
1186
|
+
vectorDimension = probe[0].length;
|
|
1187
|
+
const configPath = findConfigFile(input.directory);
|
|
1188
|
+
if (configPath) {
|
|
1189
|
+
try {
|
|
1190
|
+
persistProbedDimension(configPath, vectorDimension);
|
|
1191
|
+
}
|
|
1192
|
+
catch { /* best-effort */ }
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
appendDebugLog(logFilePath, {
|
|
1196
|
+
scope: "plugin",
|
|
1197
|
+
message: `Vector dimension: ${vectorDimension}`,
|
|
1198
|
+
}, logLevel);
|
|
1199
|
+
}
|
|
1200
|
+
catch (err) {
|
|
1201
|
+
appendDebugLog(logFilePath, {
|
|
1202
|
+
scope: "plugin",
|
|
1203
|
+
message: `Dimension probe failed, falling back to ${vectorDimension}`,
|
|
1204
|
+
error: err,
|
|
1205
|
+
}, logLevel);
|
|
1206
|
+
}
|
|
1051
1207
|
}
|
|
1052
1208
|
const store = createVectorStore(effectiveCfg, storePath, vectorDimension);
|
|
1053
1209
|
// Load or create keyword index for hybrid search
|
|
@@ -1081,6 +1237,7 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1081
1237
|
logLevel,
|
|
1082
1238
|
keywordIndex,
|
|
1083
1239
|
descriptionProvider,
|
|
1240
|
+
dimension: vectorDimension,
|
|
1084
1241
|
});
|
|
1085
1242
|
backgroundIndexers.set(input.directory, indexer);
|
|
1086
1243
|
}
|
|
@@ -1103,18 +1260,13 @@ export const ragPlugin = async (input, _options) => {
|
|
|
1103
1260
|
const mcpInstance = startMcpServerProcess(input.directory, logFilePath, logLevel);
|
|
1104
1261
|
mcpServers.set(input.directory, mcpInstance);
|
|
1105
1262
|
}
|
|
1106
|
-
// Auto-update check (non-blocking, best-effort)
|
|
1263
|
+
// Auto-update check (non-blocking, best-effort). On by default; can be
|
|
1264
|
+
// disabled via `autoUpdate.enabled: false` in opencode-rag.json. When a newer
|
|
1265
|
+
// release is found it is stored and surfaced as a one-time install prompt via
|
|
1266
|
+
// the system transform hook (see createRagHooks).
|
|
1107
1267
|
const autoUpdateCfg = effectiveCfg.autoUpdate;
|
|
1108
1268
|
if (autoUpdateCfg?.enabled) {
|
|
1109
|
-
const currentVersion = (
|
|
1110
|
-
try {
|
|
1111
|
-
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
1112
|
-
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
1113
|
-
}
|
|
1114
|
-
catch {
|
|
1115
|
-
return "0.0.0";
|
|
1116
|
-
}
|
|
1117
|
-
})();
|
|
1269
|
+
const currentVersion = getCurrentVersion();
|
|
1118
1270
|
checkForUpdate(currentVersion)
|
|
1119
1271
|
.then((info) => {
|
|
1120
1272
|
if (info.updateAvailable) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview In-memory inverted keyword index with stemming, tokenization, and optional serialization.
|
|
3
3
|
*/
|
|
4
|
-
import type { Chunk, SearchResult } from "../core/interfaces.js";
|
|
4
|
+
import type { Chunk, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
5
5
|
/**
|
|
6
6
|
* Tokenize text into normalized tokens including stems, camelCase parts, and snake_case parts.
|
|
7
7
|
*
|
|
@@ -22,7 +22,8 @@ export declare class KeywordIndex {
|
|
|
22
22
|
addChunks(chunks: Chunk[]): void;
|
|
23
23
|
getMatchedTerms(query: string, chunkId: string): string[];
|
|
24
24
|
removeByFilePath(filePath: string): void;
|
|
25
|
-
search(query: string, topK: number): SearchResult[];
|
|
25
|
+
search(query: string, topK: number, filter?: MetadataFilter): SearchResult[];
|
|
26
|
+
close(): void;
|
|
26
27
|
clear(): void;
|
|
27
28
|
count(): number;
|
|
28
29
|
save(storePath?: string): Promise<void>;
|
|
@@ -124,7 +124,7 @@ export class KeywordIndex {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
search(query, topK) {
|
|
127
|
+
search(query, topK, filter) {
|
|
128
128
|
if (this.chunkMap.size === 0)
|
|
129
129
|
return [];
|
|
130
130
|
const queryTokens = tokenize(query);
|
|
@@ -144,6 +144,7 @@ export class KeywordIndex {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
const sorted = [...scores.entries()]
|
|
147
|
+
.filter(([id]) => matchesFilter(this.chunkMap.get(id), filter))
|
|
147
148
|
.sort((a, b) => b[1] - a[1])
|
|
148
149
|
.slice(0, topK);
|
|
149
150
|
return sorted.map(([chunkId, score]) => {
|
|
@@ -151,6 +152,10 @@ export class KeywordIndex {
|
|
|
151
152
|
return { chunk, score };
|
|
152
153
|
});
|
|
153
154
|
}
|
|
155
|
+
close() {
|
|
156
|
+
this.invertedIndex.clear();
|
|
157
|
+
this.chunkMap.clear();
|
|
158
|
+
}
|
|
154
159
|
clear() {
|
|
155
160
|
this.invertedIndex.clear();
|
|
156
161
|
this.chunkMap.clear();
|
|
@@ -232,4 +237,23 @@ export class KeywordIndex {
|
|
|
232
237
|
await writeFile(targetPath, JSON.stringify({ version: INDEX_VERSION, tokens: [], chunkMap: {} }), "utf-8");
|
|
233
238
|
}
|
|
234
239
|
}
|
|
240
|
+
function globMatch(pattern, filePath) {
|
|
241
|
+
const GLOBSTAR = "\x00GS\x00";
|
|
242
|
+
let re = pattern.replace(/\*\*/g, GLOBSTAR);
|
|
243
|
+
re = re.replace(/([.+^${}()|[\]\\])/g, "\\$1");
|
|
244
|
+
re = re.replace(new RegExp(GLOBSTAR, "g"), ".*");
|
|
245
|
+
re = re.replace(/\*/g, "[^/]*");
|
|
246
|
+
re = re.replace(/\?/g, ".");
|
|
247
|
+
return new RegExp("^" + re + "$").test(filePath);
|
|
248
|
+
}
|
|
249
|
+
function matchesFilter(chunk, filter) {
|
|
250
|
+
if (!filter)
|
|
251
|
+
return true;
|
|
252
|
+
if (filter.languages?.length && !filter.languages.includes(chunk.metadata.language))
|
|
253
|
+
return false;
|
|
254
|
+
if (filter.pathPatterns?.length) {
|
|
255
|
+
return filter.pathPatterns.some((p) => globMatch(p, chunk.metadata.filePath));
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
235
259
|
//# sourceMappingURL=keyword-index.js.map
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Performs hybrid vector-keyword retrieval with configurable scoring and explanation.
|
|
3
3
|
*/
|
|
4
|
-
import type { EmbeddingProvider, KeywordIndex, VectorStore, SearchResult } from "../core/interfaces.js";
|
|
4
|
+
import type { EmbeddingProvider, KeywordIndex, VectorStore, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
5
5
|
/** Options controlling the retrieval behavior. */
|
|
6
6
|
export interface RetrieveOptions {
|
|
7
7
|
topK?: number;
|
|
8
8
|
minScore?: number;
|
|
9
9
|
keywordIndex?: KeywordIndex;
|
|
10
10
|
keywordWeight?: number;
|
|
11
|
+
/** Whether hybrid search is enabled. When false, keyword index is ignored. */
|
|
12
|
+
hybridEnabled?: boolean;
|
|
11
13
|
queryPrefix?: string;
|
|
12
14
|
explain?: boolean;
|
|
15
|
+
filter?: MetadataFilter;
|
|
13
16
|
}
|
|
14
17
|
/**
|
|
15
18
|
* Perform hybrid vector-keyword retrieval with configurable scoring.
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* We request extra results up-front, then after hybrid fusion + minScore filtering,
|
|
3
3
|
* we slice back to the requested topK. */
|
|
4
4
|
const FETCH_OVERFETCH_FACTOR = 3;
|
|
5
|
+
const RRF_K = 60;
|
|
6
|
+
/** Multiply raw RRF scores by (K+1) to normalize to ~[0,1]. */
|
|
7
|
+
const RRF_NORMALIZE = RRF_K + 1;
|
|
5
8
|
/**
|
|
6
9
|
* Perform hybrid vector-keyword retrieval with configurable scoring.
|
|
7
10
|
*
|
|
@@ -28,13 +31,13 @@ export async function retrieve(query, embedder, store, options = {}) {
|
|
|
28
31
|
if (typeof embedding[0] !== "number") {
|
|
29
32
|
return [];
|
|
30
33
|
}
|
|
31
|
-
const vectorResults = await store.
|
|
34
|
+
const vectorResults = await store.searchWithFilter(embedding, topK * FETCH_OVERFETCH_FACTOR, options.filter);
|
|
32
35
|
let keywordResults = [];
|
|
33
|
-
if (options.keywordIndex) {
|
|
34
|
-
keywordResults = options.keywordIndex.search(query, topK * FETCH_OVERFETCH_FACTOR);
|
|
36
|
+
if (options.keywordIndex && options.hybridEnabled !== false) {
|
|
37
|
+
keywordResults = options.keywordIndex.search(query, topK * FETCH_OVERFETCH_FACTOR, options.filter);
|
|
35
38
|
}
|
|
36
39
|
if (keywordResults.length === 0) {
|
|
37
|
-
const filtered = vectorResults.filter((r) => r.score >= minScore);
|
|
40
|
+
const filtered = vectorResults.filter((r) => r.score >= minScore).slice(0, topK);
|
|
38
41
|
if (options.explain) {
|
|
39
42
|
const kw = options.keywordWeight ?? 0.4;
|
|
40
43
|
for (const r of filtered) {
|
|
@@ -51,65 +54,40 @@ export async function retrieve(query, embedder, store, options = {}) {
|
|
|
51
54
|
}
|
|
52
55
|
return filtered;
|
|
53
56
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
rawKScore: 0,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
for (const r of keywordResults) {
|
|
66
|
-
const existing = combined.get(r.chunk.id);
|
|
67
|
-
const normalizedK = kwTopScore > 0 ? r.score / kwTopScore : 0;
|
|
68
|
-
if (existing) {
|
|
69
|
-
existing.kScore = normalizedK;
|
|
70
|
-
existing.rawKScore = r.score;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
combined.set(r.chunk.id, {
|
|
74
|
-
chunk: r.chunk,
|
|
75
|
-
vScore: 0,
|
|
76
|
-
kScore: normalizedK,
|
|
77
|
-
rawVScore: 0,
|
|
78
|
-
rawKScore: r.score,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
57
|
+
const vRank = new Map(vectorResults.map((r, i) => [r.chunk.id, i]));
|
|
58
|
+
const kRank = new Map(keywordResults.map((r, i) => [r.chunk.id, i]));
|
|
59
|
+
const chunkById = new Map();
|
|
60
|
+
for (const r of vectorResults)
|
|
61
|
+
chunkById.set(r.chunk.id, r);
|
|
62
|
+
for (const r of keywordResults)
|
|
63
|
+
if (!chunkById.has(r.chunk.id))
|
|
64
|
+
chunkById.set(r.chunk.id, r);
|
|
82
65
|
const kw = options.keywordWeight ?? 0.4;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: entry.kScore * 0.9;
|
|
92
|
-
const result = {
|
|
93
|
-
chunk: entry.chunk,
|
|
94
|
-
score,
|
|
95
|
-
};
|
|
66
|
+
const allIds = new Set([...vRank.keys(), ...kRank.keys()]);
|
|
67
|
+
const combinedResults = [...allIds].map((id) => {
|
|
68
|
+
const vR = vRank.get(id);
|
|
69
|
+
const kR = kRank.get(id);
|
|
70
|
+
const vContrib = vR !== undefined ? ((1 - kw) * RRF_NORMALIZE) / (RRF_K + vR + 1) : 0;
|
|
71
|
+
const kContrib = kR !== undefined ? (kw * RRF_NORMALIZE) / (RRF_K + kR + 1) : 0;
|
|
72
|
+
const score = vContrib + kContrib;
|
|
73
|
+
const result = { chunk: chunkById.get(id).chunk, score };
|
|
96
74
|
if (options.explain) {
|
|
97
|
-
|
|
75
|
+
result.explanation = {
|
|
98
76
|
scoreBreakdown: {
|
|
99
|
-
vectorScore:
|
|
100
|
-
keywordScore:
|
|
101
|
-
rawVectorScore:
|
|
102
|
-
rawKeywordScore:
|
|
77
|
+
vectorScore: vContrib,
|
|
78
|
+
keywordScore: kContrib,
|
|
79
|
+
rawVectorScore: vR !== undefined ? vectorResults[vR].score : 0,
|
|
80
|
+
rawKeywordScore: kR !== undefined ? keywordResults[kR].score : 0,
|
|
103
81
|
keywordWeight: kw,
|
|
82
|
+
vectorRank: vR,
|
|
83
|
+
keywordRank: kR,
|
|
104
84
|
},
|
|
105
85
|
};
|
|
106
|
-
if (options.keywordIndex &&
|
|
107
|
-
const terms = options.keywordIndex.getMatchedTerms(query,
|
|
108
|
-
if (terms.length > 0)
|
|
109
|
-
explanation.matchedTerms = terms;
|
|
110
|
-
}
|
|
86
|
+
if (options.keywordIndex && kR !== undefined) {
|
|
87
|
+
const terms = options.keywordIndex.getMatchedTerms(query, id);
|
|
88
|
+
if (terms.length > 0)
|
|
89
|
+
result.explanation.matchedTerms = terms;
|
|
111
90
|
}
|
|
112
|
-
result.explanation = explanation;
|
|
113
91
|
}
|
|
114
92
|
return result;
|
|
115
93
|
})
|