@juspay/neurolink 9.77.0 → 9.78.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 +6 -0
- package/dist/browser/neurolink.min.js +323 -323
- package/dist/core/toolRouting.d.ts +9 -0
- package/dist/core/toolRouting.js +178 -1
- package/dist/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/core/toolRoutingEmbedding.js +322 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/core/toolRouting.d.ts +9 -0
- package/dist/lib/core/toolRouting.js +178 -1
- package/dist/lib/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/lib/core/toolRoutingEmbedding.js +323 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/neurolink.d.ts +1 -0
- package/dist/lib/neurolink.js +177 -85
- package/dist/lib/types/toolRouting.d.ts +171 -0
- package/dist/neurolink.d.ts +1 -0
- package/dist/neurolink.js +177 -85
- package/dist/types/toolRouting.d.ts +171 -0
- package/package.json +4 -2
package/dist/neurolink.js
CHANGED
|
@@ -363,6 +363,11 @@ export class NeuroLink {
|
|
|
363
363
|
// Lazy-initialized routing decision cache (ITEM C). Created on first use so
|
|
364
364
|
// instances that don't use routing pay no overhead.
|
|
365
365
|
toolRoutingCacheInstance;
|
|
366
|
+
// Persisted vector cache for the L2 embedding fast-path (ITEM B). Populated
|
|
367
|
+
// on the first turn and reused across subsequent turns so tool embedding
|
|
368
|
+
// vectors are computed once. Cleared when the catalog changes via
|
|
369
|
+
// setToolRoutingServers() so stale vectors are never reused.
|
|
370
|
+
toolRoutingVectorCache;
|
|
366
371
|
// Opt-in tool-signature deduplication config.
|
|
367
372
|
toolDedupConfig;
|
|
368
373
|
// Add orchestration property
|
|
@@ -5934,7 +5939,11 @@ Current user's request: ${currentInput}`;
|
|
|
5934
5939
|
// Require a non-empty sessionId to avoid anonymous sessions sharing a
|
|
5935
5940
|
// ":query" namespace (cross-session cache leak).
|
|
5936
5941
|
const cacheKey = cacheEnabled && sessionId ? `${sessionId}:${routingQuery}` : undefined;
|
|
5942
|
+
const routableServerCount = catalog.filter((s) => !(routingConfig.alwaysIncludeServerIds ?? []).includes(s.id)).length;
|
|
5937
5943
|
// --- ITEM E: build the emitDecision callback ---
|
|
5944
|
+
// Constructed BEFORE the cache-hit check so it is available for all
|
|
5945
|
+
// outcome paths, including cache-hit turns (Finding 4).
|
|
5946
|
+
const routingStartTime = Date.now();
|
|
5938
5947
|
const emitDecision = (decision) => {
|
|
5939
5948
|
try {
|
|
5940
5949
|
const activeSpan = trace.getActiveSpan();
|
|
@@ -5949,100 +5958,176 @@ Current user's request: ${currentInput}`;
|
|
|
5949
5958
|
activeSpan.setAttribute("tool_routing.excluded_tool_count", decision.excludedToolCount);
|
|
5950
5959
|
activeSpan.setAttribute("tool_routing.cache_hit", decision.cacheHit);
|
|
5951
5960
|
activeSpan.setAttribute("tool_routing.duration_ms", decision.durationMs);
|
|
5961
|
+
// Optional L2 / tool-granularity fields (present only when embedding ran).
|
|
5962
|
+
if (decision.embeddingActivated !== undefined) {
|
|
5963
|
+
activeSpan.setAttribute("tool_routing.embedding_activated", decision.embeddingActivated);
|
|
5964
|
+
}
|
|
5965
|
+
if (decision.candidateToolCount !== undefined) {
|
|
5966
|
+
activeSpan.setAttribute("tool_routing.candidate_tool_count", decision.candidateToolCount);
|
|
5967
|
+
}
|
|
5968
|
+
if (decision.granularity !== undefined) {
|
|
5969
|
+
activeSpan.setAttribute("tool_routing.granularity", decision.granularity);
|
|
5970
|
+
}
|
|
5952
5971
|
}
|
|
5953
5972
|
catch {
|
|
5954
5973
|
// Telemetry must never affect routing behaviour.
|
|
5955
5974
|
}
|
|
5956
5975
|
};
|
|
5957
|
-
//
|
|
5958
|
-
|
|
5976
|
+
// Check the cache first.
|
|
5977
|
+
if (cacheEnabled && cache && cacheKey !== undefined) {
|
|
5978
|
+
const cached = cache.get(cacheKey);
|
|
5979
|
+
if (cached) {
|
|
5980
|
+
// Decrement stickiness turn counter even on a cache hit so the window
|
|
5981
|
+
// advances correctly regardless of whether the LLM router ran
|
|
5982
|
+
// (Finding 3). Re-apply stickiness to the cached exclusion list so
|
|
5983
|
+
// the live stickiness window, not the one at write-time, is honoured
|
|
5984
|
+
// (Finding 2 complement: we stored the pre-stickiness list, so
|
|
5985
|
+
// re-applying here gives the correct per-turn view).
|
|
5986
|
+
let cachedExcluded = cached.excludedToolNames;
|
|
5987
|
+
if (stickinessEnabled && sessionId) {
|
|
5988
|
+
try {
|
|
5989
|
+
const stickyIds = cache.getStickyServerIds(sessionId);
|
|
5990
|
+
if (stickyIds.length > 0) {
|
|
5991
|
+
cachedExcluded = cachedExcluded.filter((toolName) => !stickyIds.some((id) => toolName.startsWith(`${id}_`)));
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
catch {
|
|
5995
|
+
// Stickiness failure is non-fatal.
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
// Notify the emitDecision callback so custom telemetry sinks observe
|
|
5999
|
+
// every routing outcome, not just non-cached turns (Finding 4).
|
|
6000
|
+
const cachedSelectedSet = new Set(cached.selectedServerIds);
|
|
6001
|
+
const cachedExcludedServerIds = catalog
|
|
6002
|
+
.map((e) => e.id)
|
|
6003
|
+
.filter((id) => !cachedSelectedSet.has(id));
|
|
6004
|
+
emitDecision({
|
|
6005
|
+
outcome: "cache-hit",
|
|
6006
|
+
selectedServerIds: cached.selectedServerIds,
|
|
6007
|
+
excludedServerIds: cachedExcludedServerIds,
|
|
6008
|
+
hallucinatedIds: [],
|
|
6009
|
+
excludedToolCount: cachedExcluded.length,
|
|
6010
|
+
routableServerCount,
|
|
6011
|
+
cacheHit: true,
|
|
6012
|
+
durationMs: Date.now() - routingStartTime,
|
|
6013
|
+
});
|
|
6014
|
+
logger.debug("[ToolRouting] Cache hit, skipping router LLM", {
|
|
6015
|
+
hasSessionId: !!sessionId,
|
|
6016
|
+
routingQueryLength: routingQuery.length,
|
|
6017
|
+
});
|
|
6018
|
+
if (cachedExcluded.length > 0) {
|
|
6019
|
+
options.excludeTools = [
|
|
6020
|
+
...(options.excludeTools ?? []),
|
|
6021
|
+
...cachedExcluded,
|
|
6022
|
+
];
|
|
6023
|
+
}
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
// The router call below re-enters the public generate(), whose finally
|
|
6028
|
+
// block resets _disableToolCacheForCurrentRequest to false. That flag is
|
|
6029
|
+
// turn-scoped (set at the top of this turn) and read by the main tool
|
|
6030
|
+
// execution path that runs after routing, so save it before the router
|
|
6031
|
+
// call and restore it afterward to keep the turn's cache setting intact.
|
|
6032
|
+
const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
|
|
5959
6033
|
let routedExcludeTools;
|
|
5960
|
-
let selectedServerIds = [];
|
|
5961
6034
|
let resolvedDecision;
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6035
|
+
try {
|
|
6036
|
+
// Intercept the decision so we can store it in the cache.
|
|
6037
|
+
const captureDecision = (decision) => {
|
|
6038
|
+
resolvedDecision = decision;
|
|
6039
|
+
emitDecision(decision);
|
|
6040
|
+
};
|
|
6041
|
+
// --- ITEM B: build the embedFn for the L2 embedding fast-path ---
|
|
6042
|
+
// The vector cache is persisted at the NeuroLink instance level so
|
|
6043
|
+
// tool embedding vectors are computed once and reused across turns
|
|
6044
|
+
// (Finding 1 fix). It is cleared by setToolRoutingServers() when the
|
|
6045
|
+
// catalog changes so stale vectors are never used after an update.
|
|
6046
|
+
let routingEmbedFn;
|
|
6047
|
+
const embeddingCfg = routingConfig.embedding;
|
|
6048
|
+
if (embeddingCfg?.enabled === true) {
|
|
6049
|
+
try {
|
|
6050
|
+
// Resolve the embedding provider: use the explicitly configured one
|
|
6051
|
+
// if present, otherwise fall back to the stream/generate call's
|
|
6052
|
+
// provider. The factory call is wrapped in try/catch so a provider
|
|
6053
|
+
// that doesn't support embedMany (it throws at call time, not
|
|
6054
|
+
// construction time) fails open when routingEmbedFn is invoked.
|
|
6055
|
+
const embProviderName = embeddingCfg.provider ??
|
|
6056
|
+
(options.provider && options.provider !== "auto"
|
|
6057
|
+
? options.provider
|
|
6058
|
+
: undefined) ??
|
|
6059
|
+
routingConfig.routerModel?.provider;
|
|
6060
|
+
if (embProviderName) {
|
|
6061
|
+
const embProvider = await AIProviderFactory.createProvider(embProviderName, embeddingCfg.model, true, this, undefined, this.resolveCredentials(options.credentials));
|
|
6062
|
+
// Bind embedMany with the configured model (may be undefined —
|
|
6063
|
+
// the provider uses its default embedding model in that case).
|
|
6064
|
+
routingEmbedFn = (texts) => withTimeout(embProvider.embedMany(texts, embeddingCfg.model), embeddingCfg.timeoutMs ?? 10000);
|
|
6065
|
+
// Lazy-init the persistent vector cache for this instance.
|
|
6066
|
+
// Subsequent turns reuse the same Map so text→vector lookups
|
|
6067
|
+
// already populated from earlier turns are served from memory.
|
|
6068
|
+
if (!this.toolRoutingVectorCache) {
|
|
6069
|
+
this.toolRoutingVectorCache = new Map();
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
catch (embSetupError) {
|
|
6074
|
+
logger.debug("[ToolRouting] Embedding provider setup failed, L2 path disabled for this turn", {
|
|
6075
|
+
error: embSetupError instanceof Error
|
|
6076
|
+
? embSetupError.message
|
|
6077
|
+
: String(embSetupError),
|
|
6078
|
+
});
|
|
6079
|
+
// routingEmbedFn remains undefined — fast-path is skipped.
|
|
5984
6080
|
}
|
|
5985
6081
|
}
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
6082
|
+
routedExcludeTools = await resolveToolRoutingExclusions({
|
|
6083
|
+
catalog,
|
|
6084
|
+
alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
|
|
6085
|
+
userQuery: routingQuery,
|
|
6086
|
+
routerPromptPrefix: routingConfig.routerPromptPrefix,
|
|
6087
|
+
routerModel: {
|
|
6088
|
+
provider: routingConfig.routerModel?.provider ??
|
|
6089
|
+
options.provider,
|
|
6090
|
+
model: routingConfig.routerModel?.model ?? options.model,
|
|
6091
|
+
region: routingConfig.routerModel?.region ?? options.region,
|
|
6092
|
+
temperature: routingConfig.routerModel?.temperature,
|
|
6093
|
+
},
|
|
6094
|
+
timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
|
|
6095
|
+
// Forward the abort signal so a cancelled turn aborts the router
|
|
6096
|
+
// call promptly instead of waiting out the routing timeout.
|
|
6097
|
+
generateFn: (generateOptions) => this.generate({
|
|
6098
|
+
...generateOptions,
|
|
6099
|
+
abortSignal: options.abortSignal,
|
|
6100
|
+
}),
|
|
6101
|
+
emitDecision: captureDecision,
|
|
6102
|
+
// L2 / ITEM D — only populated when embedding is configured.
|
|
6103
|
+
embedFn: routingEmbedFn,
|
|
6104
|
+
embeddingConfig: embeddingCfg,
|
|
6105
|
+
granularity: routingConfig.granularity ?? "server",
|
|
6106
|
+
// Pass the persistent vector cache so tool embeddings are reused
|
|
6107
|
+
// across turns (Finding 1).
|
|
6108
|
+
embeddingVectorCache: routingEmbedFn !== undefined
|
|
6109
|
+
? this.toolRoutingVectorCache
|
|
6110
|
+
: undefined,
|
|
6111
|
+
});
|
|
5989
6112
|
}
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
// resets _disableToolCacheForCurrentRequest to false. That flag is
|
|
5994
|
-
// turn-scoped and read by the main tool execution path after routing, so
|
|
5995
|
-
// save it before the router call and restore it afterward.
|
|
5996
|
-
const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
|
|
5997
|
-
try {
|
|
5998
|
-
// Intercept the decision so we can store it in the cache.
|
|
5999
|
-
const captureDecision = (decision) => {
|
|
6000
|
-
resolvedDecision = decision;
|
|
6001
|
-
emitDecision(decision);
|
|
6002
|
-
};
|
|
6003
|
-
routedExcludeTools = await resolveToolRoutingExclusions({
|
|
6004
|
-
catalog,
|
|
6005
|
-
alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
|
|
6006
|
-
userQuery: routingQuery,
|
|
6007
|
-
routerPromptPrefix: routingConfig.routerPromptPrefix,
|
|
6008
|
-
routerModel: {
|
|
6009
|
-
provider: routingConfig.routerModel?.provider ??
|
|
6010
|
-
options.provider,
|
|
6011
|
-
model: routingConfig.routerModel?.model ?? options.model,
|
|
6012
|
-
region: routingConfig.routerModel?.region ?? options.region,
|
|
6013
|
-
temperature: routingConfig.routerModel?.temperature,
|
|
6014
|
-
},
|
|
6015
|
-
timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
|
|
6016
|
-
// Forward the abort signal so a cancelled turn aborts the router
|
|
6017
|
-
// call promptly instead of waiting out the routing timeout.
|
|
6018
|
-
generateFn: (generateOptions) => this.generate({
|
|
6019
|
-
...generateOptions,
|
|
6020
|
-
abortSignal: options.abortSignal,
|
|
6021
|
-
}),
|
|
6022
|
-
emitDecision: captureDecision,
|
|
6023
|
-
});
|
|
6024
|
-
}
|
|
6025
|
-
finally {
|
|
6026
|
-
this._disableToolCacheForCurrentRequest =
|
|
6027
|
-
cacheDisabledForCurrentRequest;
|
|
6028
|
-
}
|
|
6029
|
-
if (resolvedDecision?.outcome === "applied") {
|
|
6030
|
-
selectedServerIds = resolvedDecision.selectedServerIds;
|
|
6031
|
-
}
|
|
6113
|
+
finally {
|
|
6114
|
+
this._disableToolCacheForCurrentRequest =
|
|
6115
|
+
cacheDisabledForCurrentRequest;
|
|
6032
6116
|
}
|
|
6033
6117
|
// Aborted during the router call — skip applying now-stale exclusions;
|
|
6034
6118
|
// the main generation path enforces the abort itself.
|
|
6035
6119
|
if (options.abortSignal?.aborted) {
|
|
6036
6120
|
return;
|
|
6037
6121
|
}
|
|
6038
|
-
//
|
|
6039
|
-
|
|
6040
|
-
//
|
|
6041
|
-
|
|
6042
|
-
// the
|
|
6043
|
-
//
|
|
6044
|
-
//
|
|
6045
|
-
//
|
|
6122
|
+
// Snapshot the raw (pre-stickiness) exclusion list before applying
|
|
6123
|
+
// stickiness overrides. The cache stores this snapshot so future cache
|
|
6124
|
+
// hits can re-apply the then-current stickiness state (Finding 2).
|
|
6125
|
+
const preStickinessExcludeTools = routedExcludeTools;
|
|
6126
|
+
// Apply stickiness FIRST: the sticky ids were recorded on a prior turn and
|
|
6127
|
+
// represent servers that should stay warm for the current turn. Consuming
|
|
6128
|
+
// (decrementing) them before recordSelection ensures the window covers the
|
|
6129
|
+
// correct set of future turns rather than burning one count on the same
|
|
6130
|
+
// turn the selection is recorded (off-by-one fix).
|
|
6046
6131
|
if (stickinessEnabled && cache && sessionId) {
|
|
6047
6132
|
try {
|
|
6048
6133
|
const stickyIds = cache.getStickyServerIds(sessionId);
|
|
@@ -6057,15 +6142,16 @@ Current user's request: ${currentInput}`;
|
|
|
6057
6142
|
// Stickiness failure is non-fatal.
|
|
6058
6143
|
}
|
|
6059
6144
|
}
|
|
6060
|
-
// --- ITEM C: store result for future turns
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6145
|
+
// --- ITEM C: store result for future turns ---
|
|
6146
|
+
if (resolvedDecision?.outcome === "applied" && cache) {
|
|
6147
|
+
// Store the PRE-stickiness exclusion list in the cache so future hits
|
|
6148
|
+
// re-apply the live stickiness window rather than the one from this
|
|
6149
|
+
// turn (Finding 2).
|
|
6064
6150
|
if (cacheEnabled && cacheKey !== undefined) {
|
|
6065
6151
|
try {
|
|
6066
6152
|
cache.set(cacheKey, {
|
|
6067
|
-
excludedToolNames:
|
|
6068
|
-
selectedServerIds,
|
|
6153
|
+
excludedToolNames: preStickinessExcludeTools,
|
|
6154
|
+
selectedServerIds: resolvedDecision.selectedServerIds,
|
|
6069
6155
|
});
|
|
6070
6156
|
}
|
|
6071
6157
|
catch {
|
|
@@ -6077,7 +6163,7 @@ Current user's request: ${currentInput}`;
|
|
|
6077
6163
|
// the same turn it is set (the off-by-one fix above).
|
|
6078
6164
|
if (stickinessEnabled && sessionId) {
|
|
6079
6165
|
try {
|
|
6080
|
-
cache.recordSelection(sessionId, selectedServerIds);
|
|
6166
|
+
cache.recordSelection(sessionId, resolvedDecision.selectedServerIds);
|
|
6081
6167
|
}
|
|
6082
6168
|
catch {
|
|
6083
6169
|
// Stickiness failure is non-fatal.
|
|
@@ -6174,6 +6260,12 @@ Current user's request: ${currentInput}`;
|
|
|
6174
6260
|
return;
|
|
6175
6261
|
}
|
|
6176
6262
|
this.toolRoutingConfig.servers = servers;
|
|
6263
|
+
// Clear the persisted vector cache so tool vectors are recomputed against
|
|
6264
|
+
// the new catalog on the next turn (stale vectors must never be reused).
|
|
6265
|
+
this.toolRoutingVectorCache = undefined;
|
|
6266
|
+
// Cached routing decisions are catalog-dependent too; force the next turn
|
|
6267
|
+
// to recompute exclusions against the new server/tool set.
|
|
6268
|
+
this.toolRoutingCacheInstance = undefined;
|
|
6177
6269
|
}
|
|
6178
6270
|
async validateStreamRequestOptions(options, startTime) {
|
|
6179
6271
|
await this.validateStreamInput(options);
|
|
@@ -38,6 +38,71 @@ export type ToolRoutingModelConfig = {
|
|
|
38
38
|
/** Router sampling temperature. Default: 0. */
|
|
39
39
|
temperature?: number;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Weights for the hybrid scoring formula used by `ToolEmbeddingIndex.rank()`.
|
|
43
|
+
* Scores are computed as: `cosine * cosine + bm25 * bm25Score` then
|
|
44
|
+
* normalized before sorting.
|
|
45
|
+
* Default: `{ cosine: 0.8, bm25: 0.2 }`.
|
|
46
|
+
*/
|
|
47
|
+
export type ToolRetrievalWeights = {
|
|
48
|
+
/** Weight applied to the cosine-similarity (dense) component. */
|
|
49
|
+
cosine: number;
|
|
50
|
+
/** Weight applied to the BM25 (sparse/lexical) component. */
|
|
51
|
+
bm25: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Configuration for the L2 embedding fast-path (ITEM B).
|
|
55
|
+
*
|
|
56
|
+
* When enabled and the catalog's total tool count reaches `minToolsToActivate`,
|
|
57
|
+
* a hybrid cosine + BM25 retriever ranks all tools by relevance to the query
|
|
58
|
+
* and takes the top-`topK` candidates. This is far cheaper than an LLM call
|
|
59
|
+
* (sub-10 ms warm) and fires BEFORE or INSTEAD of the LLM router.
|
|
60
|
+
*
|
|
61
|
+
* Fail-open: any embedding error (missing provider, network failure, wrong
|
|
62
|
+
* model) silently falls back to the existing LLM-router / server-granularity
|
|
63
|
+
* path — the turn is never broken.
|
|
64
|
+
*/
|
|
65
|
+
export type ToolRoutingEmbeddingConfig = {
|
|
66
|
+
/**
|
|
67
|
+
* Activate the embedding fast-path. Default: false (backward-compatible).
|
|
68
|
+
* Setting this to true without supplying `provider`/`model` causes the SDK
|
|
69
|
+
* to try the stream call's configured provider; if that provider does not
|
|
70
|
+
* support embeddings the layer fails open.
|
|
71
|
+
*/
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Maximum number of top-ranked tool candidates passed to the post-embedding
|
|
75
|
+
* decision stage. Default: 20.
|
|
76
|
+
*/
|
|
77
|
+
topK?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Minimum total tool count in the catalog before the embedding path
|
|
80
|
+
* activates. Below this threshold the catalog is small enough that the LLM
|
|
81
|
+
* router alone is cheap and fast. Default: 20.
|
|
82
|
+
*/
|
|
83
|
+
minToolsToActivate?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Weights for the hybrid scoring formula:
|
|
86
|
+
* score = cosine * cosineSim + bm25 * bm25Score (both normalized to [0,1])
|
|
87
|
+
* Default: `{ cosine: 0.8, bm25: 0.2 }`.
|
|
88
|
+
*/
|
|
89
|
+
weights?: ToolRetrievalWeights;
|
|
90
|
+
/**
|
|
91
|
+
* Provider name to use for the embedding call (e.g. "openai", "vertex").
|
|
92
|
+
* Defaults to the stream/generate call's configured provider. The provider
|
|
93
|
+
* must support `embedMany()`.
|
|
94
|
+
*/
|
|
95
|
+
provider?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Embedding model name (provider-specific). When omitted the provider's
|
|
98
|
+
* default embedding model is used (e.g. text-embedding-3-small for OpenAI).
|
|
99
|
+
*/
|
|
100
|
+
model?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Timeout for embedding calls in milliseconds. Default: 10000.
|
|
103
|
+
*/
|
|
104
|
+
timeoutMs?: number;
|
|
105
|
+
};
|
|
41
106
|
/** Constructor-level configuration for pre-call tool routing. */
|
|
42
107
|
export type ToolRoutingConfig = {
|
|
43
108
|
/** Master switch. Routing runs only when true AND the server catalog is non-empty. */
|
|
@@ -88,6 +153,24 @@ export type ToolRoutingConfig = {
|
|
|
88
153
|
/** Number of turns for which a previously-selected server stays warm. Default: 3. */
|
|
89
154
|
turns?: number;
|
|
90
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* L2 embedding fast-path (ITEM B). When enabled the SDK ranks tools by
|
|
158
|
+
* semantic + lexical relevance using a hybrid cosine/BM25 score and narrows
|
|
159
|
+
* the candidate set BEFORE (or instead of) the LLM router. Disabled by
|
|
160
|
+
* default for backward compatibility.
|
|
161
|
+
*/
|
|
162
|
+
embedding?: ToolRoutingEmbeddingConfig;
|
|
163
|
+
/**
|
|
164
|
+
* Routing granularity (ITEM D).
|
|
165
|
+
*
|
|
166
|
+
* - `"server"` (default) — routing excludes the tools of entire unpicked
|
|
167
|
+
* servers. This is the original behavior.
|
|
168
|
+
* - `"tool"` — routing excludes individual tools that are not in the
|
|
169
|
+
* embedding top-K candidate set, regardless of which server they belong
|
|
170
|
+
* to. Requires `embedding.enabled: true`; if the embedding fast-path is
|
|
171
|
+
* off (or fails) the granularity falls back to `"server"` automatically.
|
|
172
|
+
*/
|
|
173
|
+
granularity?: "server" | "tool";
|
|
91
174
|
};
|
|
92
175
|
/** Catalog entry pairing a server descriptor with its registered tool names. */
|
|
93
176
|
export type ToolRoutingCatalogEntry = {
|
|
@@ -152,6 +235,19 @@ export type ToolRoutingDecision = {
|
|
|
152
235
|
cacheHit: boolean;
|
|
153
236
|
/** Wall-clock time spent in the routing resolution in milliseconds. */
|
|
154
237
|
durationMs: number;
|
|
238
|
+
/** True when the L2 embedding fast-path ran and produced candidate results. */
|
|
239
|
+
embeddingActivated?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Number of tool candidates produced by the embedding retriever before the
|
|
242
|
+
* post-embedding server or tool filtering step.
|
|
243
|
+
*/
|
|
244
|
+
candidateToolCount?: number;
|
|
245
|
+
/**
|
|
246
|
+
* Granularity at which exclusions were applied ("server" or "tool").
|
|
247
|
+
* Matches `ToolRoutingConfig.granularity`; present only when routing was
|
|
248
|
+
* applied (outcome === "applied").
|
|
249
|
+
*/
|
|
250
|
+
granularity?: "server" | "tool";
|
|
155
251
|
};
|
|
156
252
|
/** Parameters for `resolveToolRoutingExclusions()`. */
|
|
157
253
|
export type ToolRoutingResolutionParams = {
|
|
@@ -176,4 +272,79 @@ export type ToolRoutingResolutionParams = {
|
|
|
176
272
|
* the resolver.
|
|
177
273
|
*/
|
|
178
274
|
emitDecision?: (decision: ToolRoutingDecision) => void;
|
|
275
|
+
/**
|
|
276
|
+
* Injected async function that converts an array of texts into embedding
|
|
277
|
+
* vectors. Built by the caller (NeuroLink) from the configured embedding
|
|
278
|
+
* provider so the resolver stays pure and free of provider imports.
|
|
279
|
+
* When undefined the embedding fast-path is skipped entirely.
|
|
280
|
+
*/
|
|
281
|
+
embedFn?: (texts: string[]) => Promise<number[][]>;
|
|
282
|
+
/**
|
|
283
|
+
* Embedding fast-path configuration forwarded from `ToolRoutingConfig`.
|
|
284
|
+
* Only consulted when `embedFn` is provided.
|
|
285
|
+
*/
|
|
286
|
+
embeddingConfig?: ToolRoutingEmbeddingConfig;
|
|
287
|
+
/**
|
|
288
|
+
* Routing granularity forwarded from `ToolRoutingConfig`. Default: "server".
|
|
289
|
+
*/
|
|
290
|
+
granularity?: "server" | "tool";
|
|
291
|
+
/**
|
|
292
|
+
* Optional shared vector cache for the L2 embedding fast-path. When
|
|
293
|
+
* supplied, tool embedding vectors computed on prior turns are reused rather
|
|
294
|
+
* than re-fetched from the embedding provider on every call.
|
|
295
|
+
*
|
|
296
|
+
* The NeuroLink instance manages the lifecycle: it creates the Map once and
|
|
297
|
+
* passes the same reference across turns. It clears the reference when the
|
|
298
|
+
* tool catalog changes (via `setToolRoutingServers`) so stale vectors are
|
|
299
|
+
* never used after a catalog update.
|
|
300
|
+
*/
|
|
301
|
+
embeddingVectorCache?: Map<string, number[]>;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* A single item in the tool retrieval catalog, pairing a tool name with the
|
|
305
|
+
* text (tool description + server context) used to build its embedding vector.
|
|
306
|
+
*/
|
|
307
|
+
export type ToolRetrievalItem = {
|
|
308
|
+
/** Fully-qualified tool name (e.g. `${serverId}_${toolName}`). */
|
|
309
|
+
name: string;
|
|
310
|
+
/** Descriptive text used as the embedding document for this tool. */
|
|
311
|
+
text: string;
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* One ranked result from `ToolEmbeddingIndex.rank()` or
|
|
315
|
+
* `selectRelevantToolNames()`.
|
|
316
|
+
*/
|
|
317
|
+
export type ToolRetrievalRankedResult = {
|
|
318
|
+
/** Tool name (mirrors `ToolRetrievalItem.name`). */
|
|
319
|
+
name: string;
|
|
320
|
+
/** Combined hybrid score (higher = more relevant). */
|
|
321
|
+
score: number;
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Options passed to `selectRelevantToolNames()` — the high-level convenience
|
|
325
|
+
* wrapper around `ToolEmbeddingIndex`.
|
|
326
|
+
*/
|
|
327
|
+
export type ToolRetrievalSelectOptions = {
|
|
328
|
+
/** Maximum number of tool names to return. */
|
|
329
|
+
topK: number;
|
|
330
|
+
/** Optional weight override (defaults to `{ cosine: 0.8, bm25: 0.2 }`). */
|
|
331
|
+
weights?: ToolRetrievalWeights;
|
|
332
|
+
/**
|
|
333
|
+
* Async function that converts an array of text strings into embedding
|
|
334
|
+
* vectors. Must return one vector per input text in the same order.
|
|
335
|
+
* Errors thrown here propagate to the caller (so it can fail open).
|
|
336
|
+
*/
|
|
337
|
+
embedFn: (texts: string[]) => Promise<number[][]>;
|
|
338
|
+
/**
|
|
339
|
+
* Optional shared vector cache (keyed by text string). When supplied the
|
|
340
|
+
* underlying `ToolEmbeddingIndex` reads from and writes to this Map so that
|
|
341
|
+
* tool vectors computed on a prior call are reused on subsequent calls for
|
|
342
|
+
* the same item text. Callers that want warm-cache behavior across turns
|
|
343
|
+
* should pass the same Map instance each time.
|
|
344
|
+
*/
|
|
345
|
+
vectorCache?: Map<string, number[]>;
|
|
346
|
+
/**
|
|
347
|
+
* Timeout for each embedding provider call in milliseconds. Default: 10000.
|
|
348
|
+
*/
|
|
349
|
+
timeoutMs?: number;
|
|
179
350
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.78.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
|
|
6
6
|
"author": {
|
|
@@ -128,7 +128,9 @@
|
|
|
128
128
|
"// CI tier — fast, no live AI calls, safe for every commit": "",
|
|
129
129
|
"test:unit:vitest": "pnpm exec vitest run test/toolRouting.test.ts",
|
|
130
130
|
"test:tool-routing": "pnpm run test:unit:vitest && npx tsx test/continuous-test-suite-tool-routing.ts",
|
|
131
|
-
"test:
|
|
131
|
+
"test:tool-routing-semantic:vitest": "pnpm exec vitest run test/toolRoutingSemantic.test.ts",
|
|
132
|
+
"test:tool-routing-semantic": "pnpm run test:tool-routing-semantic:vitest && npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
133
|
+
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:tool-routing-semantic:vitest",
|
|
132
134
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
|
|
133
135
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
134
136
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",
|