@juspay/neurolink 9.76.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 +12 -0
- package/dist/browser/neurolink.min.js +341 -341
- 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 +31 -0
- package/dist/index.js +35 -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 +31 -0
- package/dist/lib/index.js +35 -0
- package/dist/lib/neurolink.d.ts +22 -0
- package/dist/lib/neurolink.js +544 -175
- package/dist/lib/routing/index.d.ts +7 -0
- package/dist/lib/routing/index.js +8 -0
- package/dist/lib/routing/modelPool.d.ts +83 -0
- package/dist/lib/routing/modelPool.js +243 -0
- package/dist/lib/routing/requestRouter.d.ts +30 -0
- package/dist/lib/routing/requestRouter.js +81 -0
- package/dist/lib/types/config.d.ts +18 -0
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/index.js +4 -0
- package/dist/lib/types/modelPool.d.ts +47 -0
- package/dist/lib/types/modelPool.js +11 -0
- package/dist/lib/types/requestRouter.d.ts +75 -0
- package/dist/lib/types/requestRouter.js +16 -0
- package/dist/lib/types/toolRouting.d.ts +171 -0
- package/dist/lib/utils/providerErrorClassification.d.ts +24 -0
- package/dist/lib/utils/providerErrorClassification.js +89 -0
- package/dist/neurolink.d.ts +22 -0
- package/dist/neurolink.js +544 -175
- package/dist/routing/index.d.ts +7 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/modelPool.d.ts +83 -0
- package/dist/routing/modelPool.js +242 -0
- package/dist/routing/requestRouter.d.ts +30 -0
- package/dist/routing/requestRouter.js +80 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +4 -0
- package/dist/types/modelPool.d.ts +47 -0
- package/dist/types/modelPool.js +10 -0
- package/dist/types/requestRouter.d.ts +75 -0
- package/dist/types/requestRouter.js +15 -0
- package/dist/types/toolRouting.d.ts +171 -0
- package/dist/utils/providerErrorClassification.d.ts +24 -0
- package/dist/utils/providerErrorClassification.js +88 -0
- package/package.json +6 -2
package/dist/index.d.ts
CHANGED
|
@@ -219,6 +219,7 @@ export { logger } from "./utils/logger.js";
|
|
|
219
219
|
export { getPoolStats } from "./utils/redis.js";
|
|
220
220
|
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
221
221
|
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
222
|
+
export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
|
|
222
223
|
export declare function initializeTelemetry(): Promise<boolean>;
|
|
223
224
|
export declare function getTelemetryStatus(): Promise<{
|
|
224
225
|
enabled: boolean;
|
|
@@ -337,6 +338,36 @@ export { BALANCED_ADAPTIVE_WORKFLOW, createAdaptiveWorkflow, QUALITY_MAX_WORKFLO
|
|
|
337
338
|
export { CONSENSUS_3_FAST_WORKFLOW, CONSENSUS_3_WORKFLOW, createConsensus3WithPrompt, } from "./workflow/workflows/consensusWorkflow.js";
|
|
338
339
|
export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflow/workflows/fallbackWorkflow.js";
|
|
339
340
|
export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
|
|
341
|
+
/**
|
|
342
|
+
* ModelPool and RequestRouter — opt-in multi-provider failover with
|
|
343
|
+
* error-class-aware cooldown, and a pluggable pre-call provider/model router.
|
|
344
|
+
*
|
|
345
|
+
* @example ModelPool
|
|
346
|
+
* ```typescript
|
|
347
|
+
* import { ModelPool, classifyProviderError } from '@juspay/neurolink';
|
|
348
|
+
*
|
|
349
|
+
* const pool = new ModelPool({
|
|
350
|
+
* members: [
|
|
351
|
+
* { provider: 'anthropic', model: 'claude-sonnet-4-5' },
|
|
352
|
+
* { provider: 'vertex', model: 'gemini-2.5-flash' },
|
|
353
|
+
* ],
|
|
354
|
+
* strategy: 'priority',
|
|
355
|
+
* cooldownMs: 30_000,
|
|
356
|
+
* });
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @example RequestRouter
|
|
360
|
+
* ```typescript
|
|
361
|
+
* import { createDefaultRequestRouter } from '@juspay/neurolink';
|
|
362
|
+
*
|
|
363
|
+
* const router = createDefaultRequestRouter({
|
|
364
|
+
* visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
|
|
365
|
+
* largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
|
|
366
|
+
* smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
|
|
367
|
+
* });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
|
|
340
371
|
/**
|
|
341
372
|
* Server Adapters for exposing NeuroLink as HTTP APIs
|
|
342
373
|
*
|
package/dist/index.js
CHANGED
|
@@ -362,6 +362,8 @@ export { getPoolStats } from "./utils/redis.js";
|
|
|
362
362
|
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
363
363
|
// Pre-call tool routing resolver — exported for host integrations and testing
|
|
364
364
|
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
365
|
+
// Pre-call tool routing — embedding fast-path (ITEM B: L2 semantic retrieval)
|
|
366
|
+
export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
|
|
365
367
|
// ============================================================================
|
|
366
368
|
// REAL-TIME SERVICES & TELEMETRY - Enterprise Platform Features
|
|
367
369
|
// ============================================================================
|
|
@@ -541,6 +543,39 @@ export { AGGRESSIVE_FALLBACK_WORKFLOW, FAST_FALLBACK_WORKFLOW, } from "./workflo
|
|
|
541
543
|
// Pre-built workflows - Multi-judge
|
|
542
544
|
export { createMultiJudgeWorkflow, MULTI_JUDGE_3_WORKFLOW, MULTI_JUDGE_5_WORKFLOW, } from "./workflow/workflows/multiJudgeWorkflow.js";
|
|
543
545
|
// ============================================================================
|
|
546
|
+
// MODEL POOL & REQUEST ROUTER - Pluggable pre-call routing + failover
|
|
547
|
+
// ============================================================================
|
|
548
|
+
/**
|
|
549
|
+
* ModelPool and RequestRouter — opt-in multi-provider failover with
|
|
550
|
+
* error-class-aware cooldown, and a pluggable pre-call provider/model router.
|
|
551
|
+
*
|
|
552
|
+
* @example ModelPool
|
|
553
|
+
* ```typescript
|
|
554
|
+
* import { ModelPool, classifyProviderError } from '@juspay/neurolink';
|
|
555
|
+
*
|
|
556
|
+
* const pool = new ModelPool({
|
|
557
|
+
* members: [
|
|
558
|
+
* { provider: 'anthropic', model: 'claude-sonnet-4-5' },
|
|
559
|
+
* { provider: 'vertex', model: 'gemini-2.5-flash' },
|
|
560
|
+
* ],
|
|
561
|
+
* strategy: 'priority',
|
|
562
|
+
* cooldownMs: 30_000,
|
|
563
|
+
* });
|
|
564
|
+
* ```
|
|
565
|
+
*
|
|
566
|
+
* @example RequestRouter
|
|
567
|
+
* ```typescript
|
|
568
|
+
* import { createDefaultRequestRouter } from '@juspay/neurolink';
|
|
569
|
+
*
|
|
570
|
+
* const router = createDefaultRequestRouter({
|
|
571
|
+
* visionTier: { provider: 'vertex', model: 'gemini-2.5-pro' },
|
|
572
|
+
* largeTier: { provider: 'anthropic', model: 'claude-opus-4-5' },
|
|
573
|
+
* smallTier: { provider: 'anthropic', model: 'claude-haiku-3-5' },
|
|
574
|
+
* });
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
export { classifyProviderError, ModelPool, createDefaultRequestRouter, } from "./routing/index.js";
|
|
578
|
+
// ============================================================================
|
|
544
579
|
// SERVER ADAPTERS - HTTP API Framework Integration
|
|
545
580
|
// ============================================================================
|
|
546
581
|
// Server Types
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
* Fail-open by design: missing query, <=1 routable server, validation
|
|
17
17
|
* failure, empty/invalid pick, or any thrown error returns an EMPTY list
|
|
18
18
|
* (exclude nothing -> all tools, identical to routing disabled). Never throws.
|
|
19
|
+
*
|
|
20
|
+
* L2 embedding fast-path (ITEM B): when `params.embedFn` is supplied and the
|
|
21
|
+
* catalog's total tool count exceeds `embeddingConfig.minToolsToActivate`, a
|
|
22
|
+
* hybrid cosine+BM25 retriever narrows the candidate set BEFORE the LLM router.
|
|
23
|
+
* Any embedding failure falls back to the standard LLM-router path.
|
|
24
|
+
*
|
|
25
|
+
* Tool granularity (ITEM D): when `params.granularity === "tool"`, individual
|
|
26
|
+
* unpicked tools are excluded rather than whole servers. Falls back to "server"
|
|
27
|
+
* if the embedding path is disabled or fails.
|
|
19
28
|
*/
|
|
20
29
|
import type { ToolRoutingCatalogEntry, ToolRoutingResolutionParams, ToolRoutingServerDescriptor } from "../types/index.js";
|
|
21
30
|
/**
|
|
@@ -16,13 +16,27 @@
|
|
|
16
16
|
* Fail-open by design: missing query, <=1 routable server, validation
|
|
17
17
|
* failure, empty/invalid pick, or any thrown error returns an EMPTY list
|
|
18
18
|
* (exclude nothing -> all tools, identical to routing disabled). Never throws.
|
|
19
|
+
*
|
|
20
|
+
* L2 embedding fast-path (ITEM B): when `params.embedFn` is supplied and the
|
|
21
|
+
* catalog's total tool count exceeds `embeddingConfig.minToolsToActivate`, a
|
|
22
|
+
* hybrid cosine+BM25 retriever narrows the candidate set BEFORE the LLM router.
|
|
23
|
+
* Any embedding failure falls back to the standard LLM-router path.
|
|
24
|
+
*
|
|
25
|
+
* Tool granularity (ITEM D): when `params.granularity === "tool"`, individual
|
|
26
|
+
* unpicked tools are excluded rather than whole servers. Falls back to "server"
|
|
27
|
+
* if the embedding path is disabled or fails.
|
|
19
28
|
*/
|
|
20
29
|
import { z } from "zod";
|
|
21
30
|
import { logger } from "../utils/logger.js";
|
|
22
31
|
import { withTimeout } from "../utils/async/index.js";
|
|
32
|
+
import { selectRelevantToolNames } from "./toolRoutingEmbedding.js";
|
|
23
33
|
const routerOutputSchema = z.object({
|
|
24
34
|
servers: z.array(z.string()),
|
|
25
35
|
});
|
|
36
|
+
/** Default minimum catalog tool count that activates the embedding fast-path. */
|
|
37
|
+
const DEFAULT_EMBEDDING_MIN_TOOLS = 20;
|
|
38
|
+
/** Default number of top-ranked tool candidates the embedding retriever keeps. */
|
|
39
|
+
const DEFAULT_EMBEDDING_TOP_K = 20;
|
|
26
40
|
/**
|
|
27
41
|
* Upper bound on the user-query text interpolated into the router prompt.
|
|
28
42
|
* The query is untrusted and can attempt prompt injection — the blast radius
|
|
@@ -169,12 +183,116 @@ function safeEmitDecision(emitDecision, decision) {
|
|
|
169
183
|
// Intentionally swallowed — telemetry must never affect routing behaviour.
|
|
170
184
|
}
|
|
171
185
|
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// L2 embedding fast-path helpers (ITEM B + D)
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// ToolRoutingEmbeddingFastPathResult is imported from src/lib/types/toolRouting.ts
|
|
190
|
+
// (Critical Rule 2 — all type aliases live in src/lib/types/).
|
|
191
|
+
/**
|
|
192
|
+
* Runs the L2 embedding fast-path over `routableServers` for `userQuery`.
|
|
193
|
+
*
|
|
194
|
+
* Returns a result with `embeddingActivated: false` on any skip or error —
|
|
195
|
+
* the caller must treat this as a "fall back to LLM router" signal.
|
|
196
|
+
*
|
|
197
|
+
* This function is PURE with respect to provider access: the caller injects
|
|
198
|
+
* `embedFn` so no provider classes are imported here.
|
|
199
|
+
*/
|
|
200
|
+
async function runEmbeddingFastPath(userQuery, routableServers, alwaysIncludeServerIds, embedFn, opts) {
|
|
201
|
+
const { granularity, vectorCache } = opts;
|
|
202
|
+
const noop = {
|
|
203
|
+
excludedToolNames: [],
|
|
204
|
+
embeddingActivated: false,
|
|
205
|
+
candidateToolCount: 0,
|
|
206
|
+
granularity,
|
|
207
|
+
};
|
|
208
|
+
// Count routable tools (always-include servers are already excluded from
|
|
209
|
+
// routableServers at call site, but be defensive).
|
|
210
|
+
const routableToolCount = routableServers.reduce((sum, server) => sum + server.toolNames.length, 0);
|
|
211
|
+
if (routableToolCount < opts.minToolsToActivate) {
|
|
212
|
+
logger.debug("[ToolRouting] L2 embedding skipped — catalog below threshold", {
|
|
213
|
+
toolCount: routableToolCount,
|
|
214
|
+
minToolsToActivate: opts.minToolsToActivate,
|
|
215
|
+
});
|
|
216
|
+
return noop;
|
|
217
|
+
}
|
|
218
|
+
// Build retrieval items: one per individual tool, with text =
|
|
219
|
+
// "<server description> — <tool name>" so both server context and the tool
|
|
220
|
+
// identifier itself inform the embedding.
|
|
221
|
+
const items = routableServers.flatMap((server) => server.toolNames.map((toolName) => ({
|
|
222
|
+
name: toolName,
|
|
223
|
+
text: `${server.description} — ${toolName}`,
|
|
224
|
+
})));
|
|
225
|
+
try {
|
|
226
|
+
const topK = Math.min(opts.topK, items.length);
|
|
227
|
+
const topToolNames = await selectRelevantToolNames(userQuery, items, {
|
|
228
|
+
topK,
|
|
229
|
+
weights: opts.weights,
|
|
230
|
+
embedFn,
|
|
231
|
+
vectorCache,
|
|
232
|
+
});
|
|
233
|
+
const topToolSet = new Set(topToolNames);
|
|
234
|
+
if (granularity === "tool") {
|
|
235
|
+
// Tool-granularity (ITEM D): exclude every catalog tool NOT in the top-K,
|
|
236
|
+
// regardless of server. Always-include server tools are not in
|
|
237
|
+
// routableServers so they are untouched.
|
|
238
|
+
const excludedToolNames = items
|
|
239
|
+
.map((item) => item.name)
|
|
240
|
+
.filter((name) => !topToolSet.has(name));
|
|
241
|
+
logger.debug("[ToolRouting] L2 embedding applied (tool granularity)", {
|
|
242
|
+
totalTools: items.length,
|
|
243
|
+
topK,
|
|
244
|
+
keptTools: topToolNames.length,
|
|
245
|
+
excludedTools: excludedToolNames.length,
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
excludedToolNames,
|
|
249
|
+
embeddingActivated: true,
|
|
250
|
+
candidateToolCount: topToolNames.length,
|
|
251
|
+
granularity: "tool",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Server-granularity (default): map surviving tools back to their servers.
|
|
255
|
+
// A server is "kept" if at least one of its tools survived the top-K cut.
|
|
256
|
+
// The tools of every server with ZERO surviving tools are excluded.
|
|
257
|
+
const keptServerIds = new Set(routableServers
|
|
258
|
+
.filter((server) => server.toolNames.some((toolName) => topToolSet.has(toolName)))
|
|
259
|
+
.map((server) => server.id));
|
|
260
|
+
// Never exclude always-include servers (defensive — they should not be in
|
|
261
|
+
// routableServers, but guard against caller mistakes).
|
|
262
|
+
const alwaysSet = new Set(alwaysIncludeServerIds);
|
|
263
|
+
const excludedServers = routableServers.filter((server) => !keptServerIds.has(server.id) && !alwaysSet.has(server.id));
|
|
264
|
+
const excludedToolNames = excludedServers.flatMap((server) => server.toolNames);
|
|
265
|
+
logger.debug("[ToolRouting] L2 embedding applied (server granularity)", {
|
|
266
|
+
totalTools: items.length,
|
|
267
|
+
topK,
|
|
268
|
+
keptServers: keptServerIds.size,
|
|
269
|
+
excludedServers: excludedServers.length,
|
|
270
|
+
excludedTools: excludedToolNames.length,
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
excludedToolNames,
|
|
274
|
+
embeddingActivated: true,
|
|
275
|
+
candidateToolCount: topToolNames.length,
|
|
276
|
+
granularity: "server",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (embeddingError) {
|
|
280
|
+
logger.warn("[ToolRouting] L2 embedding fast-path failed, falling back to LLM router", {
|
|
281
|
+
error: embeddingError instanceof Error
|
|
282
|
+
? embeddingError.message
|
|
283
|
+
: String(embeddingError),
|
|
284
|
+
});
|
|
285
|
+
return noop;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
172
288
|
/**
|
|
173
289
|
* Resolves which registered tool names to EXCLUDE for a single stream() turn.
|
|
174
290
|
* Returns an empty list on any skip/failure path — see module doc.
|
|
175
291
|
*/
|
|
176
292
|
export async function resolveToolRoutingExclusions(params) {
|
|
177
|
-
const { catalog, alwaysIncludeServerIds, userQuery, routerPromptPrefix, routerModel, timeoutMs, generateFn, emitDecision,
|
|
293
|
+
const { catalog, alwaysIncludeServerIds, userQuery, routerPromptPrefix, routerModel, timeoutMs, generateFn, emitDecision,
|
|
294
|
+
// L2 / ITEM D parameters (all optional — omitting reproduces today's behavior)
|
|
295
|
+
embedFn, embeddingConfig, granularity = "server", embeddingVectorCache, } = params;
|
|
178
296
|
const routableServers = catalog.filter((server) => !alwaysIncludeServerIds.includes(server.id));
|
|
179
297
|
const routingStartTime = Date.now();
|
|
180
298
|
try {
|
|
@@ -198,6 +316,64 @@ export async function resolveToolRoutingExclusions(params) {
|
|
|
198
316
|
});
|
|
199
317
|
return [];
|
|
200
318
|
}
|
|
319
|
+
// -------------------------------------------------------------------------
|
|
320
|
+
// L2 EMBEDDING FAST-PATH (ITEM B + D)
|
|
321
|
+
// -------------------------------------------------------------------------
|
|
322
|
+
// Attempt the embedding retriever when an embedFn is provided. On success,
|
|
323
|
+
// return the embedding-derived exclusion list immediately WITHOUT an LLM
|
|
324
|
+
// call. On any failure (embedFn throws, threshold not met, etc.), fall
|
|
325
|
+
// through to the existing LLM-router path below.
|
|
326
|
+
if (embedFn) {
|
|
327
|
+
const embResult = await runEmbeddingFastPath(userQuery, routableServers, alwaysIncludeServerIds, embedFn, {
|
|
328
|
+
topK: embeddingConfig?.topK ?? DEFAULT_EMBEDDING_TOP_K,
|
|
329
|
+
minToolsToActivate: embeddingConfig?.minToolsToActivate ?? DEFAULT_EMBEDDING_MIN_TOOLS,
|
|
330
|
+
weights: embeddingConfig?.weights,
|
|
331
|
+
granularity,
|
|
332
|
+
vectorCache: embeddingVectorCache,
|
|
333
|
+
});
|
|
334
|
+
if (embResult.embeddingActivated) {
|
|
335
|
+
logger.debug("[ToolRouting] L2 embedding fast-path succeeded", {
|
|
336
|
+
granularity: embResult.granularity,
|
|
337
|
+
candidateToolCount: embResult.candidateToolCount,
|
|
338
|
+
excludedToolCount: embResult.excludedToolNames.length,
|
|
339
|
+
routableServerCount: routableServers.length,
|
|
340
|
+
durationMs: Date.now() - routingStartTime,
|
|
341
|
+
});
|
|
342
|
+
// For server-granularity results, reconstruct selected/excluded server
|
|
343
|
+
// ids for the decision so telemetry fields are consistent.
|
|
344
|
+
let selectedServerIds = [];
|
|
345
|
+
let excludedServerIds = [];
|
|
346
|
+
if (embResult.granularity === "server") {
|
|
347
|
+
const excludedToolSet = new Set(embResult.excludedToolNames);
|
|
348
|
+
// A server is "excluded" if ALL its tools are excluded.
|
|
349
|
+
excludedServerIds = routableServers
|
|
350
|
+
.filter((server) => server.toolNames.every((name) => excludedToolSet.has(name)))
|
|
351
|
+
.map((server) => server.id);
|
|
352
|
+
selectedServerIds = routableServers
|
|
353
|
+
.filter((server) => !excludedServerIds.includes(server.id))
|
|
354
|
+
.map((server) => server.id);
|
|
355
|
+
}
|
|
356
|
+
safeEmitDecision(emitDecision, {
|
|
357
|
+
outcome: "applied",
|
|
358
|
+
selectedServerIds,
|
|
359
|
+
excludedServerIds,
|
|
360
|
+
hallucinatedIds: [],
|
|
361
|
+
excludedToolCount: embResult.excludedToolNames.length,
|
|
362
|
+
routableServerCount: routableServers.length,
|
|
363
|
+
cacheHit: false,
|
|
364
|
+
durationMs: Date.now() - routingStartTime,
|
|
365
|
+
embeddingActivated: true,
|
|
366
|
+
candidateToolCount: embResult.candidateToolCount,
|
|
367
|
+
granularity: embResult.granularity,
|
|
368
|
+
});
|
|
369
|
+
return embResult.excludedToolNames;
|
|
370
|
+
}
|
|
371
|
+
// embedFn was provided but threshold not met OR retriever failed open —
|
|
372
|
+
// fall through to the LLM-router path below.
|
|
373
|
+
}
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
// EXISTING LLM ROUTER PATH (L3) — runs when embedding is off or fell back
|
|
376
|
+
// -------------------------------------------------------------------------
|
|
201
377
|
const routerPrompt = buildRouterPrompt(userQuery, routableServers, routerPromptPrefix);
|
|
202
378
|
// `timeout` lets the provider abort its own request (frees the socket);
|
|
203
379
|
// withTimeout adds a hard wall-clock ceiling over the whole call so router
|
|
@@ -290,6 +466,7 @@ export async function resolveToolRoutingExclusions(params) {
|
|
|
290
466
|
routableServerCount: routableServers.length,
|
|
291
467
|
cacheHit: false,
|
|
292
468
|
durationMs: Date.now() - routingStartTime,
|
|
469
|
+
granularity: "server",
|
|
293
470
|
});
|
|
294
471
|
return excludedToolNames;
|
|
295
472
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedding-retrieval fast-path for pre-call tool routing (ITEM B / L2).
|
|
3
|
+
*
|
|
4
|
+
* For large tool catalogs, ranking by semantic similarity is far cheaper than
|
|
5
|
+
* an LLM router call and benchmarks at sub-10 ms when embedding vectors are
|
|
6
|
+
* cached. This module is intentionally PURE with no provider imports: the
|
|
7
|
+
* caller injects an `embedFn` so the retriever stays testable and free of
|
|
8
|
+
* circular dependencies.
|
|
9
|
+
*
|
|
10
|
+
* Hybrid scoring formula:
|
|
11
|
+
* score = weights.cosine * cosineSimilarity(queryVec, toolVec)
|
|
12
|
+
* + weights.bm25 * bm25Score(query, toolText)
|
|
13
|
+
*
|
|
14
|
+
* Both components are independently normalized to [0, 1] before combination
|
|
15
|
+
* so neither scale dominates.
|
|
16
|
+
*
|
|
17
|
+
* Fail-safe contract:
|
|
18
|
+
* `ToolEmbeddingIndex.rank()` and `selectRelevantToolNames()` propagate any
|
|
19
|
+
* error thrown by the injected `embedFn`. The CALLER (tool-routing
|
|
20
|
+
* orchestration) is responsible for catching that error and degrading
|
|
21
|
+
* gracefully to the LLM-router / server-granularity path.
|
|
22
|
+
*
|
|
23
|
+
* Determinism:
|
|
24
|
+
* All public functions are deterministic given the same inputs and embedFn.
|
|
25
|
+
* No Math.random(), no argless Date, no global mutable state outside the
|
|
26
|
+
* instance-level vector cache (which is keyed by text content).
|
|
27
|
+
*/
|
|
28
|
+
import type { ToolRetrievalItem, ToolRetrievalRankedResult, ToolRetrievalSelectOptions, ToolRetrievalWeights } from "../types/index.js";
|
|
29
|
+
/**
|
|
30
|
+
* Cosine similarity between two vectors.
|
|
31
|
+
*
|
|
32
|
+
* Returns 0 when either vector is zero-length, has zero magnitude, or the
|
|
33
|
+
* lengths differ — all of which indicate the comparison is meaningless.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
36
|
+
/**
|
|
37
|
+
* An in-process index that ranks tool catalog items by hybrid semantic +
|
|
38
|
+
* lexical relevance to a query.
|
|
39
|
+
*
|
|
40
|
+
* Embedding vectors for tool descriptions are computed lazily on the first
|
|
41
|
+
* `rank()` call and cached by description text, so subsequent turns that
|
|
42
|
+
* share the same catalog pay only the cost of embedding the query itself.
|
|
43
|
+
*
|
|
44
|
+
* ### Fail-safe
|
|
45
|
+
* Any error thrown by `embedFn` propagates out of `rank()`. The CALLER must
|
|
46
|
+
* catch it and degrade to the LLM-router path; `ToolEmbeddingIndex` itself
|
|
47
|
+
* never silently swallows embedFn errors.
|
|
48
|
+
*
|
|
49
|
+
* ### Thread-safety
|
|
50
|
+
* The internal cache is a plain `Map`. Node.js is single-threaded so there
|
|
51
|
+
* are no data races, but if the same index instance is used concurrently
|
|
52
|
+
* (e.g. two turns in parallel) both calls will race to populate the cache;
|
|
53
|
+
* the last writer wins (same content either way because `embedFn` is
|
|
54
|
+
* deterministic for a given text).
|
|
55
|
+
*/
|
|
56
|
+
export declare class ToolEmbeddingIndex {
|
|
57
|
+
private readonly items;
|
|
58
|
+
private readonly embedFn;
|
|
59
|
+
private bm25CorpusCache?;
|
|
60
|
+
/**
|
|
61
|
+
* Cached embedding vectors keyed by the item's `text` field.
|
|
62
|
+
* Populated on first `rank()` call for texts not yet seen.
|
|
63
|
+
*
|
|
64
|
+
* Callers can share a single Map across multiple index instances (e.g.
|
|
65
|
+
* across turns in the same NeuroLink session) so tool vectors are computed
|
|
66
|
+
* once and reused. Pass the shared Map via the constructor's third argument.
|
|
67
|
+
*/
|
|
68
|
+
private readonly vectorCache;
|
|
69
|
+
constructor(items: ToolRetrievalItem[], embedFn: (texts: string[]) => Promise<number[][]>,
|
|
70
|
+
/**
|
|
71
|
+
* Optional shared vector cache. When supplied, cached vectors from
|
|
72
|
+
* previous turns are reused and any newly-computed vectors are stored
|
|
73
|
+
* into this same Map, making it warm for the next call.
|
|
74
|
+
*/
|
|
75
|
+
sharedVectorCache?: Map<string, number[]>);
|
|
76
|
+
/**
|
|
77
|
+
* Returns the top-K catalog items ranked by hybrid score descending.
|
|
78
|
+
*
|
|
79
|
+
* @throws If `embedFn` throws — propagated verbatim so the caller can fail
|
|
80
|
+
* open. No wrapping, no swallowing.
|
|
81
|
+
*/
|
|
82
|
+
rank(query: string, opts: {
|
|
83
|
+
topK: number;
|
|
84
|
+
weights?: ToolRetrievalWeights;
|
|
85
|
+
timeoutMs?: number;
|
|
86
|
+
}): Promise<ToolRetrievalRankedResult[]>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Selects the most relevant tool names from a catalog given a query.
|
|
90
|
+
*
|
|
91
|
+
* Creates a temporary `ToolEmbeddingIndex`, runs `rank()`, and returns just
|
|
92
|
+
* the tool names. Use `ToolEmbeddingIndex` directly when you want to reuse
|
|
93
|
+
* cached tool vectors across multiple queries (e.g. multiple turns with the
|
|
94
|
+
* same catalog).
|
|
95
|
+
*
|
|
96
|
+
* @throws If `opts.embedFn` throws — propagated so the caller can degrade to
|
|
97
|
+
* the LLM-router path.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const tools = await selectRelevantToolNames(
|
|
102
|
+
* "show me yesterday's sales",
|
|
103
|
+
* catalog.flatMap(server => server.toolNames.map(name => ({
|
|
104
|
+
* name,
|
|
105
|
+
* text: `${server.description} — ${name}`,
|
|
106
|
+
* }))),
|
|
107
|
+
* { topK: 5, embedFn: myEmbedFn },
|
|
108
|
+
* );
|
|
109
|
+
* // => ["analytics_getSales", "analytics_getRevenue", ...]
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function selectRelevantToolNames(query: string, items: ToolRetrievalItem[], opts: ToolRetrievalSelectOptions): Promise<string[]>;
|