@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/adapters/tts/googleTTSHandler.d.ts +10 -0
  3. package/dist/adapters/tts/googleTTSHandler.js +27 -18
  4. package/dist/agent/directTools.js +1 -1
  5. package/dist/browser/neurolink.min.js +387 -365
  6. package/dist/core/baseProvider.d.ts +37 -3
  7. package/dist/core/baseProvider.js +167 -44
  8. package/dist/core/modules/GenerationHandler.js +7 -1
  9. package/dist/core/modules/ToolsManager.js +5 -0
  10. package/dist/core/toolDedup.js +4 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/lib/adapters/tts/googleTTSHandler.d.ts +10 -0
  14. package/dist/lib/adapters/tts/googleTTSHandler.js +27 -18
  15. package/dist/lib/agent/directTools.js +1 -1
  16. package/dist/lib/core/baseProvider.d.ts +37 -3
  17. package/dist/lib/core/baseProvider.js +167 -44
  18. package/dist/lib/core/modules/GenerationHandler.js +7 -1
  19. package/dist/lib/core/modules/ToolsManager.js +5 -0
  20. package/dist/lib/core/toolDedup.js +4 -1
  21. package/dist/lib/index.d.ts +1 -1
  22. package/dist/lib/index.js +1 -1
  23. package/dist/lib/mcp/toolRegistry.js +7 -2
  24. package/dist/lib/neurolink.d.ts +31 -1
  25. package/dist/lib/neurolink.js +151 -26
  26. package/dist/lib/rag/index.d.ts +1 -0
  27. package/dist/lib/rag/index.js +2 -0
  28. package/dist/lib/rag/stores/chroma.d.ts +90 -0
  29. package/dist/lib/rag/stores/chroma.js +281 -0
  30. package/dist/lib/rag/stores/index.d.ts +21 -0
  31. package/dist/lib/rag/stores/index.js +22 -0
  32. package/dist/lib/rag/stores/pgvector.d.ts +95 -0
  33. package/dist/lib/rag/stores/pgvector.js +400 -0
  34. package/dist/lib/rag/stores/pinecone.d.ts +85 -0
  35. package/dist/lib/rag/stores/pinecone.js +159 -0
  36. package/dist/lib/server/routes/agentRoutes.js +25 -2
  37. package/dist/lib/tools/toolDiscovery.d.ts +48 -0
  38. package/dist/lib/tools/toolDiscovery.js +231 -0
  39. package/dist/lib/tools/toolGate.d.ts +16 -0
  40. package/dist/lib/tools/toolGate.js +51 -0
  41. package/dist/lib/tools/toolPolicy.d.ts +40 -0
  42. package/dist/lib/tools/toolPolicy.js +194 -0
  43. package/dist/lib/types/config.d.ts +43 -3
  44. package/dist/lib/types/index.d.ts +3 -0
  45. package/dist/lib/types/index.js +3 -0
  46. package/dist/lib/types/providers.d.ts +8 -0
  47. package/dist/lib/types/rag.d.ts +30 -0
  48. package/dist/lib/types/toolResolution.d.ts +73 -0
  49. package/dist/lib/types/toolResolution.js +11 -0
  50. package/dist/lib/types/vectorStoreChroma.d.ts +67 -0
  51. package/dist/lib/types/vectorStoreChroma.js +12 -0
  52. package/dist/lib/types/vectorStorePinecone.d.ts +48 -0
  53. package/dist/lib/types/vectorStorePinecone.js +12 -0
  54. package/dist/mcp/toolRegistry.js +7 -2
  55. package/dist/neurolink.d.ts +31 -1
  56. package/dist/neurolink.js +151 -26
  57. package/dist/rag/index.d.ts +1 -0
  58. package/dist/rag/index.js +2 -0
  59. package/dist/rag/stores/chroma.d.ts +90 -0
  60. package/dist/rag/stores/chroma.js +280 -0
  61. package/dist/rag/stores/index.d.ts +21 -0
  62. package/dist/rag/stores/index.js +21 -0
  63. package/dist/rag/stores/pgvector.d.ts +95 -0
  64. package/dist/rag/stores/pgvector.js +399 -0
  65. package/dist/rag/stores/pinecone.d.ts +85 -0
  66. package/dist/rag/stores/pinecone.js +158 -0
  67. package/dist/server/routes/agentRoutes.js +25 -2
  68. package/dist/tools/toolDiscovery.d.ts +48 -0
  69. package/dist/tools/toolDiscovery.js +230 -0
  70. package/dist/tools/toolGate.d.ts +16 -0
  71. package/dist/tools/toolGate.js +50 -0
  72. package/dist/tools/toolPolicy.d.ts +40 -0
  73. package/dist/tools/toolPolicy.js +193 -0
  74. package/dist/types/config.d.ts +43 -3
  75. package/dist/types/index.d.ts +3 -0
  76. package/dist/types/index.js +3 -0
  77. package/dist/types/providers.d.ts +8 -0
  78. package/dist/types/rag.d.ts +30 -0
  79. package/dist/types/toolResolution.d.ts +73 -0
  80. package/dist/types/toolResolution.js +10 -0
  81. package/dist/types/vectorStoreChroma.d.ts +67 -0
  82. package/dist/types/vectorStoreChroma.js +11 -0
  83. package/dist/types/vectorStorePinecone.d.ts +48 -0
  84. package/dist/types/vectorStorePinecone.js +11 -0
  85. package/package.json +12 -7
package/dist/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
- // enabledToolNames is an additional whitelist — merged into toolFilter
5906
- const whitelist = [
5907
- ...(options.toolFilter ?? []),
5908
- ...(options.enabledToolNames ?? []),
5909
- ];
5910
- if (whitelist.length === 0 &&
5911
- (!options.excludeTools || options.excludeTools.length === 0)) {
5912
- return tools;
5913
- }
5914
- let filtered = tools;
5915
- if (whitelist.length > 0) {
5916
- const allowSet = new Set(whitelist);
5917
- filtered = filtered.filter((t) => allowSet.has(t.name));
5918
- }
5919
- if (options.excludeTools && options.excludeTools.length > 0) {
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
- const tools = uniqueTools;
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,
@@ -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/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,280 @@
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
+ }