@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
113 lines (112 loc) • 4.82 kB
TypeScript
/**
* Embedding-retrieval fast-path for pre-call tool routing (ITEM B / L2).
*
* For large tool catalogs, ranking by semantic similarity is far cheaper than
* an LLM router call and benchmarks at sub-10 ms when embedding vectors are
* cached. This module is intentionally PURE with no provider imports: the
* caller injects an `embedFn` so the retriever stays testable and free of
* circular dependencies.
*
* Hybrid scoring formula:
* score = weights.cosine * cosineSimilarity(queryVec, toolVec)
* + weights.bm25 * bm25Score(query, toolText)
*
* Both components are independently normalized to [0, 1] before combination
* so neither scale dominates.
*
* Fail-safe contract:
* `ToolEmbeddingIndex.rank()` and `selectRelevantToolNames()` propagate any
* error thrown by the injected `embedFn`. The CALLER (tool-routing
* orchestration) is responsible for catching that error and degrading
* gracefully to the LLM-router / server-granularity path.
*
* Determinism:
* All public functions are deterministic given the same inputs and embedFn.
* No Math.random(), no argless Date, no global mutable state outside the
* instance-level vector cache (which is keyed by text content).
*/
import type { ToolRetrievalItem, ToolRetrievalRankedResult, ToolRetrievalSelectOptions, ToolRetrievalWeights } from "../types/index.js";
/**
* Cosine similarity between two vectors.
*
* Returns 0 when either vector is zero-length, has zero magnitude, or the
* lengths differ — all of which indicate the comparison is meaningless.
*/
export declare function cosineSimilarity(a: number[], b: number[]): number;
/**
* An in-process index that ranks tool catalog items by hybrid semantic +
* lexical relevance to a query.
*
* Embedding vectors for tool descriptions are computed lazily on the first
* `rank()` call and cached by description text, so subsequent turns that
* share the same catalog pay only the cost of embedding the query itself.
*
* ### Fail-safe
* Any error thrown by `embedFn` propagates out of `rank()`. The CALLER must
* catch it and degrade to the LLM-router path; `ToolEmbeddingIndex` itself
* never silently swallows embedFn errors.
*
* ### Thread-safety
* The internal cache is a plain `Map`. Node.js is single-threaded so there
* are no data races, but if the same index instance is used concurrently
* (e.g. two turns in parallel) both calls will race to populate the cache;
* the last writer wins (same content either way because `embedFn` is
* deterministic for a given text).
*/
export declare class ToolEmbeddingIndex {
private readonly items;
private readonly embedFn;
private bm25CorpusCache?;
/**
* Cached embedding vectors keyed by the item's `text` field.
* Populated on first `rank()` call for texts not yet seen.
*
* Callers can share a single Map across multiple index instances (e.g.
* across turns in the same NeuroLink session) so tool vectors are computed
* once and reused. Pass the shared Map via the constructor's third argument.
*/
private readonly vectorCache;
constructor(items: ToolRetrievalItem[], embedFn: (texts: string[]) => Promise<number[][]>,
/**
* Optional shared vector cache. When supplied, cached vectors from
* previous turns are reused and any newly-computed vectors are stored
* into this same Map, making it warm for the next call.
*/
sharedVectorCache?: Map<string, number[]>);
/**
* Returns the top-K catalog items ranked by hybrid score descending.
*
* @throws If `embedFn` throws — propagated verbatim so the caller can fail
* open. No wrapping, no swallowing.
*/
rank(query: string, opts: {
topK: number;
weights?: ToolRetrievalWeights;
timeoutMs?: number;
}): Promise<ToolRetrievalRankedResult[]>;
}
/**
* Selects the most relevant tool names from a catalog given a query.
*
* Creates a temporary `ToolEmbeddingIndex`, runs `rank()`, and returns just
* the tool names. Use `ToolEmbeddingIndex` directly when you want to reuse
* cached tool vectors across multiple queries (e.g. multiple turns with the
* same catalog).
*
* @throws If `opts.embedFn` throws — propagated so the caller can degrade to
* the LLM-router path.
*
* @example
* ```ts
* const tools = await selectRelevantToolNames(
* "show me yesterday's sales",
* catalog.flatMap(server => server.toolNames.map(name => ({
* name,
* text: `${server.description} — ${name}`,
* }))),
* { topK: 5, embedFn: myEmbedFn },
* );
* // => ["analytics_getSales", "analytics_getRevenue", ...]
* ```
*/
export declare function selectRelevantToolNames(query: string, items: ToolRetrievalItem[], opts: ToolRetrievalSelectOptions): Promise<string[]>;