semantic-router-ts 1.0.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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.d.mts +272 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +467 -0
- package/dist/index.mjs +426 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nilesh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @anthropic/semantic-router
|
|
2
|
+
|
|
3
|
+
Superfast semantic routing for LLMs and AI agents. TypeScript port of [aurelio-labs/semantic-router](https://github.com/aurelio-labs/semantic-router).
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@anthropic/semantic-router)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Fast** - Uses semantic embeddings for sub-100ms routing decisions
|
|
11
|
+
- 🔌 **Pluggable Encoders** - OpenAI, local Transformers.js, or bring your own
|
|
12
|
+
- 🎯 **Accurate** - Semantic similarity beats keyword matching
|
|
13
|
+
- 📦 **Zero Dependencies** - Core package has no deps; encoders are optional
|
|
14
|
+
- 🔒 **Type Safe** - Full TypeScript support with strict types
|
|
15
|
+
- 🌐 **Offline Support** - LocalEncoder works without API calls
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @anthropic/semantic-router
|
|
21
|
+
|
|
22
|
+
# For local/offline embeddings (recommended)
|
|
23
|
+
npm install @xenova/transformers
|
|
24
|
+
|
|
25
|
+
# For OpenAI embeddings
|
|
26
|
+
npm install openai
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { SemanticRouter, Route, LocalEncoder } from '@anthropic/semantic-router';
|
|
33
|
+
|
|
34
|
+
// Define your routes
|
|
35
|
+
const routes: Route[] = [
|
|
36
|
+
{
|
|
37
|
+
name: 'greeting',
|
|
38
|
+
utterances: ['hello', 'hi there', 'hey', 'good morning'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'farewell',
|
|
42
|
+
utterances: ['goodbye', 'bye', 'see you later', 'take care'],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'help',
|
|
46
|
+
utterances: ['I need help', 'can you assist me', 'support please'],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Create router with local encoder (no API calls)
|
|
51
|
+
const router = new SemanticRouter({
|
|
52
|
+
routes,
|
|
53
|
+
encoder: new LocalEncoder(),
|
|
54
|
+
threshold: 0.4, // Minimum confidence to match
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Initialize (pre-encodes all routes)
|
|
58
|
+
await router.initialize();
|
|
59
|
+
|
|
60
|
+
// Route queries
|
|
61
|
+
const result = await router.route('hey, how are you doing?');
|
|
62
|
+
console.log(result.name); // 'greeting'
|
|
63
|
+
console.log(result.confidence); // 0.85
|
|
64
|
+
|
|
65
|
+
// No match below threshold
|
|
66
|
+
const noMatch = await router.route('what is the weather?');
|
|
67
|
+
console.log(noMatch.name); // null
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Encoders
|
|
71
|
+
|
|
72
|
+
### LocalEncoder (Recommended)
|
|
73
|
+
|
|
74
|
+
Uses [Transformers.js](https://huggingface.co/docs/transformers.js) for fully offline embeddings:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { LocalEncoder } from '@anthropic/semantic-router';
|
|
78
|
+
|
|
79
|
+
const encoder = new LocalEncoder({
|
|
80
|
+
model: 'Xenova/all-MiniLM-L6-v2', // Default, 384 dimensions
|
|
81
|
+
normalize: true,
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Supported models:
|
|
86
|
+
- `Xenova/all-MiniLM-L6-v2` (384d, fast)
|
|
87
|
+
- `Xenova/all-mpnet-base-v2` (768d, more accurate)
|
|
88
|
+
- `Xenova/bge-small-en-v1.5` (384d)
|
|
89
|
+
- `Xenova/bge-base-en-v1.5` (768d)
|
|
90
|
+
|
|
91
|
+
### OpenAIEncoder
|
|
92
|
+
|
|
93
|
+
Uses OpenAI's embedding API:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { OpenAIEncoder } from '@anthropic/semantic-router';
|
|
97
|
+
|
|
98
|
+
const encoder = new OpenAIEncoder({
|
|
99
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
100
|
+
model: 'text-embedding-3-small', // Default
|
|
101
|
+
dimensions: 1536,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const router = new SemanticRouter({
|
|
109
|
+
routes: [...],
|
|
110
|
+
encoder: new LocalEncoder(),
|
|
111
|
+
|
|
112
|
+
// How many similar utterances to consider
|
|
113
|
+
topK: 5,
|
|
114
|
+
|
|
115
|
+
// How to aggregate scores ('mean' | 'max' | 'sum')
|
|
116
|
+
aggregation: 'mean',
|
|
117
|
+
|
|
118
|
+
// Minimum confidence threshold (0-1)
|
|
119
|
+
threshold: 0.4,
|
|
120
|
+
|
|
121
|
+
// Optional LLM for low-confidence fallback
|
|
122
|
+
llm: myLLMImplementation,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Dynamic Routes
|
|
127
|
+
|
|
128
|
+
Add routes at runtime:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
await router.addRoute({
|
|
132
|
+
name: 'billing',
|
|
133
|
+
utterances: ['payment issue', 'invoice problem', 'billing question'],
|
|
134
|
+
description: 'Billing and payment related queries',
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## LLM Fallback
|
|
139
|
+
|
|
140
|
+
For ambiguous queries, provide an LLM to classify:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const router = new SemanticRouter({
|
|
144
|
+
routes,
|
|
145
|
+
encoder: new LocalEncoder(),
|
|
146
|
+
threshold: 0.4,
|
|
147
|
+
llm: {
|
|
148
|
+
name: 'claude',
|
|
149
|
+
generate: async (prompt) => {
|
|
150
|
+
// Your LLM call here
|
|
151
|
+
return await callClaude(prompt);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## API Reference
|
|
158
|
+
|
|
159
|
+
### SemanticRouter
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
|--------|-------------|
|
|
163
|
+
| `initialize()` | Pre-encode all routes (call once) |
|
|
164
|
+
| `route(query)` | Route a query, returns `RouteMatch` |
|
|
165
|
+
| `classify(query)` | Shorthand, returns route name or null |
|
|
166
|
+
| `addRoute(route)` | Add a route dynamically |
|
|
167
|
+
| `getStats()` | Get router statistics |
|
|
168
|
+
| `isReady()` | Check if initialized |
|
|
169
|
+
|
|
170
|
+
### RouteMatch
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
interface RouteMatch {
|
|
174
|
+
route: Route | null; // Full route object
|
|
175
|
+
name: string | null; // Route name
|
|
176
|
+
confidence: number; // 0-1 score
|
|
177
|
+
scores?: RouteScore[]; // All route scores
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Acknowledgments
|
|
182
|
+
|
|
183
|
+
This is a TypeScript port of the excellent [semantic-router](https://github.com/aurelio-labs/semantic-router) by Aurelio Labs.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route - A semantic route definition
|
|
3
|
+
*
|
|
4
|
+
* Routes define decision paths that the router can choose based on
|
|
5
|
+
* semantic similarity to user queries.
|
|
6
|
+
*/
|
|
7
|
+
interface Route {
|
|
8
|
+
/** Unique identifier for this route */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Example utterances that should trigger this route */
|
|
11
|
+
utterances: string[];
|
|
12
|
+
/** Optional description for documentation */
|
|
13
|
+
description?: string;
|
|
14
|
+
/** Optional metadata to attach to route matches */
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
/** Optional function schema for tool calling */
|
|
17
|
+
functionSchema?: FunctionSchema;
|
|
18
|
+
}
|
|
19
|
+
interface FunctionSchema {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
parameters?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* RouteMatch - Result of routing a query
|
|
26
|
+
*/
|
|
27
|
+
interface RouteMatch {
|
|
28
|
+
/** The matched route, or null if no match */
|
|
29
|
+
route: Route | null;
|
|
30
|
+
/** Name of the matched route */
|
|
31
|
+
name: string | null;
|
|
32
|
+
/** Confidence score (0-1) */
|
|
33
|
+
confidence: number;
|
|
34
|
+
/** All scored routes for debugging */
|
|
35
|
+
scores?: RouteScore[];
|
|
36
|
+
}
|
|
37
|
+
interface RouteScore {
|
|
38
|
+
name: string;
|
|
39
|
+
score: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* RouterConfig - Configuration for SemanticRouter
|
|
43
|
+
*/
|
|
44
|
+
interface RouterConfig {
|
|
45
|
+
/** Routes to register */
|
|
46
|
+
routes?: Route[];
|
|
47
|
+
/** Encoder to use for embeddings */
|
|
48
|
+
encoder?: Encoder;
|
|
49
|
+
/** Number of top matches to consider */
|
|
50
|
+
topK?: number;
|
|
51
|
+
/** Score aggregation method */
|
|
52
|
+
aggregation?: 'mean' | 'max' | 'sum';
|
|
53
|
+
/** Minimum score to consider a match */
|
|
54
|
+
threshold?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Encoder - Interface for embedding providers
|
|
58
|
+
*/
|
|
59
|
+
interface Encoder {
|
|
60
|
+
/** Encode a single text */
|
|
61
|
+
encode(text: string): Promise<number[]>;
|
|
62
|
+
/** Encode multiple texts (batch) */
|
|
63
|
+
encodeBatch(texts: string[]): Promise<number[][]>;
|
|
64
|
+
/** Encoder name for logging */
|
|
65
|
+
readonly name: string;
|
|
66
|
+
/** Embedding dimensions */
|
|
67
|
+
readonly dimensions: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Index - Interface for vector storage
|
|
71
|
+
*/
|
|
72
|
+
interface Index {
|
|
73
|
+
/** Add embeddings with route names */
|
|
74
|
+
add(embeddings: number[][], routes: string[], utterances: string[]): Promise<void>;
|
|
75
|
+
/** Query for similar embeddings */
|
|
76
|
+
query(embedding: number[], topK: number): Promise<IndexMatch[]>;
|
|
77
|
+
/** Clear all stored data */
|
|
78
|
+
clear(): Promise<void>;
|
|
79
|
+
/** Check if index is ready */
|
|
80
|
+
isReady(): boolean;
|
|
81
|
+
}
|
|
82
|
+
interface IndexMatch {
|
|
83
|
+
route: string;
|
|
84
|
+
utterance: string;
|
|
85
|
+
score: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* LLM - Interface for LLM fallback
|
|
89
|
+
*/
|
|
90
|
+
interface LLM {
|
|
91
|
+
/** Generate a response */
|
|
92
|
+
generate(prompt: string): Promise<string>;
|
|
93
|
+
/** LLM name for logging */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* SemanticRouter - Fast decision-making layer for LLMs
|
|
99
|
+
*
|
|
100
|
+
* Port of Python semantic_router.routers.semantic
|
|
101
|
+
*
|
|
102
|
+
* Uses semantic vector space to route queries to the best matching route
|
|
103
|
+
* based on similarity to pre-encoded utterances.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
interface SemanticRouterConfig extends RouterConfig {
|
|
107
|
+
/** Optional LLM for fallback classification */
|
|
108
|
+
llm?: LLM;
|
|
109
|
+
}
|
|
110
|
+
declare class SemanticRouter {
|
|
111
|
+
private routes;
|
|
112
|
+
private encoder;
|
|
113
|
+
private index;
|
|
114
|
+
private llm?;
|
|
115
|
+
private topK;
|
|
116
|
+
private aggregation;
|
|
117
|
+
private threshold;
|
|
118
|
+
private initialized;
|
|
119
|
+
private initPromise;
|
|
120
|
+
constructor(config?: SemanticRouterConfig);
|
|
121
|
+
/**
|
|
122
|
+
* Initialize the router by encoding all routes.
|
|
123
|
+
* Safe to call multiple times.
|
|
124
|
+
*/
|
|
125
|
+
initialize(): Promise<void>;
|
|
126
|
+
private doInitialize;
|
|
127
|
+
/**
|
|
128
|
+
* Route a query to the best matching route.
|
|
129
|
+
*/
|
|
130
|
+
route(query: string): Promise<RouteMatch>;
|
|
131
|
+
/**
|
|
132
|
+
* Shorthand to get just the route name
|
|
133
|
+
*/
|
|
134
|
+
classify(query: string): Promise<string | null>;
|
|
135
|
+
/**
|
|
136
|
+
* Add a route dynamically
|
|
137
|
+
*/
|
|
138
|
+
addRoute(route: Route): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Aggregate scores from multiple matches by route
|
|
141
|
+
*/
|
|
142
|
+
private aggregateScores;
|
|
143
|
+
/**
|
|
144
|
+
* Use LLM to classify when similarity is low
|
|
145
|
+
*/
|
|
146
|
+
private llmFallback;
|
|
147
|
+
/**
|
|
148
|
+
* Return a no-match result
|
|
149
|
+
*/
|
|
150
|
+
private noMatch;
|
|
151
|
+
/**
|
|
152
|
+
* Get router statistics
|
|
153
|
+
*/
|
|
154
|
+
getStats(): {
|
|
155
|
+
routeCount: number;
|
|
156
|
+
routes: string[];
|
|
157
|
+
encoder: string;
|
|
158
|
+
threshold: number;
|
|
159
|
+
initialized: boolean;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Check if router is ready
|
|
163
|
+
*/
|
|
164
|
+
isReady(): boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* BaseEncoder - Abstract base class for all encoders
|
|
169
|
+
*
|
|
170
|
+
* Port of Python semantic_router.encoders.base
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
declare abstract class BaseEncoder implements Encoder {
|
|
174
|
+
abstract readonly name: string;
|
|
175
|
+
abstract readonly dimensions: number;
|
|
176
|
+
protected initialized: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Encode a single text into an embedding vector.
|
|
179
|
+
*/
|
|
180
|
+
abstract encode(text: string): Promise<number[]>;
|
|
181
|
+
/**
|
|
182
|
+
* Encode multiple texts in batch.
|
|
183
|
+
* Default implementation calls encode() for each, but subclasses
|
|
184
|
+
* can override for more efficient batch processing.
|
|
185
|
+
*/
|
|
186
|
+
encodeBatch(texts: string[]): Promise<number[][]>;
|
|
187
|
+
/**
|
|
188
|
+
* Normalize an embedding vector to unit length.
|
|
189
|
+
*/
|
|
190
|
+
protected normalize(embedding: number[]): number[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* LocalEncoder - Offline embedding using Transformers.js
|
|
195
|
+
*
|
|
196
|
+
* Uses Xenova/all-MiniLM-L6-v2 by default (384 dimensions).
|
|
197
|
+
* Runs entirely locally with no API calls.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
interface LocalEncoderConfig {
|
|
201
|
+
/** Model to use (default: Xenova/all-MiniLM-L6-v2) */
|
|
202
|
+
model?: string;
|
|
203
|
+
/** Whether to normalize embeddings (default: true) */
|
|
204
|
+
normalize?: boolean;
|
|
205
|
+
}
|
|
206
|
+
declare class LocalEncoder extends BaseEncoder {
|
|
207
|
+
readonly name = "LocalEncoder";
|
|
208
|
+
readonly dimensions: number;
|
|
209
|
+
private model;
|
|
210
|
+
private embedder;
|
|
211
|
+
private initPromise;
|
|
212
|
+
private shouldNormalize;
|
|
213
|
+
private static readonly MODEL_DIMENSIONS;
|
|
214
|
+
constructor(config?: LocalEncoderConfig);
|
|
215
|
+
private ensureInitialized;
|
|
216
|
+
private initialize;
|
|
217
|
+
encode(text: string): Promise<number[]>;
|
|
218
|
+
encodeBatch(texts: string[]): Promise<number[][]>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* OpenAIEncoder - Embedding using OpenAI API
|
|
223
|
+
*
|
|
224
|
+
* Uses text-embedding-3-small by default (1536 dimensions).
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
interface OpenAIEncoderConfig {
|
|
228
|
+
/** OpenAI API key */
|
|
229
|
+
apiKey?: string;
|
|
230
|
+
/** Model to use (default: text-embedding-3-small) */
|
|
231
|
+
model?: string;
|
|
232
|
+
/** Embedding dimensions (for ada-3 models that support it) */
|
|
233
|
+
dimensions?: number;
|
|
234
|
+
}
|
|
235
|
+
declare class OpenAIEncoder extends BaseEncoder {
|
|
236
|
+
readonly name = "OpenAIEncoder";
|
|
237
|
+
readonly dimensions: number;
|
|
238
|
+
private model;
|
|
239
|
+
private apiKey;
|
|
240
|
+
private client;
|
|
241
|
+
private static readonly MODEL_DIMENSIONS;
|
|
242
|
+
constructor(config?: OpenAIEncoderConfig);
|
|
243
|
+
private ensureClient;
|
|
244
|
+
encode(text: string): Promise<number[]>;
|
|
245
|
+
encodeBatch(texts: string[]): Promise<number[][]>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* LocalIndex - In-memory vector storage
|
|
250
|
+
*
|
|
251
|
+
* Simple, fast index for development and small-scale use.
|
|
252
|
+
* Port of Python semantic_router.index.local
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
declare class LocalIndex implements Index {
|
|
256
|
+
private vectors;
|
|
257
|
+
private ready;
|
|
258
|
+
add(embeddings: number[][], routes: string[], utterances: string[]): Promise<void>;
|
|
259
|
+
query(embedding: number[], topK: number): Promise<IndexMatch[]>;
|
|
260
|
+
clear(): Promise<void>;
|
|
261
|
+
isReady(): boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Get number of stored vectors
|
|
264
|
+
*/
|
|
265
|
+
get size(): number;
|
|
266
|
+
/**
|
|
267
|
+
* Cosine similarity between two vectors
|
|
268
|
+
*/
|
|
269
|
+
private cosineSimilarity;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { BaseEncoder, type Encoder, type FunctionSchema, type Index, type IndexMatch, type LLM, LocalEncoder, type LocalEncoderConfig, LocalIndex, OpenAIEncoder, type OpenAIEncoderConfig, type Route, type RouteMatch, type RouteScore, type RouterConfig, SemanticRouter, type SemanticRouterConfig };
|