@juspay/neurolink 9.89.0 → 9.91.0
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/CHANGELOG.md +16 -0
- package/dist/adapters/tts/googleTTSHandler.d.ts +10 -0
- package/dist/adapters/tts/googleTTSHandler.js +27 -18
- package/dist/agent/directTools.js +1 -1
- package/dist/browser/neurolink.min.js +387 -365
- package/dist/core/baseProvider.d.ts +37 -3
- package/dist/core/baseProvider.js +167 -44
- package/dist/core/modules/GenerationHandler.js +7 -1
- package/dist/core/modules/ToolsManager.js +5 -0
- package/dist/core/toolDedup.js +4 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/adapters/tts/googleTTSHandler.d.ts +10 -0
- package/dist/lib/adapters/tts/googleTTSHandler.js +27 -18
- package/dist/lib/agent/directTools.js +1 -1
- package/dist/lib/core/baseProvider.d.ts +37 -3
- package/dist/lib/core/baseProvider.js +167 -44
- package/dist/lib/core/modules/GenerationHandler.js +7 -1
- package/dist/lib/core/modules/ToolsManager.js +5 -0
- package/dist/lib/core/toolDedup.js +4 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/mcp/toolRegistry.js +7 -2
- package/dist/lib/neurolink.d.ts +31 -1
- package/dist/lib/neurolink.js +151 -26
- package/dist/lib/rag/index.d.ts +1 -0
- package/dist/lib/rag/index.js +2 -0
- package/dist/lib/rag/stores/chroma.d.ts +90 -0
- package/dist/lib/rag/stores/chroma.js +281 -0
- package/dist/lib/rag/stores/index.d.ts +21 -0
- package/dist/lib/rag/stores/index.js +22 -0
- package/dist/lib/rag/stores/pgvector.d.ts +95 -0
- package/dist/lib/rag/stores/pgvector.js +400 -0
- package/dist/lib/rag/stores/pinecone.d.ts +85 -0
- package/dist/lib/rag/stores/pinecone.js +159 -0
- package/dist/lib/server/routes/agentRoutes.js +25 -2
- package/dist/lib/tools/toolDiscovery.d.ts +48 -0
- package/dist/lib/tools/toolDiscovery.js +231 -0
- package/dist/lib/tools/toolGate.d.ts +16 -0
- package/dist/lib/tools/toolGate.js +51 -0
- package/dist/lib/tools/toolPolicy.d.ts +40 -0
- package/dist/lib/tools/toolPolicy.js +194 -0
- package/dist/lib/types/config.d.ts +43 -3
- package/dist/lib/types/index.d.ts +3 -0
- package/dist/lib/types/index.js +3 -0
- package/dist/lib/types/providers.d.ts +8 -0
- package/dist/lib/types/rag.d.ts +30 -0
- package/dist/lib/types/toolResolution.d.ts +73 -0
- package/dist/lib/types/toolResolution.js +11 -0
- package/dist/lib/types/vectorStoreChroma.d.ts +67 -0
- package/dist/lib/types/vectorStoreChroma.js +12 -0
- package/dist/lib/types/vectorStorePinecone.d.ts +48 -0
- package/dist/lib/types/vectorStorePinecone.js +12 -0
- package/dist/mcp/toolRegistry.js +7 -2
- package/dist/neurolink.d.ts +31 -1
- package/dist/neurolink.js +151 -26
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +2 -0
- package/dist/rag/stores/chroma.d.ts +90 -0
- package/dist/rag/stores/chroma.js +280 -0
- package/dist/rag/stores/index.d.ts +21 -0
- package/dist/rag/stores/index.js +21 -0
- package/dist/rag/stores/pgvector.d.ts +95 -0
- package/dist/rag/stores/pgvector.js +399 -0
- package/dist/rag/stores/pinecone.d.ts +85 -0
- package/dist/rag/stores/pinecone.js +158 -0
- package/dist/server/routes/agentRoutes.js +25 -2
- package/dist/tools/toolDiscovery.d.ts +48 -0
- package/dist/tools/toolDiscovery.js +230 -0
- package/dist/tools/toolGate.d.ts +16 -0
- package/dist/tools/toolGate.js +50 -0
- package/dist/tools/toolPolicy.d.ts +40 -0
- package/dist/tools/toolPolicy.js +193 -0
- package/dist/types/config.d.ts +43 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/rag.d.ts +30 -0
- package/dist/types/toolResolution.d.ts +73 -0
- package/dist/types/toolResolution.js +10 -0
- package/dist/types/vectorStoreChroma.d.ts +67 -0
- package/dist/types/vectorStoreChroma.js +11 -0
- package/dist/types/vectorStorePinecone.d.ts +48 -0
- package/dist/types/vectorStorePinecone.js +11 -0
- package/package.json +12 -7
package/dist/lib/neurolink.js
CHANGED
|
@@ -83,7 +83,10 @@ import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
|
83
83
|
// Import orchestration components
|
|
84
84
|
import { ModelRouter } from "./utils/modelRouter.js";
|
|
85
85
|
import { getBestProvider } from "./utils/providerUtils.js";
|
|
86
|
-
import { isZodSchema } from "./utils/schemaConversion.js";
|
|
86
|
+
import { isZodSchema, convertZodToJsonSchema, } from "./utils/schemaConversion.js";
|
|
87
|
+
import { resolveToolPolicy } from "./tools/toolPolicy.js";
|
|
88
|
+
import { applyToolGate } from "./tools/toolGate.js";
|
|
89
|
+
import { directAgentTools } from "./agent/directTools.js";
|
|
87
90
|
import { BinaryTaskClassifier } from "./utils/taskClassifier.js";
|
|
88
91
|
// Tool detection and execution imports
|
|
89
92
|
// Transformation utilities
|
|
@@ -284,6 +287,30 @@ export function markStreamProviderEmittedGenerationEnd(options) {
|
|
|
284
287
|
* survives minification AND doesn't rely on method-name stability.
|
|
285
288
|
*/
|
|
286
289
|
export const NEUROLINK_BRAND = Symbol.for("@juspay/neurolink/sdk-brand");
|
|
290
|
+
/**
|
|
291
|
+
* Providers whose native tool-calling support is model-dependent or absent —
|
|
292
|
+
* i.e. every provider that overrides `supportsTools()` and can return false
|
|
293
|
+
* (verified against src/lib/providers: ollama and openrouter are
|
|
294
|
+
* model-dependent; huggingface is deployment-dependent; the rest are
|
|
295
|
+
* image/embedding providers). Only these still receive the full tool listing
|
|
296
|
+
* in the system prompt on the generate path, where no provider instance
|
|
297
|
+
* exists yet to ask directly; every other provider gets tool definitions
|
|
298
|
+
* natively via its `tools` parameter, so repeating them in the prompt was
|
|
299
|
+
* pure token duplication. The stream path asks the provider instance
|
|
300
|
+
* (`provider.supportsTools()`) instead of this list. Keep in sync with
|
|
301
|
+
* `supportsTools()` overrides when adding providers.
|
|
302
|
+
*/
|
|
303
|
+
const PROMPT_ONLY_TOOL_PROVIDERS = new Set([
|
|
304
|
+
"ollama",
|
|
305
|
+
"huggingface",
|
|
306
|
+
"openrouter",
|
|
307
|
+
"ideogram",
|
|
308
|
+
"recraft",
|
|
309
|
+
"replicate",
|
|
310
|
+
"stability",
|
|
311
|
+
"jina",
|
|
312
|
+
"voyage",
|
|
313
|
+
]);
|
|
287
314
|
/**
|
|
288
315
|
* Type-guard for opaque values that should be a {@link NeuroLink} instance.
|
|
289
316
|
*
|
|
@@ -379,6 +406,9 @@ export class NeuroLink {
|
|
|
379
406
|
toolRoutingVectorCache;
|
|
380
407
|
// Opt-in tool-signature deduplication config.
|
|
381
408
|
toolDedupConfig;
|
|
409
|
+
toolsConfig;
|
|
410
|
+
/** Session-scoped pins of tools discovered via search_tools (tools.discovery mode). */
|
|
411
|
+
discoveryPins = new Map();
|
|
382
412
|
// Add orchestration property
|
|
383
413
|
enableOrchestration;
|
|
384
414
|
// Authentication provider for secure access control
|
|
@@ -807,6 +837,11 @@ export class NeuroLink {
|
|
|
807
837
|
if (config?.toolDedup) {
|
|
808
838
|
this.toolDedupConfig = { ...config.toolDedup };
|
|
809
839
|
}
|
|
840
|
+
if (config?.tools) {
|
|
841
|
+
// Shallow-clone so later caller mutations of the config object don't
|
|
842
|
+
// change this instance's tool policy mid-flight.
|
|
843
|
+
this.toolsConfig = { ...config.tools };
|
|
844
|
+
}
|
|
810
845
|
// ModelPool: build one instance from config; null when not configured.
|
|
811
846
|
this.modelPool = config?.modelPool ? new ModelPool(config.modelPool) : null;
|
|
812
847
|
// RequestRouter: store the host-supplied function; null when not configured.
|
|
@@ -1108,10 +1143,15 @@ export class NeuroLink {
|
|
|
1108
1143
|
// Use void to handle async registration without blocking constructor
|
|
1109
1144
|
const registrations = Object.entries(fileTools).map(async ([toolName, toolDef]) => {
|
|
1110
1145
|
const toolId = `direct.${toolName}`;
|
|
1146
|
+
// Register the real parameter schema (converted from Zod) instead of a
|
|
1147
|
+
// `{}` placeholder so tool listings and token-budget accounting see the
|
|
1148
|
+
// schema that actually ships to providers.
|
|
1149
|
+
const fileToolSchema = convertZodToJsonSchema((toolDef.inputSchema ??
|
|
1150
|
+
toolDef.parameters));
|
|
1111
1151
|
const toolInfo = {
|
|
1112
1152
|
name: toolName,
|
|
1113
1153
|
description: toolDef.description || `File tool: ${toolName}`,
|
|
1114
|
-
inputSchema:
|
|
1154
|
+
inputSchema: fileToolSchema,
|
|
1115
1155
|
serverId: "direct",
|
|
1116
1156
|
category: "built-in",
|
|
1117
1157
|
};
|
|
@@ -5143,10 +5183,21 @@ Current user's request: ${currentInput}`;
|
|
|
5143
5183
|
const circuitBreakerNote = unavailableTools.length > 0
|
|
5144
5184
|
? `\n\nNOTE: The following tools are temporarily unavailable due to repeated failures: ${unavailableTools.join(", ")}. Do not attempt to call these tools.`
|
|
5145
5185
|
: "";
|
|
5186
|
+
// Circuit-breaker-open tools are ENFORCED out of the native tool set via
|
|
5187
|
+
// the per-call denylist — asking the model nicely (the note above) is
|
|
5188
|
+
// informational, exclusion is the guarantee. Previously only the
|
|
5189
|
+
// system-prompt listing was filtered; the native array still shipped them.
|
|
5190
|
+
if (unavailableTools.length > 0) {
|
|
5191
|
+
options.excludeTools = [
|
|
5192
|
+
...new Set([...(options.excludeTools ?? []), ...unavailableTools]),
|
|
5193
|
+
];
|
|
5194
|
+
}
|
|
5195
|
+
// Providers with native tool calling receive full definitions via their
|
|
5196
|
+
// `tools` parameter; only prompt-based providers still get the listing.
|
|
5197
|
+
const nativeToolSupport = !PROMPT_ONLY_TOOL_PROVIDERS.has(String(providerName).toLowerCase());
|
|
5146
5198
|
const enhancedSystemPrompt = options.skipToolPromptInjection
|
|
5147
5199
|
? (options.systemPrompt || "") + circuitBreakerNote
|
|
5148
|
-
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools) +
|
|
5149
|
-
circuitBreakerNote;
|
|
5200
|
+
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools, nativeToolSupport) + circuitBreakerNote;
|
|
5150
5201
|
logger.debug("Tool-aware system prompt created", {
|
|
5151
5202
|
requestId,
|
|
5152
5203
|
originalPromptLength: options.systemPrompt?.length || 0,
|
|
@@ -5900,48 +5951,64 @@ Current user's request: ${currentInput}`;
|
|
|
5900
5951
|
/**
|
|
5901
5952
|
* Apply per-call tool filtering (whitelist/blacklist) to a ToolInfo array.
|
|
5902
5953
|
* Used to filter the tool list before building the system prompt.
|
|
5954
|
+
*
|
|
5955
|
+
* Resolves the SAME policy as the native gate (`BaseProvider`
|
|
5956
|
+
* `applyToolFiltering`) — per-call options plus the instance-level `tools`
|
|
5957
|
+
* config — so what the model is told about tools never diverges from the
|
|
5958
|
+
* tools it can actually call.
|
|
5903
5959
|
*/
|
|
5904
5960
|
applyToolInfoFiltering(tools, options) {
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
const denySet = new Set(options.excludeTools);
|
|
5921
|
-
filtered = filtered.filter((t) => !denySet.has(t.name));
|
|
5961
|
+
const policy = resolveToolPolicy({
|
|
5962
|
+
options: {
|
|
5963
|
+
disableTools: options.disableTools,
|
|
5964
|
+
toolFilter: options.toolFilter,
|
|
5965
|
+
enabledToolNames: options.enabledToolNames,
|
|
5966
|
+
excludeTools: options.excludeTools,
|
|
5967
|
+
},
|
|
5968
|
+
instanceConfig: this.toolsConfig,
|
|
5969
|
+
builtinToolNames: Object.keys(directAgentTools),
|
|
5970
|
+
});
|
|
5971
|
+
const record = {};
|
|
5972
|
+
for (const tool of tools) {
|
|
5973
|
+
if (!(tool.name in record)) {
|
|
5974
|
+
record[tool.name] = tool;
|
|
5975
|
+
}
|
|
5922
5976
|
}
|
|
5977
|
+
const gated = applyToolGate(record, policy);
|
|
5978
|
+
const filtered = tools.filter((t) => t.name in gated);
|
|
5923
5979
|
if (filtered.length !== tools.length) {
|
|
5924
5980
|
logger.debug(`Tool info filtering applied for system prompt`, {
|
|
5925
5981
|
beforeCount: tools.length,
|
|
5926
5982
|
afterCount: filtered.length,
|
|
5983
|
+
policySources: policy.sources,
|
|
5927
5984
|
toolFilter: options.toolFilter,
|
|
5928
5985
|
excludeTools: options.excludeTools,
|
|
5929
5986
|
});
|
|
5930
5987
|
}
|
|
5931
5988
|
return filtered;
|
|
5932
5989
|
}
|
|
5933
|
-
createToolAwareSystemPrompt(originalSystemPrompt, availableTools) {
|
|
5990
|
+
createToolAwareSystemPrompt(originalSystemPrompt, availableTools, nativeToolSupport) {
|
|
5934
5991
|
// AI prompt generation with tool analysis and structured logging
|
|
5935
5992
|
const promptGenerationData = {
|
|
5936
5993
|
originalPromptLength: originalSystemPrompt?.length || 0,
|
|
5937
5994
|
availableToolsCount: availableTools.length,
|
|
5938
5995
|
hasOriginalPrompt: !!originalSystemPrompt,
|
|
5996
|
+
nativeToolSupport,
|
|
5939
5997
|
};
|
|
5940
5998
|
logger.debug("AI prompt generation with tool schemas", promptGenerationData);
|
|
5941
5999
|
if (availableTools.length === 0) {
|
|
5942
6000
|
logger.debug("No tools available - returning original prompt");
|
|
5943
6001
|
return originalSystemPrompt || "";
|
|
5944
6002
|
}
|
|
6003
|
+
if (nativeToolSupport) {
|
|
6004
|
+
// The provider receives full tool definitions natively via its `tools`
|
|
6005
|
+
// parameter — repeating name/description/parameters here was pure
|
|
6006
|
+
// duplication (~850 tokens on the default surface, tens of thousands
|
|
6007
|
+
// with MCP servers attached). Keep only a short, static (cache-stable)
|
|
6008
|
+
// damping line so models don't over-reach for tools.
|
|
6009
|
+
return ((originalSystemPrompt || "") +
|
|
6010
|
+
"\n\nTools are available via native tool calling. Use them only when they genuinely improve your response; for creative or conversational requests, respond naturally without tools.");
|
|
6011
|
+
}
|
|
5945
6012
|
const toolDescriptions = transformToolsToDescriptions(availableTools.map((t) => ({
|
|
5946
6013
|
name: t.name,
|
|
5947
6014
|
description: t.description ?? "",
|
|
@@ -7746,10 +7813,13 @@ Current user's request: ${currentInput}`;
|
|
|
7746
7813
|
let availableTools = await this.getAllAvailableTools();
|
|
7747
7814
|
// Apply per-call tool filtering for system prompt tool descriptions
|
|
7748
7815
|
availableTools = this.applyToolInfoFiltering(availableTools, options);
|
|
7749
|
-
// Skip tool prompt injection if skipToolPromptInjection is true
|
|
7816
|
+
// Skip tool prompt injection if skipToolPromptInjection is true.
|
|
7817
|
+
// For providers with native tool calling (instance truth via
|
|
7818
|
+
// supportsTools()), the listing is skipped and only the short damping
|
|
7819
|
+
// line is appended — full definitions ship natively via `tools`.
|
|
7750
7820
|
const enhancedSystemPrompt = options.skipToolPromptInjection
|
|
7751
7821
|
? options.systemPrompt || ""
|
|
7752
|
-
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools);
|
|
7822
|
+
: this.createToolAwareSystemPrompt(options.systemPrompt, availableTools, provider.supportsTools?.() ?? true);
|
|
7753
7823
|
// Get conversation messages for context.
|
|
7754
7824
|
// If the caller already supplied conversationMessages (e.g. proxy routes
|
|
7755
7825
|
// forwarding a multi-turn Claude request), honour them — including an
|
|
@@ -8450,6 +8520,58 @@ Current user's request: ${currentInput}`;
|
|
|
8450
8520
|
getToolDedupConfig() {
|
|
8451
8521
|
return this.toolDedupConfig;
|
|
8452
8522
|
}
|
|
8523
|
+
/**
|
|
8524
|
+
* Returns the instance-level `tools` config (master switch, include/exclude
|
|
8525
|
+
* lists, discovery mode), or `undefined` when not provided at construction.
|
|
8526
|
+
*
|
|
8527
|
+
* Called by `BaseProvider.applyToolFiltering` so the tool gate composes the
|
|
8528
|
+
* instance policy with per-call options on every generate/stream call.
|
|
8529
|
+
*/
|
|
8530
|
+
getToolsConfig() {
|
|
8531
|
+
return this.toolsConfig;
|
|
8532
|
+
}
|
|
8533
|
+
/**
|
|
8534
|
+
* Tools discovered via `search_tools` for a session (`tools.discovery`
|
|
8535
|
+
* mode). Pinned tools are sent in full on every subsequent call of that
|
|
8536
|
+
* session instead of being deferred — discovery cost is paid once.
|
|
8537
|
+
* Reading refreshes the session's recency (LRU), so active conversations
|
|
8538
|
+
* are never the ones evicted at the session cap.
|
|
8539
|
+
*/
|
|
8540
|
+
getDiscoveryPins(sessionKey) {
|
|
8541
|
+
const pins = this.discoveryPins.get(sessionKey);
|
|
8542
|
+
if (!pins) {
|
|
8543
|
+
return new Set();
|
|
8544
|
+
}
|
|
8545
|
+
// Map iteration order is insertion order — re-inserting on access turns
|
|
8546
|
+
// the eviction below into LRU rather than FIFO.
|
|
8547
|
+
this.discoveryPins.delete(sessionKey);
|
|
8548
|
+
this.discoveryPins.set(sessionKey, pins);
|
|
8549
|
+
return pins;
|
|
8550
|
+
}
|
|
8551
|
+
/**
|
|
8552
|
+
* Pin discovered tools to a session (called by the `search_tools`
|
|
8553
|
+
* meta-tool on hydration). Append-only within a session; the map is
|
|
8554
|
+
* bounded by evicting the least-recently-used session past 1000 sessions.
|
|
8555
|
+
*/
|
|
8556
|
+
pinDiscoveredTools(sessionKey, toolNames) {
|
|
8557
|
+
let pins = this.discoveryPins.get(sessionKey);
|
|
8558
|
+
if (!pins) {
|
|
8559
|
+
pins = new Set();
|
|
8560
|
+
}
|
|
8561
|
+
else {
|
|
8562
|
+
this.discoveryPins.delete(sessionKey);
|
|
8563
|
+
}
|
|
8564
|
+
this.discoveryPins.set(sessionKey, pins);
|
|
8565
|
+
for (const name of toolNames) {
|
|
8566
|
+
pins.add(name);
|
|
8567
|
+
}
|
|
8568
|
+
if (this.discoveryPins.size > 1000) {
|
|
8569
|
+
const oldest = this.discoveryPins.keys().next().value;
|
|
8570
|
+
if (oldest !== undefined) {
|
|
8571
|
+
this.discoveryPins.delete(oldest);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8453
8575
|
/**
|
|
8454
8576
|
* Curator P1-1: synchronous credential health check for a single provider.
|
|
8455
8577
|
*
|
|
@@ -9809,8 +9931,11 @@ Current user's request: ${currentInput}`;
|
|
|
9809
9931
|
}
|
|
9810
9932
|
}
|
|
9811
9933
|
}
|
|
9812
|
-
// Return canonical ToolInfo[]; defer presentation transforms to call sites
|
|
9813
|
-
|
|
9934
|
+
// Return canonical ToolInfo[]; defer presentation transforms to call sites.
|
|
9935
|
+
// Name-sorted: external MCP discovery completes in parallel, so Map
|
|
9936
|
+
// insertion order varies across restarts — downstream consumers (tool
|
|
9937
|
+
// listings, prompt construction, budget accounting) need a stable order.
|
|
9938
|
+
const tools = uniqueTools.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
9814
9939
|
// Update the cache
|
|
9815
9940
|
this.toolCache = {
|
|
9816
9941
|
tools,
|
package/dist/lib/rag/index.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export { prepareRAGTool } from "./ragIntegration.js";
|
|
|
56
56
|
export { batchRerank, CohereRelevanceScorer, CrossEncoderReranker, createReranker, getAvailableRerankers, getAvailableRerankerTypes, getRegisteredRerankerMetadata, getReranker, getRerankerDefaultConfig, getRerankerMetadata, RerankerFactory, RerankerRegistry, rerank, rerankerFactory, rerankerRegistry, simpleRerank, } from "./reranker/index.js";
|
|
57
57
|
export * from "./resilience/index.js";
|
|
58
58
|
export { createHybridSearch, createVectorQueryTool, InMemoryBM25Index, InMemoryVectorStore, linearCombination, reciprocalRankFusion, } from "./retrieval/index.js";
|
|
59
|
+
export * from "./stores/index.js";
|
|
59
60
|
export * from "../types/index.js";
|
|
60
61
|
import type { Chunk, ChunkingStrategy, ExtractParams } from "../types/index.js";
|
|
61
62
|
/**
|
package/dist/lib/rag/index.js
CHANGED
|
@@ -76,6 +76,8 @@ RerankerRegistry, rerank, rerankerFactory, rerankerRegistry, simpleRerank, } fro
|
|
|
76
76
|
export * from "./resilience/index.js";
|
|
77
77
|
// Retrieval
|
|
78
78
|
export { createHybridSearch, createVectorQueryTool, InMemoryBM25Index, InMemoryVectorStore, linearCombination, reciprocalRankFusion, } from "./retrieval/index.js";
|
|
79
|
+
// Vector store adapters (Pinecone, pgvector, Chroma — client-injection)
|
|
80
|
+
export * from "./stores/index.js";
|
|
79
81
|
// Types
|
|
80
82
|
export * from "../types/index.js";
|
|
81
83
|
// Convenience functions
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chroma vector store adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the `VectorStore` contract (see `src/lib/types/rag.ts`) against
|
|
5
|
+
* a Chroma collection. This module has zero runtime dependency on the
|
|
6
|
+
* `chromadb` package: callers construct their own `ChromaClient` (or any
|
|
7
|
+
* compatible object) and inject it here. The structural interfaces below
|
|
8
|
+
* mirror the subset of the `chromadb` API this adapter relies on
|
|
9
|
+
* (`getOrCreateCollection`, `collection.upsert/query/delete`).
|
|
10
|
+
*
|
|
11
|
+
* ## Distance -> similarity scoring
|
|
12
|
+
*
|
|
13
|
+
* Chroma returns *distances*, not similarities, and the meaning of that
|
|
14
|
+
* distance depends entirely on the collection's configured `hnsw:space`
|
|
15
|
+
* (this adapter does not — and cannot — set that; it is chosen when the
|
|
16
|
+
* collection is created on the Chroma side). `distanceMetric` tells this
|
|
17
|
+
* adapter which space the collection uses so it can invert the distance
|
|
18
|
+
* into a `score` consistent with `InMemoryVectorStore` (higher = more
|
|
19
|
+
* similar):
|
|
20
|
+
*
|
|
21
|
+
* - `"cosine"` (default): Chroma returns `distance = 1 - cosineSimilarity`,
|
|
22
|
+
* so `score = 1 - distance` reproduces `InMemoryVectorStore`'s cosine
|
|
23
|
+
* score exactly.
|
|
24
|
+
* - `"ip"`: Chroma returns `distance = 1 - dot(a, b)`, so
|
|
25
|
+
* `score = 1 - distance` yields the raw dot product — only equal to
|
|
26
|
+
* cosine similarity if both vectors are unit-normalized.
|
|
27
|
+
* - `"l2"`: Chroma returns squared Euclidean distance, range
|
|
28
|
+
* `[0, Infinity)`, which has no exact inverse into a cosine-similarity
|
|
29
|
+
* scale. It is converted via `score = 1 / (1 + distance)`, a
|
|
30
|
+
* monotonically decreasing-in-distance map into `(0, 1]`. This preserves
|
|
31
|
+
* result ordering but is NOT numerically comparable to cosine scores.
|
|
32
|
+
*
|
|
33
|
+
* Passing a `distanceMetric` that does not match the collection's actual
|
|
34
|
+
* `hnsw:space` will silently produce meaningless scores (Chroma has no way
|
|
35
|
+
* to report its configured space back to the client), so callers must keep
|
|
36
|
+
* the two in sync.
|
|
37
|
+
*
|
|
38
|
+
* ## Metadata round-tripping
|
|
39
|
+
*
|
|
40
|
+
* Chroma metadata values must be strings, numbers, or booleans. Values that
|
|
41
|
+
* are `undefined` are dropped (Chroma rejects `undefined`); any other
|
|
42
|
+
* non-primitive value (object, array, `null`) is `JSON.stringify`-ed on
|
|
43
|
+
* write. This is lossy: such values come back as JSON-encoded strings on
|
|
44
|
+
* read, not their original type. Store only primitive metadata values if
|
|
45
|
+
* you need exact round-tripping.
|
|
46
|
+
*
|
|
47
|
+
* ## indexName -> collection mapping
|
|
48
|
+
*
|
|
49
|
+
* `indexName` is used verbatim as the Chroma collection name (1:1,
|
|
50
|
+
* lazily created via `getOrCreateCollection` and cached for the lifetime of
|
|
51
|
+
* this adapter instance).
|
|
52
|
+
*/
|
|
53
|
+
import type { ChromaClientLike, ChromaVectorStoreOptions, ChromaWhere, MetadataFilter, VectorQueryResult, VectorStore } from "../../types/index.js";
|
|
54
|
+
/**
|
|
55
|
+
* Translates a `MetadataFilter` into a Chroma `where` clause.
|
|
56
|
+
*
|
|
57
|
+
* Throws a clear error for any operator Chroma's metadata `where` clause
|
|
58
|
+
* cannot express ($not, $nor, $exists, $contains, $regex, $size) rather than
|
|
59
|
+
* silently dropping the constraint and mis-filtering results.
|
|
60
|
+
*/
|
|
61
|
+
export declare function translateMetadataFilter(filter: MetadataFilter): ChromaWhere;
|
|
62
|
+
export declare class ChromaVectorStore implements VectorStore {
|
|
63
|
+
private readonly client;
|
|
64
|
+
private readonly collections;
|
|
65
|
+
private readonly distanceMetric;
|
|
66
|
+
constructor(client: ChromaClientLike, options?: ChromaVectorStoreOptions);
|
|
67
|
+
private getCollection;
|
|
68
|
+
/**
|
|
69
|
+
* Add or update vectors in an index (Chroma collection).
|
|
70
|
+
*/
|
|
71
|
+
upsert(indexName: string, items: Array<{
|
|
72
|
+
id: string;
|
|
73
|
+
vector: number[];
|
|
74
|
+
metadata?: Record<string, unknown>;
|
|
75
|
+
}>): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Query vectors by similarity.
|
|
78
|
+
*/
|
|
79
|
+
query(params: {
|
|
80
|
+
indexName: string;
|
|
81
|
+
queryVector: number[];
|
|
82
|
+
topK?: number;
|
|
83
|
+
filter?: MetadataFilter;
|
|
84
|
+
includeVectors?: boolean;
|
|
85
|
+
}): Promise<VectorQueryResult[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Delete vectors from an index (Chroma collection) by id.
|
|
88
|
+
*/
|
|
89
|
+
delete(indexName: string, ids: string[]): Promise<void>;
|
|
90
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chroma vector store adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the `VectorStore` contract (see `src/lib/types/rag.ts`) against
|
|
5
|
+
* a Chroma collection. This module has zero runtime dependency on the
|
|
6
|
+
* `chromadb` package: callers construct their own `ChromaClient` (or any
|
|
7
|
+
* compatible object) and inject it here. The structural interfaces below
|
|
8
|
+
* mirror the subset of the `chromadb` API this adapter relies on
|
|
9
|
+
* (`getOrCreateCollection`, `collection.upsert/query/delete`).
|
|
10
|
+
*
|
|
11
|
+
* ## Distance -> similarity scoring
|
|
12
|
+
*
|
|
13
|
+
* Chroma returns *distances*, not similarities, and the meaning of that
|
|
14
|
+
* distance depends entirely on the collection's configured `hnsw:space`
|
|
15
|
+
* (this adapter does not — and cannot — set that; it is chosen when the
|
|
16
|
+
* collection is created on the Chroma side). `distanceMetric` tells this
|
|
17
|
+
* adapter which space the collection uses so it can invert the distance
|
|
18
|
+
* into a `score` consistent with `InMemoryVectorStore` (higher = more
|
|
19
|
+
* similar):
|
|
20
|
+
*
|
|
21
|
+
* - `"cosine"` (default): Chroma returns `distance = 1 - cosineSimilarity`,
|
|
22
|
+
* so `score = 1 - distance` reproduces `InMemoryVectorStore`'s cosine
|
|
23
|
+
* score exactly.
|
|
24
|
+
* - `"ip"`: Chroma returns `distance = 1 - dot(a, b)`, so
|
|
25
|
+
* `score = 1 - distance` yields the raw dot product — only equal to
|
|
26
|
+
* cosine similarity if both vectors are unit-normalized.
|
|
27
|
+
* - `"l2"`: Chroma returns squared Euclidean distance, range
|
|
28
|
+
* `[0, Infinity)`, which has no exact inverse into a cosine-similarity
|
|
29
|
+
* scale. It is converted via `score = 1 / (1 + distance)`, a
|
|
30
|
+
* monotonically decreasing-in-distance map into `(0, 1]`. This preserves
|
|
31
|
+
* result ordering but is NOT numerically comparable to cosine scores.
|
|
32
|
+
*
|
|
33
|
+
* Passing a `distanceMetric` that does not match the collection's actual
|
|
34
|
+
* `hnsw:space` will silently produce meaningless scores (Chroma has no way
|
|
35
|
+
* to report its configured space back to the client), so callers must keep
|
|
36
|
+
* the two in sync.
|
|
37
|
+
*
|
|
38
|
+
* ## Metadata round-tripping
|
|
39
|
+
*
|
|
40
|
+
* Chroma metadata values must be strings, numbers, or booleans. Values that
|
|
41
|
+
* are `undefined` are dropped (Chroma rejects `undefined`); any other
|
|
42
|
+
* non-primitive value (object, array, `null`) is `JSON.stringify`-ed on
|
|
43
|
+
* write. This is lossy: such values come back as JSON-encoded strings on
|
|
44
|
+
* read, not their original type. Store only primitive metadata values if
|
|
45
|
+
* you need exact round-tripping.
|
|
46
|
+
*
|
|
47
|
+
* ## indexName -> collection mapping
|
|
48
|
+
*
|
|
49
|
+
* `indexName` is used verbatim as the Chroma collection name (1:1,
|
|
50
|
+
* lazily created via `getOrCreateCollection` and cached for the lifetime of
|
|
51
|
+
* this adapter instance).
|
|
52
|
+
*/
|
|
53
|
+
// Structural Chroma client/collection types (`ChromaClientLike`,
|
|
54
|
+
// `ChromaCollectionLike`, `ChromaUpsertParams`, etc.) live in
|
|
55
|
+
// `src/lib/types/vectorStoreChroma.ts` per this repo's canonical-type-
|
|
56
|
+
// location rule (see CLAUDE.md Critical Rules 2 & 12) and are imported from
|
|
57
|
+
// the types barrel above. This file only re-exports runtime values
|
|
58
|
+
// (`ChromaVectorStore`, `translateMetadataFilter`) — consumers who need the
|
|
59
|
+
// structural types should import them from `@juspay/neurolink` (root) or
|
|
60
|
+
// the types barrel directly, per Critical Rule 13.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// MetadataFilter -> Chroma `where` translation
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const CHROMA_SUPPORTED_FIELD_OPS = new Set([
|
|
65
|
+
"$eq",
|
|
66
|
+
"$ne",
|
|
67
|
+
"$gt",
|
|
68
|
+
"$gte",
|
|
69
|
+
"$lt",
|
|
70
|
+
"$lte",
|
|
71
|
+
"$in",
|
|
72
|
+
"$nin",
|
|
73
|
+
]);
|
|
74
|
+
const CHROMA_UNSUPPORTED_FIELD_OP_REASONS = {
|
|
75
|
+
$exists: 'Chroma metadata filtering has no existence check on the "where" clause.',
|
|
76
|
+
$contains: 'Chroma metadata filtering has no substring-match operator on the "where" clause (Chroma\'s substring matching only applies to document text via "where_document", not metadata field values).',
|
|
77
|
+
$regex: 'Chroma metadata filtering has no regular-expression operator on the "where" clause.',
|
|
78
|
+
$size: 'Chroma metadata filtering has no array-length operator on the "where" clause.',
|
|
79
|
+
};
|
|
80
|
+
function unsupportedFieldOpError(opKey, field) {
|
|
81
|
+
const reason = CHROMA_UNSUPPORTED_FIELD_OP_REASONS[opKey] ??
|
|
82
|
+
`Chroma metadata filtering does not support the "${opKey}" operator on the "where" clause.`;
|
|
83
|
+
return new Error(`Unsupported operator "${opKey}" (field "${field}"): ${reason}`);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Translates a field-level filter value (either a direct-equality primitive
|
|
87
|
+
* or an operator bag) into a Chroma `where` clause fragment for that field.
|
|
88
|
+
*
|
|
89
|
+
* Returns `undefined` when the filter value carries no enforceable
|
|
90
|
+
* constraint (an array, or an operator bag with no recognized keys) — this
|
|
91
|
+
* mirrors `InMemoryVectorStore.matchesFilter`, which treats any non-null
|
|
92
|
+
* object value as an "operator bag" and silently applies none of its checks
|
|
93
|
+
* when no recognized `$`-key is present, i.e. the field matches
|
|
94
|
+
* unconditionally. Omitting the field from the Chroma `where` clause
|
|
95
|
+
* reproduces that same "always matches" behavior.
|
|
96
|
+
*/
|
|
97
|
+
function translateFieldFilter(field, value) {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (value === null || typeof value !== "object") {
|
|
102
|
+
return { [field]: { $eq: value } };
|
|
103
|
+
}
|
|
104
|
+
const ops = value;
|
|
105
|
+
const opKeys = Object.keys(ops);
|
|
106
|
+
if (opKeys.length === 0) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const chromaOps = {};
|
|
110
|
+
for (const opKey of opKeys) {
|
|
111
|
+
if (!CHROMA_SUPPORTED_FIELD_OPS.has(opKey)) {
|
|
112
|
+
throw unsupportedFieldOpError(opKey, field);
|
|
113
|
+
}
|
|
114
|
+
chromaOps[opKey] = ops[opKey];
|
|
115
|
+
}
|
|
116
|
+
return { [field]: chromaOps };
|
|
117
|
+
}
|
|
118
|
+
function translateLogicalOperator(key, value) {
|
|
119
|
+
switch (key) {
|
|
120
|
+
case "$and":
|
|
121
|
+
return {
|
|
122
|
+
$and: value.map(translateMetadataFilter),
|
|
123
|
+
};
|
|
124
|
+
case "$or":
|
|
125
|
+
return {
|
|
126
|
+
$or: value.map(translateMetadataFilter),
|
|
127
|
+
};
|
|
128
|
+
case "$not":
|
|
129
|
+
throw new Error('Chroma metadata filtering has no negation operator on the "where" clause; "$not" cannot be translated. Rewrite the filter using $ne/$nin instead.');
|
|
130
|
+
case "$nor":
|
|
131
|
+
throw new Error('"$nor" is declared on MetadataFilter but is not implemented by InMemoryVectorStore either, and Chroma has no negation operator on the "where" clause. Rewrite using $and of $ne/$nin clauses.');
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown logical operator "${key}" in MetadataFilter.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Translates a `MetadataFilter` into a Chroma `where` clause.
|
|
138
|
+
*
|
|
139
|
+
* Throws a clear error for any operator Chroma's metadata `where` clause
|
|
140
|
+
* cannot express ($not, $nor, $exists, $contains, $regex, $size) rather than
|
|
141
|
+
* silently dropping the constraint and mis-filtering results.
|
|
142
|
+
*/
|
|
143
|
+
export function translateMetadataFilter(filter) {
|
|
144
|
+
const clauses = [];
|
|
145
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
146
|
+
const clause = key.startsWith("$")
|
|
147
|
+
? translateLogicalOperator(key, value)
|
|
148
|
+
: translateFieldFilter(key, value);
|
|
149
|
+
if (clause !== undefined) {
|
|
150
|
+
clauses.push(clause);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (clauses.length === 0) {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
if (clauses.length === 1) {
|
|
157
|
+
return clauses[0];
|
|
158
|
+
}
|
|
159
|
+
// Chroma requires a single top-level operator; combine multiple field/
|
|
160
|
+
// logical clauses with an explicit $and (mirrors matchesFilter ANDing
|
|
161
|
+
// every top-level entry).
|
|
162
|
+
return { $and: clauses };
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Metadata <-> Chroma metadata conversion
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
function toChromaMetadataValue(value) {
|
|
168
|
+
if (typeof value === "string" ||
|
|
169
|
+
typeof value === "number" ||
|
|
170
|
+
typeof value === "boolean") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
// Chroma metadata values must be string | number | boolean. Non-primitive
|
|
174
|
+
// values (object, array, null) are JSON-stringified — lossy on read back,
|
|
175
|
+
// see module doc comment.
|
|
176
|
+
return JSON.stringify(value);
|
|
177
|
+
}
|
|
178
|
+
function toChromaMetadata(metadata) {
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
// Chroma rejects `undefined` metadata values outright.
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
result[key] = toChromaMetadataValue(value);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
function distanceToScore(distance, metric) {
|
|
190
|
+
switch (metric) {
|
|
191
|
+
case "cosine":
|
|
192
|
+
case "ip":
|
|
193
|
+
return 1 - distance;
|
|
194
|
+
case "l2":
|
|
195
|
+
return 1 / (1 + distance);
|
|
196
|
+
default: {
|
|
197
|
+
const exhaustive = metric;
|
|
198
|
+
throw new Error(`Unknown Chroma distance metric: ${String(exhaustive)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// ChromaVectorStore
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
export class ChromaVectorStore {
|
|
206
|
+
client;
|
|
207
|
+
collections = new Map();
|
|
208
|
+
distanceMetric;
|
|
209
|
+
constructor(client, options = {}) {
|
|
210
|
+
this.client = client;
|
|
211
|
+
this.distanceMetric = options.distanceMetric ?? "cosine";
|
|
212
|
+
}
|
|
213
|
+
getCollection(indexName) {
|
|
214
|
+
let collection = this.collections.get(indexName);
|
|
215
|
+
if (!collection) {
|
|
216
|
+
collection = this.client.getOrCreateCollection({ name: indexName });
|
|
217
|
+
this.collections.set(indexName, collection);
|
|
218
|
+
}
|
|
219
|
+
return collection;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Add or update vectors in an index (Chroma collection).
|
|
223
|
+
*/
|
|
224
|
+
async upsert(indexName, items) {
|
|
225
|
+
if (items.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const collection = await this.getCollection(indexName);
|
|
229
|
+
await collection.upsert({
|
|
230
|
+
ids: items.map((item) => item.id),
|
|
231
|
+
embeddings: items.map((item) => item.vector),
|
|
232
|
+
metadatas: items.map((item) => toChromaMetadata(item.metadata ?? {})),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Query vectors by similarity.
|
|
237
|
+
*/
|
|
238
|
+
async query(params) {
|
|
239
|
+
const { indexName, queryVector, topK = 10, filter, includeVectors = false, } = params;
|
|
240
|
+
const collection = await this.getCollection(indexName);
|
|
241
|
+
const include = ["metadatas", "distances"];
|
|
242
|
+
if (includeVectors) {
|
|
243
|
+
include.push("embeddings");
|
|
244
|
+
}
|
|
245
|
+
const response = await collection.query({
|
|
246
|
+
queryEmbeddings: [queryVector],
|
|
247
|
+
nResults: topK,
|
|
248
|
+
...(filter ? { where: translateMetadataFilter(filter) } : {}),
|
|
249
|
+
include,
|
|
250
|
+
});
|
|
251
|
+
const ids = response.ids[0] ?? [];
|
|
252
|
+
const metadatasRow = response.metadatas?.[0] ?? [];
|
|
253
|
+
const distancesRow = response.distances?.[0] ?? [];
|
|
254
|
+
const embeddingsRow = response.embeddings?.[0] ?? [];
|
|
255
|
+
return ids.map((id, i) => {
|
|
256
|
+
const metadata = (metadatasRow[i] ?? {});
|
|
257
|
+
const distance = distancesRow[i];
|
|
258
|
+
const score = distance === null || distance === undefined
|
|
259
|
+
? undefined
|
|
260
|
+
: distanceToScore(distance, this.distanceMetric);
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
score,
|
|
264
|
+
text: metadata.text,
|
|
265
|
+
metadata,
|
|
266
|
+
...(includeVectors ? { vector: embeddingsRow[i] ?? [] } : {}),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Delete vectors from an index (Chroma collection) by id.
|
|
272
|
+
*/
|
|
273
|
+
async delete(indexName, ids) {
|
|
274
|
+
if (ids.length === 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const collection = await this.getCollection(indexName);
|
|
278
|
+
await collection.delete({ ids });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=chroma.js.map
|