@lincoln504/pi-research 0.1.13 → 1.0.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/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- package/tsconfig.json +0 -40
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { KnowledgeStore } from './store.ts';
|
|
3
|
+
import { WriterQueue } from './writer-queue.ts';
|
|
4
|
+
import { Chunker } from './chunker.ts';
|
|
5
|
+
import { getConfig, validateConfig, getDbDir, type Config } from '../config.ts';
|
|
6
|
+
import { logger } from '../logger.ts';
|
|
7
|
+
import { getModelChunkConfig } from './model-config.ts';
|
|
8
|
+
import type { MigrationStrategy } from './migration.ts';
|
|
9
|
+
import type {
|
|
10
|
+
IEmbedder,
|
|
11
|
+
IKnowledgeStore
|
|
12
|
+
} from '../core/interfaces/knowledge-interfaces.ts';
|
|
13
|
+
|
|
14
|
+
export { SUPPORTED_MODELS, getModelEmbedderConfig, getModelChunkConfig } from './model-config.ts';
|
|
15
|
+
|
|
16
|
+
/** Migration strategy for model changes (read from config) */
|
|
17
|
+
function getMigrationStrategy(config: Config): MigrationStrategy | undefined {
|
|
18
|
+
const strategy = config.MIGRATION_STRATEGY;
|
|
19
|
+
if (!strategy) return undefined;
|
|
20
|
+
|
|
21
|
+
const validStrategies: MigrationStrategy[] = ['drop', 're-embed', 'backup'];
|
|
22
|
+
if (validStrategies.includes(strategy as MigrationStrategy)) {
|
|
23
|
+
return strategy as MigrationStrategy;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.warn(`[knowledge] Invalid migration strategy '${strategy}'. Valid options: drop, re-embed, backup. Falling back to default (backup).`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result of knowledge store initialization
|
|
32
|
+
*/
|
|
33
|
+
export interface KnowledgeStoreComponents {
|
|
34
|
+
embedder: IEmbedder;
|
|
35
|
+
store: IKnowledgeStore;
|
|
36
|
+
writerQueue: WriterQueue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_INIT_RETRIES = 5;
|
|
40
|
+
const BASE_RETRY_DELAY_MS = 1000;
|
|
41
|
+
const MAX_RETRY_DELAY_MS = 30000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize knowledge store components.
|
|
45
|
+
*/
|
|
46
|
+
export async function createKnowledgeStoreComponents(
|
|
47
|
+
embedderFactory: () => Promise<IEmbedder>,
|
|
48
|
+
reconnectFactory?: () => Promise<IEmbedder>,
|
|
49
|
+
withLock?: <T>(fn: () => Promise<T>) => Promise<T>,
|
|
50
|
+
config: Config = getConfig(),
|
|
51
|
+
workspace: string = process.cwd()
|
|
52
|
+
): Promise<KnowledgeStoreComponents | null> {
|
|
53
|
+
if (config.KNOWLEDGE_STORE_MODE === 'none') {
|
|
54
|
+
logger.debug('[knowledge] Knowledge store is disabled in configuration');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
validateConfig(config);
|
|
58
|
+
|
|
59
|
+
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
60
|
+
let store: KnowledgeStore | undefined;
|
|
61
|
+
try {
|
|
62
|
+
logger.info(`[knowledge] Creating Knowledge Store components (attempt ${attempt}/${MAX_INIT_RETRIES})...`);
|
|
63
|
+
|
|
64
|
+
const embedder = await embedderFactory();
|
|
65
|
+
const migrationStrategy = getMigrationStrategy(config);
|
|
66
|
+
|
|
67
|
+
store = new KnowledgeStore({
|
|
68
|
+
dbDir: getDbDir(config, workspace),
|
|
69
|
+
embedder,
|
|
70
|
+
modelName: config.EMBEDDING_MODEL,
|
|
71
|
+
migrationStrategy: migrationStrategy,
|
|
72
|
+
reconnectFactory,
|
|
73
|
+
withLock,
|
|
74
|
+
workspace,
|
|
75
|
+
knowledgeMode: config.KNOWLEDGE_STORE_MODE,
|
|
76
|
+
ttlDays: config.KNOWLEDGE_STORE_CACHE_TTL_DAYS,
|
|
77
|
+
maxServeAgeDays: config.KNOWLEDGE_STORE_MAX_SERVE_AGE_DAYS,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const chunkCfg = getModelChunkConfig(config.EMBEDDING_MODEL);
|
|
81
|
+
const chunkOverlap = Math.round(chunkCfg.chunkSize * chunkCfg.overlapPct);
|
|
82
|
+
const chunker = new Chunker({ targetSize: chunkCfg.chunkSize, overlap: chunkOverlap });
|
|
83
|
+
const writerQueue = new WriterQueue({ store, chunker });
|
|
84
|
+
|
|
85
|
+
await store.initialize();
|
|
86
|
+
|
|
87
|
+
return { embedder, store, writerQueue };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Close any partially-opened store before retrying/giving up: store.initialize()
|
|
90
|
+
// can throw AFTER lancedb.connect() set its db handle, and discarding the object
|
|
91
|
+
// without close() leaks the LanceDB connection/file handles (up to MAX_INIT_RETRIES
|
|
92
|
+
// times per failing startup).
|
|
93
|
+
if (store) {
|
|
94
|
+
try { await store.close(); } catch { /* best-effort cleanup */ }
|
|
95
|
+
}
|
|
96
|
+
const givingUp = attempt === MAX_INIT_RETRIES;
|
|
97
|
+
if (givingUp) {
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const backoffDelay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1), MAX_RETRY_DELAY_MS);
|
|
101
|
+
const totalDelay = backoffDelay + Math.random() * 500;
|
|
102
|
+
logger.warn(`[knowledge] Attempt ${attempt}/${MAX_INIT_RETRIES} failed. Retrying in ${(totalDelay / 1000).toFixed(1)}s...`, err);
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, totalDelay));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error('Failed to create knowledge store components');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Force delete the entire knowledge store directory.
|
|
112
|
+
* Used for system-level clearing or recovery from corruption.
|
|
113
|
+
*/
|
|
114
|
+
export async function forceDeleteKnowledgeStore(config?: Config, workspace?: string): Promise<void> {
|
|
115
|
+
const dbDir = getDbDir(config, workspace);
|
|
116
|
+
if (fs.existsSync(dbDir)) {
|
|
117
|
+
try {
|
|
118
|
+
fs.rmSync(dbDir, { recursive: true, force: true });
|
|
119
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
120
|
+
logger.info(`[knowledge] Knowledge store at ${dbDir} forcefully deleted and recreated.`);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(`[knowledge] Failed to forcefully delete directory ${dbDir}:`, err);
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy loader for the @lancedb/lancedb native module.
|
|
3
|
+
*
|
|
4
|
+
* The knowledge store is only reached behind a dynamic import() (see
|
|
5
|
+
* infrastructure/service-initialization.ts, which loads the store service via
|
|
6
|
+
* `await import(...)` inside a lazy service factory). That source-level lazy
|
|
7
|
+
* boundary is, however, defeated by the CLI build: esbuild bundles src/cli.ts
|
|
8
|
+
* into a single ESM file with `packages: 'external'` and no code-splitting, and
|
|
9
|
+
* in that mode it HOISTS every *static* `import` — including transitive ones —
|
|
10
|
+
* to the top of the bundle. A top-of-module `import * as lancedb from
|
|
11
|
+
* '@lancedb/lancedb'` therefore becomes an eager top-level import in dist/cli.mjs,
|
|
12
|
+
* evaluated before main() runs. lancedb's entry eagerly loads its native binding,
|
|
13
|
+
* so on any platform where that binding is absent (Intel macOS — no darwin-x64
|
|
14
|
+
* binding is published; musl/Alpine; or an npm optional-dependency install miss,
|
|
15
|
+
* npm/cli#4828) the whole bundle fails to evaluate and even `pi-research --help`
|
|
16
|
+
* / `status` exit 1.
|
|
17
|
+
*
|
|
18
|
+
* Loading lancedb through a runtime import() instead keeps esbuild from hoisting
|
|
19
|
+
* it (a dynamic import stays a deferred call), so the native binding is required
|
|
20
|
+
* only on first actual store access — exactly where
|
|
21
|
+
* KnowledgeStoreService.initialize() already catches a missing native stack
|
|
22
|
+
* (isNativeStackUnavailableError) and degrades the store to DISABLED. Help,
|
|
23
|
+
* version, status, and browser-only research then work with no binding present.
|
|
24
|
+
*
|
|
25
|
+
* Modules that need lancedb *types* should keep `import type * as lancedb from
|
|
26
|
+
* '@lancedb/lancedb'` (type imports are erased and never hoisted); only runtime
|
|
27
|
+
* value access (connect, Index, rerankers, …) must go through getLancedb().
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
let _lancedb: typeof import('@lancedb/lancedb') | undefined;
|
|
31
|
+
|
|
32
|
+
/** Resolve the lancedb module, memoizing after the first load. */
|
|
33
|
+
export async function getLancedb(): Promise<typeof import('@lancedb/lancedb')> {
|
|
34
|
+
return (_lancedb ??= await import('@lancedb/lancedb'));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified migration system for Knowledge Store.
|
|
3
|
+
*
|
|
4
|
+
* For a local research tool, we only need 3 strategies:
|
|
5
|
+
* - 'drop': Recreate table (fast, simple, data loss)
|
|
6
|
+
* - 're-embed': Preserve data by re-embedding with new model
|
|
7
|
+
* - 'backup': Rename old data and start fresh (safe, no data loss)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type MigrationStrategy = 'drop' | 're-embed' | 'backup';
|
|
11
|
+
|
|
12
|
+
export interface MigrationResult {
|
|
13
|
+
strategy: MigrationStrategy;
|
|
14
|
+
success: boolean;
|
|
15
|
+
documentsProcessed: number;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding Model Configuration
|
|
3
|
+
*
|
|
4
|
+
* Pure lookup table for supported embedding models.
|
|
5
|
+
* Extracted to a standalone module so both knowledge/index.ts and
|
|
6
|
+
* infrastructure/embedding/embedding-factory.ts can import it without
|
|
7
|
+
* creating a circular dependency between those two modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface ModelConfig {
|
|
11
|
+
pooling: 'mean' | 'cls' | 'last_token';
|
|
12
|
+
queryPrefix?: string;
|
|
13
|
+
chunkSize: number;
|
|
14
|
+
overlapPct: number;
|
|
15
|
+
maxTokens?: number;
|
|
16
|
+
batchSize?: number;
|
|
17
|
+
charsPerToken?: number;
|
|
18
|
+
documentPrefix?: string;
|
|
19
|
+
useCache?: boolean;
|
|
20
|
+
multilingual: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MODEL_CONFIG: Record<string, ModelConfig> = {
|
|
24
|
+
'Xenova/multilingual-e5-small': {
|
|
25
|
+
pooling: 'mean',
|
|
26
|
+
chunkSize: 1500,
|
|
27
|
+
overlapPct: 0.15,
|
|
28
|
+
queryPrefix: 'query: ',
|
|
29
|
+
documentPrefix: 'passage: ',
|
|
30
|
+
charsPerToken: 3.5,
|
|
31
|
+
multilingual: true,
|
|
32
|
+
},
|
|
33
|
+
'Xenova/multilingual-e5-base': {
|
|
34
|
+
pooling: 'mean',
|
|
35
|
+
chunkSize: 1500,
|
|
36
|
+
overlapPct: 0.15,
|
|
37
|
+
queryPrefix: 'query: ',
|
|
38
|
+
documentPrefix: 'passage: ',
|
|
39
|
+
charsPerToken: 3.5,
|
|
40
|
+
multilingual: true,
|
|
41
|
+
},
|
|
42
|
+
'Xenova/bge-m3': {
|
|
43
|
+
pooling: 'cls',
|
|
44
|
+
chunkSize: 1500,
|
|
45
|
+
overlapPct: 0.15,
|
|
46
|
+
charsPerToken: 3.5,
|
|
47
|
+
multilingual: true,
|
|
48
|
+
},
|
|
49
|
+
'onnx-community/embeddinggemma-300m-ONNX': {
|
|
50
|
+
pooling: 'mean',
|
|
51
|
+
chunkSize: 1600,
|
|
52
|
+
overlapPct: 0.15,
|
|
53
|
+
queryPrefix: 'Instruct: Given a web search query, retrieve relevant passages that answer the query.\nQuery: ',
|
|
54
|
+
charsPerToken: 3.5,
|
|
55
|
+
multilingual: true,
|
|
56
|
+
},
|
|
57
|
+
'onnx-community/Qwen3-Embedding-0.6B-ONNX': {
|
|
58
|
+
pooling: 'last_token',
|
|
59
|
+
queryPrefix: 'Instruct: Given a web search query, retrieve relevant passages.\nQuery: ',
|
|
60
|
+
chunkSize: 1200,
|
|
61
|
+
overlapPct: 0.15,
|
|
62
|
+
maxTokens: 512,
|
|
63
|
+
batchSize: 2,
|
|
64
|
+
charsPerToken: 2.5,
|
|
65
|
+
useCache: false,
|
|
66
|
+
multilingual: true,
|
|
67
|
+
},
|
|
68
|
+
'Xenova/all-MiniLM-L6-v2': {
|
|
69
|
+
pooling: 'mean',
|
|
70
|
+
chunkSize: 800,
|
|
71
|
+
overlapPct: 0.15,
|
|
72
|
+
maxTokens: 256,
|
|
73
|
+
multilingual: false,
|
|
74
|
+
},
|
|
75
|
+
'Xenova/bge-small-en-v1.5': {
|
|
76
|
+
pooling: 'cls',
|
|
77
|
+
chunkSize: 1500,
|
|
78
|
+
overlapPct: 0.15,
|
|
79
|
+
multilingual: false,
|
|
80
|
+
},
|
|
81
|
+
'Xenova/all-mpnet-base-v2': {
|
|
82
|
+
pooling: 'mean',
|
|
83
|
+
chunkSize: 1200,
|
|
84
|
+
overlapPct: 0.15,
|
|
85
|
+
maxTokens: 384,
|
|
86
|
+
multilingual: false,
|
|
87
|
+
},
|
|
88
|
+
'onnx-community/granite-embedding-small-english-r2-ONNX': {
|
|
89
|
+
pooling: 'cls',
|
|
90
|
+
chunkSize: 1800,
|
|
91
|
+
overlapPct: 0.15,
|
|
92
|
+
maxTokens: 512,
|
|
93
|
+
batchSize: 8,
|
|
94
|
+
multilingual: false,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const SUPPORTED_MODELS: ReadonlyArray<{ id: string; multilingual: boolean }> =
|
|
99
|
+
Object.entries(MODEL_CONFIG).map(([id, cfg]) => ({ id, multilingual: cfg.multilingual }));
|
|
100
|
+
|
|
101
|
+
const DEFAULT_CHUNK_SIZE = 1200;
|
|
102
|
+
const DEFAULT_OVERLAP_PCT = 0.15;
|
|
103
|
+
|
|
104
|
+
export function getModelEmbedderConfig(modelId: string): {
|
|
105
|
+
pooling: 'mean' | 'cls' | 'last_token';
|
|
106
|
+
queryPrefix?: string;
|
|
107
|
+
documentPrefix?: string;
|
|
108
|
+
maxTokens?: number;
|
|
109
|
+
batchSize?: number;
|
|
110
|
+
charsPerToken?: number;
|
|
111
|
+
useCache?: boolean;
|
|
112
|
+
} {
|
|
113
|
+
const cfg = MODEL_CONFIG[modelId];
|
|
114
|
+
return cfg
|
|
115
|
+
? {
|
|
116
|
+
pooling: cfg.pooling,
|
|
117
|
+
queryPrefix: cfg.queryPrefix,
|
|
118
|
+
documentPrefix: cfg.documentPrefix,
|
|
119
|
+
maxTokens: cfg.maxTokens,
|
|
120
|
+
batchSize: cfg.batchSize,
|
|
121
|
+
charsPerToken: cfg.charsPerToken,
|
|
122
|
+
useCache: cfg.useCache,
|
|
123
|
+
}
|
|
124
|
+
: { pooling: 'mean' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getModelChunkConfig(modelId: string): { chunkSize: number; overlapPct: number } {
|
|
128
|
+
const cfg = MODEL_CONFIG[modelId];
|
|
129
|
+
return cfg
|
|
130
|
+
? { chunkSize: cfg.chunkSize, overlapPct: cfg.overlapPct }
|
|
131
|
+
: { chunkSize: DEFAULT_CHUNK_SIZE, overlapPct: DEFAULT_OVERLAP_PCT };
|
|
132
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONNX / transformers.js environment setup.
|
|
3
|
+
*
|
|
4
|
+
* `@huggingface/transformers` (which in turn loads the native onnxruntime-node
|
|
5
|
+
* binding) is pulled in LAZILY via ./transformers-loader.ts, never as a static
|
|
6
|
+
* import — otherwise esbuild would hoist it to the top of the CLI bundle and crash
|
|
7
|
+
* startup on any platform lacking the binding (see transformers-loader.ts). Both
|
|
8
|
+
* accessors below are async so the native ML stack loads only when the embedder is
|
|
9
|
+
* actually constructed — i.e. when the knowledge store is enabled and used.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { HFEnv } from '../core/interfaces/knowledge-interfaces.ts';
|
|
13
|
+
import { logger } from '../logger.ts';
|
|
14
|
+
import { getModelCacheDir } from './embedder-utils.ts';
|
|
15
|
+
import { getTransformers } from './transformers-loader.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the HuggingFace env object (loads transformers lazily on first call).
|
|
19
|
+
*/
|
|
20
|
+
export async function getHFEnv() {
|
|
21
|
+
return (await getTransformers()).env;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the ONNX environment
|
|
26
|
+
*/
|
|
27
|
+
let onnxInitialized = false;
|
|
28
|
+
export async function initializeONNXEnv(): Promise<void> {
|
|
29
|
+
if (onnxInitialized) return;
|
|
30
|
+
|
|
31
|
+
const hfEnv = (await getTransformers()).env;
|
|
32
|
+
hfEnv.cacheDir = getModelCacheDir();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const envObj = hfEnv as unknown as HFEnv;
|
|
36
|
+
if (envObj.backends?.onnx) {
|
|
37
|
+
envObj.backends.onnx.logLevel = 'error';
|
|
38
|
+
}
|
|
39
|
+
// Fallback for older transformers.js versions if any
|
|
40
|
+
if ((envObj as any).onnx) {
|
|
41
|
+
(envObj as any).onnx.logLevel = 'error';
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
logger.debug('[embedder] Failed to set ONNX logLevel:', e);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onnxInitialized = true;
|
|
48
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Store Operations
|
|
3
|
+
*
|
|
4
|
+
* Core database operations for the KnowledgeStore.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Type-only import: this module references lancedb solely in type positions
|
|
8
|
+
// (lancedb.Table / lancedb.rerankers.*). A type import is erased at build time,
|
|
9
|
+
// so esbuild never hoists a native-loading `import` into the CLI bundle. See
|
|
10
|
+
// ./lancedb-loader.ts for why that matters.
|
|
11
|
+
import type * as lancedb from '@lancedb/lancedb';
|
|
12
|
+
import { logger } from '../logger.ts';
|
|
13
|
+
import { metrics } from '../utils/metrics.ts';
|
|
14
|
+
import { StoreDocument } from '../core/interfaces/knowledge-interfaces.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add documents to the store
|
|
18
|
+
*/
|
|
19
|
+
export async function addDocumentsToStore(
|
|
20
|
+
table: lancedb.Table,
|
|
21
|
+
docs: StoreDocument[],
|
|
22
|
+
embedder: { embedMany(texts: string[]): Promise<(Float32Array | number[])[]>; getDimension?(): number | null },
|
|
23
|
+
isClosing: () => boolean,
|
|
24
|
+
workspace: string,
|
|
25
|
+
isGlobal: boolean
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
if (docs.length === 0) return;
|
|
28
|
+
if (isClosing()) {
|
|
29
|
+
logger.warn('[store] Ignoring addDocuments during close');
|
|
30
|
+
metrics.increment('knowledge_store_add_documents_total', 1, { status: 'ignored_closing' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const vectors = await embedder.embedMany(docs.map(d => d.text));
|
|
38
|
+
|
|
39
|
+
// Guard against a broken embedder silently writing corrupt vectors: a
|
|
40
|
+
// wrong-count, empty, or ragged result would otherwise land in the
|
|
41
|
+
// FixedSizeList column and surface only as an opaque Arrow error or, worse,
|
|
42
|
+
// as silently degraded search. Fail loudly here instead.
|
|
43
|
+
if (vectors.length !== docs.length) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[store] embedder returned ${vectors.length} vectors for ${docs.length} documents`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// Expected width: the embedder's declared dimension, falling back to the
|
|
49
|
+
// first vector's width. Every vector must match it exactly — a uniformly
|
|
50
|
+
// wrong width is just as corrupting as a ragged one.
|
|
51
|
+
const declaredDim = embedder.getDimension?.();
|
|
52
|
+
const dim = declaredDim && declaredDim > 0 ? declaredDim : (vectors[0]?.length ?? 0);
|
|
53
|
+
if (dim === 0) {
|
|
54
|
+
throw new Error('[store] embedder returned empty/zero-width vectors');
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
57
|
+
const v = vectors[i];
|
|
58
|
+
if (!v || v.length !== dim) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[store] embedder returned a wrong-width vector at index ${i} (got ${v?.length ?? 0}, expected ${dim})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
// A correct-width vector can still be poisoned: a WebGPU device-lost
|
|
64
|
+
// mid-inference, or a truncated server response coerced through
|
|
65
|
+
// Float32Array, yields NaN/Inf entries that pass every length check but
|
|
66
|
+
// silently corrupt the ANN index (NaN distances break ordering). Scan
|
|
67
|
+
// here — this is the last gate before the vector is persisted.
|
|
68
|
+
for (let j = 0; j < v.length; j++) {
|
|
69
|
+
if (!Number.isFinite(v[j]!)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`[store] embedder returned a non-finite value (${v[j]}) at vector ${i}, position ${j}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = docs.map((doc, i) => ({
|
|
78
|
+
vector: vectors[i]!,
|
|
79
|
+
url: doc.url,
|
|
80
|
+
text: doc.text,
|
|
81
|
+
content: doc.content ?? null,
|
|
82
|
+
metadata: JSON.stringify(doc.metadata),
|
|
83
|
+
workspace,
|
|
84
|
+
is_global: isGlobal,
|
|
85
|
+
ingestion_type: doc.ingestion_type || (doc.metadata['ingestionType'] as string) || 'unknown',
|
|
86
|
+
timestamp: BigInt(doc.timestamp),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
await table.add(data);
|
|
90
|
+
const duration = Date.now() - startTime;
|
|
91
|
+
metrics.observe('knowledge_store_add_documents_duration_ms', duration);
|
|
92
|
+
metrics.increment('knowledge_store_add_documents_total', 1, { status: 'success' });
|
|
93
|
+
metrics.increment('knowledge_store_chunks_added_total', docs.length);
|
|
94
|
+
logger.log(`[store] Added ${docs.length} chunk(s) for ${docs[0]?.url} [workspace=${workspace}, global=${isGlobal}]`);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
metrics.observe('knowledge_store_add_documents_duration_ms', duration, { status: 'error' });
|
|
98
|
+
metrics.increment('knowledge_store_add_documents_total', 1, { status: 'error' });
|
|
99
|
+
logger.error('[store] Failed to add documents:', err);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Search for documents in the store using hybrid search.
|
|
106
|
+
*
|
|
107
|
+
* Semantic search (vector similarity) targets the 'text' field (synthesis descriptions)
|
|
108
|
+
* since vectors are computed from description chunks. Keyword search (BM25/FTS) targets
|
|
109
|
+
* BOTH 'text' and 'content' fields — 'text' for description keywords and 'content'
|
|
110
|
+
* for full article body text. Results are fused via Reciprocal Rank Fusion (RRF).
|
|
111
|
+
*/
|
|
112
|
+
export async function searchStore(
|
|
113
|
+
table: lancedb.Table,
|
|
114
|
+
embedder: { embed(query: string): Promise<Float32Array | number[]> },
|
|
115
|
+
query: string,
|
|
116
|
+
getReranker: () => Promise<lancedb.rerankers.RRFReranker>,
|
|
117
|
+
limit: number,
|
|
118
|
+
scopeFilter?: string
|
|
119
|
+
): Promise<StoreDocument[]> {
|
|
120
|
+
const startTime = Date.now();
|
|
121
|
+
|
|
122
|
+
const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
|
|
123
|
+
if (rowCount === 0) {
|
|
124
|
+
metrics.increment('knowledge_store_search_total', 1, { status: 'empty' });
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const vector = await embedder.embed(query);
|
|
129
|
+
|
|
130
|
+
let filter = "ingestion_type = 'synthesis-description'";
|
|
131
|
+
if (scopeFilter) {
|
|
132
|
+
filter = `(${filter}) AND (${scopeFilter})`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const results = await table
|
|
136
|
+
.query()
|
|
137
|
+
.nearestTo(vector)
|
|
138
|
+
.where(filter)
|
|
139
|
+
.fullTextSearch(query, { columns: ['text', 'content'] })
|
|
140
|
+
.rerank(await getReranker())
|
|
141
|
+
.limit(limit)
|
|
142
|
+
.toArray();
|
|
143
|
+
|
|
144
|
+
const filteredResults = results
|
|
145
|
+
.map(r => {
|
|
146
|
+
let metadata: Record<string, unknown>;
|
|
147
|
+
try {
|
|
148
|
+
metadata = JSON.parse(r.metadata as string) as Record<string, unknown>;
|
|
149
|
+
} catch {
|
|
150
|
+
logger.debug('[store] Corrupted metadata in searchStore, skipping row');
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
url: r.url as string,
|
|
155
|
+
text: r.text as string,
|
|
156
|
+
content: (r.content as string | null) ?? undefined,
|
|
157
|
+
metadata,
|
|
158
|
+
timestamp: Number(r.timestamp),
|
|
159
|
+
};
|
|
160
|
+
})
|
|
161
|
+
.filter((doc): doc is NonNullable<typeof doc> => doc !== null);
|
|
162
|
+
|
|
163
|
+
const duration = Date.now() - startTime;
|
|
164
|
+
metrics.observe('knowledge_store_search_duration_ms', duration);
|
|
165
|
+
metrics.increment('knowledge_store_search_total', 1, { status: 'success' });
|
|
166
|
+
metrics.increment('knowledge_store_search_results_total', filteredResults.length);
|
|
167
|
+
|
|
168
|
+
return filteredResults;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find documents by URL
|
|
173
|
+
*/
|
|
174
|
+
export async function findDocumentsByUrl(
|
|
175
|
+
table: lancedb.Table,
|
|
176
|
+
url: string,
|
|
177
|
+
scopeFilter?: string
|
|
178
|
+
): Promise<StoreDocument[]> {
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
|
|
181
|
+
// Escape single quotes in URL to prevent SQL injection
|
|
182
|
+
const escapedUrl = url.replace(/'/g, "''");
|
|
183
|
+
|
|
184
|
+
let filter = `url = '${escapedUrl}'`;
|
|
185
|
+
if (scopeFilter) {
|
|
186
|
+
filter = `(${filter}) AND (${scopeFilter})`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const results = await table
|
|
190
|
+
.query()
|
|
191
|
+
.where(filter)
|
|
192
|
+
.limit(1000)
|
|
193
|
+
.toArray();
|
|
194
|
+
|
|
195
|
+
const duration = Date.now() - startTime;
|
|
196
|
+
metrics.observe('knowledge_store_query_duration_ms', duration, { operation: 'find_by_url' });
|
|
197
|
+
metrics.increment('knowledge_store_query_total', 1, { operation: 'find_by_url' });
|
|
198
|
+
|
|
199
|
+
if (results.length === 1000) {
|
|
200
|
+
logger.warn(`[store] findByUrl hit 1000-chunk cap for ${url} - some chunks may be missing`);
|
|
201
|
+
metrics.increment('knowledge_store_query_cap_hits_total', 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return results.map(r => {
|
|
205
|
+
let metadata: Record<string, unknown> = {};
|
|
206
|
+
try {
|
|
207
|
+
metadata = JSON.parse(r.metadata as string);
|
|
208
|
+
} catch {
|
|
209
|
+
logger.debug('[store] Corrupted metadata in findDocumentsByUrl, using empty object');
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
url: r.url as string,
|
|
213
|
+
text: r.text as string,
|
|
214
|
+
content: (r.content as string | null) ?? undefined,
|
|
215
|
+
metadata,
|
|
216
|
+
timestamp: Number(r.timestamp),
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Find URLs relevant to a query using hybrid search.
|
|
223
|
+
*
|
|
224
|
+
* Semantic search (vector) targets the 'text' field (descriptions).
|
|
225
|
+
* Keyword search (BM25/FTS) targets both 'text' and 'content' fields.
|
|
226
|
+
* Results are fused via RRF reranking.
|
|
227
|
+
*/
|
|
228
|
+
export async function findRelevantUrls(
|
|
229
|
+
table: lancedb.Table,
|
|
230
|
+
embedder: { embed(query: string): Promise<Float32Array | number[]> },
|
|
231
|
+
query: string,
|
|
232
|
+
getReranker: () => Promise<lancedb.rerankers.RRFReranker>,
|
|
233
|
+
limit: number,
|
|
234
|
+
scopeFilter?: string
|
|
235
|
+
): Promise<{ url: string; description: string; provenance?: string }[]> {
|
|
236
|
+
const startTime = Date.now();
|
|
237
|
+
const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
|
|
238
|
+
if (rowCount === 0) {
|
|
239
|
+
metrics.increment('knowledge_store_find_urls_total', 1, { status: 'empty' });
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const vector = await embedder.embed(query);
|
|
244
|
+
|
|
245
|
+
let filter = "ingestion_type = 'synthesis-description'";
|
|
246
|
+
if (scopeFilter) {
|
|
247
|
+
filter = `(${filter}) AND (${scopeFilter})`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const results = await table
|
|
251
|
+
.query()
|
|
252
|
+
.nearestTo(vector)
|
|
253
|
+
.where(filter)
|
|
254
|
+
.fullTextSearch(query, { columns: ['text', 'content'] })
|
|
255
|
+
.rerank(await getReranker())
|
|
256
|
+
.limit(limit)
|
|
257
|
+
.toArray();
|
|
258
|
+
|
|
259
|
+
const entries: { url: string; description: string; provenance?: string }[] = [];
|
|
260
|
+
const seen = new Set<string>();
|
|
261
|
+
for (const r of results) {
|
|
262
|
+
const url = r.url as string;
|
|
263
|
+
if (seen.has(url)) continue;
|
|
264
|
+
seen.add(url);
|
|
265
|
+
|
|
266
|
+
let description = '';
|
|
267
|
+
let provenance = 'description-unverified';
|
|
268
|
+
try {
|
|
269
|
+
const meta = JSON.parse(r.metadata as string);
|
|
270
|
+
description = meta.description as string ?? '';
|
|
271
|
+
// Surface provenance metadata so consumers can prefer verified entries
|
|
272
|
+
if (meta.provenance) provenance = meta.provenance;
|
|
273
|
+
else if (meta.hasContent === true) provenance = 'scraped-verified';
|
|
274
|
+
} catch { logger.debug('[store] Corrupted metadata in findRelevantUrls, skipping row'); }
|
|
275
|
+
|
|
276
|
+
if (!description) description = (r.text as string ?? '').substring(0, 300);
|
|
277
|
+
|
|
278
|
+
// Add store scope to provenance
|
|
279
|
+
const scope = r.is_global === true ? 'Global Store' : `Local Project (${r.workspace || 'unknown'})`;
|
|
280
|
+
const finalProvenance = `${provenance} [${scope}]`;
|
|
281
|
+
|
|
282
|
+
entries.push({ url, description, provenance: finalProvenance });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const duration = Date.now() - startTime;
|
|
286
|
+
metrics.observe('knowledge_store_find_urls_duration_ms', duration);
|
|
287
|
+
metrics.increment('knowledge_store_find_urls_total', 1, { status: 'success' });
|
|
288
|
+
metrics.increment('knowledge_store_urls_found_total', entries.length);
|
|
289
|
+
|
|
290
|
+
return entries;
|
|
291
|
+
}
|