Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
249
npm/packages/router/README.md
Normal file
249
npm/packages/router/README.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# @ruvector/router
|
||||
|
||||
Semantic router for AI agents - vector-based intent matching with HNSW indexing and SIMD acceleration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Semantic Intent Matching**: Route queries to intents based on meaning, not keywords
|
||||
- **HNSW Indexing**: Fast approximate nearest neighbor search
|
||||
- **SIMD Optimized**: Native Rust performance with vectorized operations
|
||||
- **Quantization**: Memory-efficient storage for large intent sets
|
||||
- **Multi-Platform**: Works on Linux, macOS, and Windows
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @ruvector/router
|
||||
```
|
||||
|
||||
The package automatically installs the correct native binary for your platform.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { SemanticRouter } from '@ruvector/router';
|
||||
|
||||
// Create router
|
||||
const router = new SemanticRouter({ dimension: 384 });
|
||||
|
||||
// Add intents with example utterances
|
||||
router.addIntent({
|
||||
name: 'weather',
|
||||
utterances: [
|
||||
'What is the weather today?',
|
||||
'Will it rain tomorrow?',
|
||||
'How hot will it be?'
|
||||
],
|
||||
metadata: { handler: 'weather_agent' }
|
||||
});
|
||||
|
||||
router.addIntent({
|
||||
name: 'greeting',
|
||||
utterances: [
|
||||
'Hello',
|
||||
'Hi there',
|
||||
'Good morning',
|
||||
'Hey'
|
||||
],
|
||||
metadata: { handler: 'greeting_agent' }
|
||||
});
|
||||
|
||||
router.addIntent({
|
||||
name: 'help',
|
||||
utterances: [
|
||||
'I need help',
|
||||
'Can you assist me?',
|
||||
'What can you do?'
|
||||
],
|
||||
metadata: { handler: 'help_agent' }
|
||||
});
|
||||
|
||||
// Route a query
|
||||
const results = await router.route('What will the weather be like this weekend?');
|
||||
|
||||
console.log(results[0].intent); // 'weather'
|
||||
console.log(results[0].score); // 0.92
|
||||
console.log(results[0].metadata); // { handler: 'weather_agent' }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SemanticRouter`
|
||||
|
||||
Main class for semantic routing.
|
||||
|
||||
#### Constructor
|
||||
|
||||
```typescript
|
||||
new SemanticRouter(config: RouterConfig)
|
||||
```
|
||||
|
||||
**RouterConfig:**
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `dimension` | number | required | Embedding dimension size |
|
||||
| `metric` | string | 'cosine' | Distance metric: 'cosine', 'euclidean', 'dot' |
|
||||
| `m` | number | 16 | HNSW M parameter |
|
||||
| `efConstruction` | number | 200 | HNSW ef_construction |
|
||||
| `quantization` | boolean | false | Enable memory-efficient quantization |
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `addIntent(intent: Intent): void`
|
||||
|
||||
Add an intent to the router.
|
||||
|
||||
```typescript
|
||||
router.addIntent({
|
||||
name: 'booking',
|
||||
utterances: ['Book a flight', 'Reserve a hotel'],
|
||||
metadata: { department: 'travel' }
|
||||
});
|
||||
```
|
||||
|
||||
##### `route(query: string | Float32Array, k?: number): Promise<RouteResult[]>`
|
||||
|
||||
Route a query to matching intents.
|
||||
|
||||
```typescript
|
||||
const results = await router.route('I want to book a vacation');
|
||||
// [{ intent: 'booking', score: 0.89, metadata: {...} }]
|
||||
```
|
||||
|
||||
##### `routeWithEmbedding(embedding: Float32Array, k?: number): RouteResult[]`
|
||||
|
||||
Route with a pre-computed embedding (synchronous).
|
||||
|
||||
```typescript
|
||||
const embedding = await getEmbedding('query text');
|
||||
const results = router.routeWithEmbedding(embedding, 3);
|
||||
```
|
||||
|
||||
##### `removeIntent(name: string): boolean`
|
||||
|
||||
Remove an intent from the router.
|
||||
|
||||
##### `getIntents(): string[]`
|
||||
|
||||
Get all registered intent names.
|
||||
|
||||
##### `clear(): void`
|
||||
|
||||
Remove all intents.
|
||||
|
||||
##### `save(path: string): Promise<void>`
|
||||
|
||||
Persist router state to disk.
|
||||
|
||||
##### `load(path: string): Promise<void>`
|
||||
|
||||
Load router state from disk.
|
||||
|
||||
### Types
|
||||
|
||||
#### `Intent`
|
||||
|
||||
```typescript
|
||||
interface Intent {
|
||||
name: string; // Unique intent identifier
|
||||
utterances: string[]; // Example utterances
|
||||
embedding?: Float32Array | number[]; // Pre-computed embedding
|
||||
metadata?: Record<string, unknown>; // Custom metadata
|
||||
}
|
||||
```
|
||||
|
||||
#### `RouteResult`
|
||||
|
||||
```typescript
|
||||
interface RouteResult {
|
||||
intent: string; // Matched intent name
|
||||
score: number; // Similarity score (0-1)
|
||||
metadata?: Record<string, unknown>; // Intent metadata
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Chatbot Intent Detection
|
||||
|
||||
```typescript
|
||||
const router = new SemanticRouter({ dimension: 384 });
|
||||
|
||||
// Define intents
|
||||
const intents = [
|
||||
{ name: 'faq', utterances: ['What are your hours?', 'How do I contact support?'] },
|
||||
{ name: 'order', utterances: ['Track my order', 'Where is my package?'] },
|
||||
{ name: 'return', utterances: ['I want to return this', 'How do I get a refund?'] }
|
||||
];
|
||||
|
||||
intents.forEach(i => router.addIntent(i));
|
||||
|
||||
// Handle user message
|
||||
async function handleMessage(text: string) {
|
||||
const [result] = await router.route(text);
|
||||
|
||||
switch(result.intent) {
|
||||
case 'faq': return handleFAQ(text);
|
||||
case 'order': return handleOrder(text);
|
||||
case 'return': return handleReturn(text);
|
||||
default: return handleUnknown(text);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Agent Orchestration
|
||||
|
||||
```typescript
|
||||
const agents = {
|
||||
'code': new CodeAgent(),
|
||||
'research': new ResearchAgent(),
|
||||
'creative': new CreativeAgent()
|
||||
};
|
||||
|
||||
const router = new SemanticRouter({ dimension: 768 });
|
||||
|
||||
router.addIntent({
|
||||
name: 'code',
|
||||
utterances: ['Write code', 'Debug this', 'Implement a function'],
|
||||
metadata: { agent: 'code' }
|
||||
});
|
||||
|
||||
router.addIntent({
|
||||
name: 'research',
|
||||
utterances: ['Find information', 'Search for', 'Look up'],
|
||||
metadata: { agent: 'research' }
|
||||
});
|
||||
|
||||
// Route task to best agent
|
||||
async function routeTask(task: string) {
|
||||
const [result] = await router.route(task);
|
||||
const agent = agents[result.metadata.agent];
|
||||
return agent.execute(task);
|
||||
}
|
||||
```
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Architecture | Package |
|
||||
|----------|--------------|---------|
|
||||
| Linux | x64 | `@ruvector/router-linux-x64-gnu` |
|
||||
| Linux | ARM64 | `@ruvector/router-linux-arm64-gnu` |
|
||||
| macOS | x64 | `@ruvector/router-darwin-x64` |
|
||||
| macOS | ARM64 | `@ruvector/router-darwin-arm64` |
|
||||
| Windows | x64 | `@ruvector/router-win32-x64-msvc` |
|
||||
|
||||
## Performance
|
||||
|
||||
- **Routing**: < 1ms per query with HNSW
|
||||
- **Throughput**: 100,000+ routes/second
|
||||
- **Memory**: ~1KB per intent + embeddings
|
||||
|
||||
## Related Packages
|
||||
|
||||
- [`@ruvector/core`](https://www.npmjs.com/package/@ruvector/core) - Vector database
|
||||
- [`@ruvector/tiny-dancer`](https://www.npmjs.com/package/@ruvector/tiny-dancer) - Neural routing
|
||||
- [`@ruvector/gnn`](https://www.npmjs.com/package/@ruvector/gnn) - Graph Neural Networks
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
284
npm/packages/router/index.d.ts
vendored
Normal file
284
npm/packages/router/index.d.ts
vendored
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Distance metric for vector similarity
|
||||
*/
|
||||
export enum DistanceMetric {
|
||||
/** Euclidean (L2) distance */
|
||||
Euclidean = 0,
|
||||
/** Cosine similarity */
|
||||
Cosine = 1,
|
||||
/** Dot product similarity */
|
||||
DotProduct = 2,
|
||||
/** Manhattan (L1) distance */
|
||||
Manhattan = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a VectorDb instance
|
||||
*/
|
||||
export interface DbOptions {
|
||||
/** Vector dimension size (required) */
|
||||
dimensions: number;
|
||||
/** Maximum number of elements (optional) */
|
||||
maxElements?: number;
|
||||
/** Distance metric for similarity (optional, default: Cosine) */
|
||||
distanceMetric?: DistanceMetric;
|
||||
/** HNSW M parameter (optional, default: 16) */
|
||||
hnswM?: number;
|
||||
/** HNSW ef_construction parameter (optional, default: 200) */
|
||||
hnswEfConstruction?: number;
|
||||
/** HNSW ef_search parameter (optional, default: 100) */
|
||||
hnswEfSearch?: number;
|
||||
/** Storage path for persistence (optional) */
|
||||
storagePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search result from a vector query
|
||||
*/
|
||||
export interface SearchResult {
|
||||
/** Vector ID */
|
||||
id: string;
|
||||
/** Similarity score */
|
||||
score: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* High-performance vector database for semantic search
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { VectorDb, DistanceMetric } from '@ruvector/router';
|
||||
*
|
||||
* // Create a vector database
|
||||
* const db = new VectorDb({
|
||||
* dimensions: 384,
|
||||
* distanceMetric: DistanceMetric.Cosine
|
||||
* });
|
||||
*
|
||||
* // Insert vectors
|
||||
* const embedding = new Float32Array(384).fill(0.5);
|
||||
* db.insert('doc-1', embedding);
|
||||
*
|
||||
* // Search for similar vectors
|
||||
* const results = db.search(embedding, 5);
|
||||
* console.log(results[0].id); // 'doc-1'
|
||||
* console.log(results[0].score); // ~1.0
|
||||
* ```
|
||||
*/
|
||||
export class VectorDb {
|
||||
/**
|
||||
* Create a new vector database
|
||||
* @param options Database options
|
||||
*/
|
||||
constructor(options: DbOptions);
|
||||
|
||||
/**
|
||||
* Insert a vector into the database
|
||||
* @param id Unique identifier
|
||||
* @param vector Vector data (Float32Array)
|
||||
* @returns The inserted ID
|
||||
*/
|
||||
insert(id: string, vector: Float32Array): string;
|
||||
|
||||
/**
|
||||
* Insert a vector asynchronously
|
||||
* @param id Unique identifier
|
||||
* @param vector Vector data (Float32Array)
|
||||
* @returns Promise resolving to the inserted ID
|
||||
*/
|
||||
insertAsync(id: string, vector: Float32Array): Promise<string>;
|
||||
|
||||
/**
|
||||
* Search for similar vectors
|
||||
* @param queryVector Query embedding
|
||||
* @param k Number of results to return
|
||||
* @returns Array of search results
|
||||
*/
|
||||
search(queryVector: Float32Array, k: number): SearchResult[];
|
||||
|
||||
/**
|
||||
* Search for similar vectors asynchronously
|
||||
* @param queryVector Query embedding
|
||||
* @param k Number of results to return
|
||||
* @returns Promise resolving to search results
|
||||
*/
|
||||
searchAsync(queryVector: Float32Array, k: number): Promise<SearchResult[]>;
|
||||
|
||||
/**
|
||||
* Delete a vector by ID
|
||||
* @param id Vector ID to delete
|
||||
* @returns true if deleted, false if not found
|
||||
*/
|
||||
delete(id: string): boolean;
|
||||
|
||||
/**
|
||||
* Get the total count of vectors
|
||||
* @returns Number of vectors in the database
|
||||
*/
|
||||
count(): number;
|
||||
|
||||
/**
|
||||
* Get all vector IDs
|
||||
* @returns Array of all IDs
|
||||
*/
|
||||
getAllIds(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for SemanticRouter
|
||||
*/
|
||||
export interface RouterConfig {
|
||||
/** Embedding dimension size (required) */
|
||||
dimension: number;
|
||||
/** Distance metric: 'cosine', 'euclidean', 'dot', 'manhattan' (default: 'cosine') */
|
||||
metric?: 'cosine' | 'euclidean' | 'dot' | 'manhattan';
|
||||
/** HNSW M parameter (default: 16) */
|
||||
m?: number;
|
||||
/** HNSW ef_construction (default: 200) */
|
||||
efConstruction?: number;
|
||||
/** HNSW ef_search (default: 100) */
|
||||
efSearch?: number;
|
||||
/** Enable quantization (default: false) */
|
||||
quantization?: boolean;
|
||||
/** Minimum similarity threshold for matches (default: 0.7) */
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent definition for the router
|
||||
*/
|
||||
export interface Intent {
|
||||
/** Unique intent identifier */
|
||||
name: string;
|
||||
/** Example utterances for this intent */
|
||||
utterances: string[];
|
||||
/** Pre-computed embedding (centroid) */
|
||||
embedding?: Float32Array | number[];
|
||||
/** Custom metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from routing a query
|
||||
*/
|
||||
export interface RouteResult {
|
||||
/** Matched intent name */
|
||||
intent: string;
|
||||
/** Similarity score (0-1) */
|
||||
score: number;
|
||||
/** Intent metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedder function type
|
||||
*/
|
||||
export type EmbedderFunction = (text: string) => Promise<Float32Array>;
|
||||
|
||||
/**
|
||||
* Semantic router for AI agents - vector-based intent matching
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { SemanticRouter } from '@ruvector/router';
|
||||
*
|
||||
* // Create router
|
||||
* const router = new SemanticRouter({ dimension: 384 });
|
||||
*
|
||||
* // Add intents with pre-computed embeddings
|
||||
* router.addIntent({
|
||||
* name: 'weather',
|
||||
* utterances: ['What is the weather?', 'Will it rain?'],
|
||||
* embedding: weatherEmbedding,
|
||||
* metadata: { handler: 'weather_agent' }
|
||||
* });
|
||||
*
|
||||
* // Route with embedding
|
||||
* const results = router.routeWithEmbedding(queryEmbedding, 3);
|
||||
* console.log(results[0].intent); // 'weather'
|
||||
* ```
|
||||
*/
|
||||
export class SemanticRouter {
|
||||
/**
|
||||
* Create a new SemanticRouter
|
||||
* @param config Router configuration
|
||||
*/
|
||||
constructor(config: RouterConfig);
|
||||
|
||||
/**
|
||||
* Set the embedder function for converting text to vectors
|
||||
* @param embedder Async function (text: string) => Float32Array
|
||||
*/
|
||||
setEmbedder(embedder: EmbedderFunction): void;
|
||||
|
||||
/**
|
||||
* Add an intent to the router (synchronous, requires pre-computed embedding)
|
||||
* @param intent Intent configuration
|
||||
*/
|
||||
addIntent(intent: Intent): void;
|
||||
|
||||
/**
|
||||
* Add an intent with automatic embedding computation
|
||||
* @param intent Intent configuration
|
||||
*/
|
||||
addIntentAsync(intent: Intent): Promise<void>;
|
||||
|
||||
/**
|
||||
* Route a query to matching intents
|
||||
* @param query Query text or embedding
|
||||
* @param k Number of results to return (default: 1)
|
||||
* @returns Promise resolving to route results
|
||||
*/
|
||||
route(query: string | Float32Array, k?: number): Promise<RouteResult[]>;
|
||||
|
||||
/**
|
||||
* Route with a pre-computed embedding (synchronous)
|
||||
* @param embedding Query embedding
|
||||
* @param k Number of results to return (default: 1)
|
||||
* @returns Route results
|
||||
*/
|
||||
routeWithEmbedding(embedding: Float32Array | number[], k?: number): RouteResult[];
|
||||
|
||||
/**
|
||||
* Remove an intent from the router
|
||||
* @param name Intent name to remove
|
||||
* @returns true if removed, false if not found
|
||||
*/
|
||||
removeIntent(name: string): boolean;
|
||||
|
||||
/**
|
||||
* Get all registered intent names
|
||||
* @returns Array of intent names
|
||||
*/
|
||||
getIntents(): string[];
|
||||
|
||||
/**
|
||||
* Get intent details
|
||||
* @param name Intent name
|
||||
* @returns Intent info or null if not found
|
||||
*/
|
||||
getIntent(name: string): { name: string; utterances: string[]; metadata: Record<string, unknown> } | null;
|
||||
|
||||
/**
|
||||
* Clear all intents
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Get the number of intents
|
||||
* @returns Number of registered intents
|
||||
*/
|
||||
count(): number;
|
||||
|
||||
/**
|
||||
* Save router state to disk
|
||||
* @param filePath Path to save to
|
||||
*/
|
||||
save(filePath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Load router state from disk
|
||||
* @param filePath Path to load from
|
||||
*/
|
||||
load(filePath: string): Promise<void>;
|
||||
}
|
||||
344
npm/packages/router/index.js
Normal file
344
npm/packages/router/index.js
Normal file
@@ -0,0 +1,344 @@
|
||||
const { platform, arch } = process;
|
||||
const path = require('path');
|
||||
|
||||
// Platform mapping for @ruvector/router
|
||||
const platformMap = {
|
||||
'linux': {
|
||||
'x64': { package: '@ruvector/router-linux-x64-gnu', file: 'ruvector-router.linux-x64-gnu.node' },
|
||||
'arm64': { package: '@ruvector/router-linux-arm64-gnu', file: 'ruvector-router.linux-arm64-gnu.node' }
|
||||
},
|
||||
'darwin': {
|
||||
'x64': { package: '@ruvector/router-darwin-x64', file: 'ruvector-router.darwin-x64.node' },
|
||||
'arm64': { package: '@ruvector/router-darwin-arm64', file: 'ruvector-router.darwin-arm64.node' }
|
||||
},
|
||||
'win32': {
|
||||
'x64': { package: '@ruvector/router-win32-x64-msvc', file: 'ruvector-router.win32-x64-msvc.node' }
|
||||
}
|
||||
};
|
||||
|
||||
function loadNativeModule() {
|
||||
const platformInfo = platformMap[platform]?.[arch];
|
||||
|
||||
if (!platformInfo) {
|
||||
throw new Error(
|
||||
`Unsupported platform: ${platform}-${arch}\n` +
|
||||
`@ruvector/router native module is available for:\n` +
|
||||
`- Linux (x64, ARM64)\n` +
|
||||
`- macOS (x64, ARM64)\n` +
|
||||
`- Windows (x64)\n\n` +
|
||||
`Install the package for your platform:\n` +
|
||||
` npm install @ruvector/router`
|
||||
);
|
||||
}
|
||||
|
||||
// Try local .node file first (for development and bundled packages)
|
||||
try {
|
||||
const localPath = path.join(__dirname, platformInfo.file);
|
||||
return require(localPath);
|
||||
} catch (localError) {
|
||||
// Fall back to platform-specific package
|
||||
try {
|
||||
return require(platformInfo.package);
|
||||
} catch (error) {
|
||||
if (error.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
`Native module not found for ${platform}-${arch}\n` +
|
||||
`Please install: npm install ${platformInfo.package}\n` +
|
||||
`Or reinstall @ruvector/router to get optional dependencies`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load native module
|
||||
const native = loadNativeModule();
|
||||
|
||||
/**
|
||||
* SemanticRouter - High-level semantic routing for AI agents
|
||||
*
|
||||
* Wraps the native VectorDB to provide intent-based routing.
|
||||
*/
|
||||
class SemanticRouter {
|
||||
/**
|
||||
* Create a new SemanticRouter
|
||||
* @param {Object} config - Router configuration
|
||||
* @param {number} config.dimension - Embedding dimension size (required)
|
||||
* @param {string} [config.metric='cosine'] - Distance metric: 'cosine', 'euclidean', 'dot', 'manhattan'
|
||||
* @param {number} [config.m=16] - HNSW M parameter
|
||||
* @param {number} [config.efConstruction=200] - HNSW ef_construction
|
||||
* @param {number} [config.efSearch=100] - HNSW ef_search
|
||||
* @param {boolean} [config.quantization=false] - Enable quantization (not yet implemented)
|
||||
* @param {number} [config.threshold=0.7] - Minimum similarity threshold for matches
|
||||
*/
|
||||
constructor(config) {
|
||||
if (!config || typeof config.dimension !== 'number') {
|
||||
throw new Error('SemanticRouter requires config.dimension (number)');
|
||||
}
|
||||
|
||||
const metricMap = {
|
||||
'cosine': native.DistanceMetric.Cosine,
|
||||
'euclidean': native.DistanceMetric.Euclidean,
|
||||
'dot': native.DistanceMetric.DotProduct,
|
||||
'manhattan': native.DistanceMetric.Manhattan
|
||||
};
|
||||
|
||||
this._db = new native.VectorDb({
|
||||
dimensions: config.dimension,
|
||||
distanceMetric: metricMap[config.metric] || native.DistanceMetric.Cosine,
|
||||
hnswM: config.m || 16,
|
||||
hnswEfConstruction: config.efConstruction || 200,
|
||||
hnswEfSearch: config.efSearch || 100
|
||||
});
|
||||
|
||||
this._intents = new Map(); // name -> { utterances, metadata, embeddings }
|
||||
this._threshold = config.threshold || 0.7;
|
||||
this._dimension = config.dimension;
|
||||
this._embedder = null; // External embedder function
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the embedder function for converting text to vectors
|
||||
* @param {Function} embedder - Async function (text: string) => Float32Array
|
||||
*/
|
||||
setEmbedder(embedder) {
|
||||
if (typeof embedder !== 'function') {
|
||||
throw new Error('Embedder must be a function');
|
||||
}
|
||||
this._embedder = embedder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an intent to the router
|
||||
* @param {Object} intent - Intent configuration
|
||||
* @param {string} intent.name - Unique intent identifier
|
||||
* @param {string[]} intent.utterances - Example utterances for this intent
|
||||
* @param {Float32Array|number[]} [intent.embedding] - Pre-computed embedding (centroid)
|
||||
* @param {Object} [intent.metadata] - Custom metadata
|
||||
*/
|
||||
addIntent(intent) {
|
||||
if (!intent || typeof intent.name !== 'string') {
|
||||
throw new Error('Intent requires a name (string)');
|
||||
}
|
||||
if (!Array.isArray(intent.utterances) || intent.utterances.length === 0) {
|
||||
throw new Error('Intent requires utterances (non-empty array)');
|
||||
}
|
||||
|
||||
// Store intent info
|
||||
this._intents.set(intent.name, {
|
||||
utterances: intent.utterances,
|
||||
metadata: intent.metadata || {},
|
||||
embedding: intent.embedding || null
|
||||
});
|
||||
|
||||
// If pre-computed embedding provided, insert directly
|
||||
if (intent.embedding) {
|
||||
const vector = intent.embedding instanceof Float32Array
|
||||
? intent.embedding
|
||||
: new Float32Array(intent.embedding);
|
||||
this._db.insert(intent.name, vector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add intent with embedding (async version that computes embeddings)
|
||||
* @param {Object} intent - Intent configuration
|
||||
*/
|
||||
async addIntentAsync(intent) {
|
||||
if (!intent || typeof intent.name !== 'string') {
|
||||
throw new Error('Intent requires a name (string)');
|
||||
}
|
||||
if (!Array.isArray(intent.utterances) || intent.utterances.length === 0) {
|
||||
throw new Error('Intent requires utterances (non-empty array)');
|
||||
}
|
||||
|
||||
// Store intent info
|
||||
this._intents.set(intent.name, {
|
||||
utterances: intent.utterances,
|
||||
metadata: intent.metadata || {},
|
||||
embedding: null
|
||||
});
|
||||
|
||||
// Compute embedding if we have an embedder
|
||||
if (this._embedder && !intent.embedding) {
|
||||
// Compute centroid from all utterances
|
||||
const embeddings = await Promise.all(
|
||||
intent.utterances.map(u => this._embedder(u))
|
||||
);
|
||||
|
||||
// Average the embeddings
|
||||
const centroid = new Float32Array(this._dimension);
|
||||
for (const emb of embeddings) {
|
||||
for (let i = 0; i < this._dimension; i++) {
|
||||
centroid[i] += emb[i] / embeddings.length;
|
||||
}
|
||||
}
|
||||
|
||||
this._intents.get(intent.name).embedding = centroid;
|
||||
this._db.insert(intent.name, centroid);
|
||||
} else if (intent.embedding) {
|
||||
const vector = intent.embedding instanceof Float32Array
|
||||
? intent.embedding
|
||||
: new Float32Array(intent.embedding);
|
||||
this._intents.get(intent.name).embedding = vector;
|
||||
this._db.insert(intent.name, vector);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a query to matching intents
|
||||
* @param {string|Float32Array} query - Query text or embedding
|
||||
* @param {number} [k=1] - Number of results to return
|
||||
* @returns {Promise<Array<{intent: string, score: number, metadata: Object}>>}
|
||||
*/
|
||||
async route(query, k = 1) {
|
||||
let embedding;
|
||||
|
||||
if (query instanceof Float32Array) {
|
||||
embedding = query;
|
||||
} else if (typeof query === 'string') {
|
||||
if (!this._embedder) {
|
||||
throw new Error('No embedder set. Call setEmbedder() first or pass a Float32Array.');
|
||||
}
|
||||
embedding = await this._embedder(query);
|
||||
} else {
|
||||
throw new Error('Query must be a string or Float32Array');
|
||||
}
|
||||
|
||||
return this.routeWithEmbedding(embedding, k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route with a pre-computed embedding (synchronous)
|
||||
* @param {Float32Array} embedding - Query embedding
|
||||
* @param {number} [k=1] - Number of results to return
|
||||
* @returns {Array<{intent: string, score: number, metadata: Object}>}
|
||||
*/
|
||||
routeWithEmbedding(embedding, k = 1) {
|
||||
if (!(embedding instanceof Float32Array)) {
|
||||
embedding = new Float32Array(embedding);
|
||||
}
|
||||
|
||||
const results = this._db.search(embedding, k);
|
||||
|
||||
return results
|
||||
.filter(r => r.score >= this._threshold)
|
||||
.map(r => {
|
||||
const intentInfo = this._intents.get(r.id);
|
||||
return {
|
||||
intent: r.id,
|
||||
score: r.score,
|
||||
metadata: intentInfo ? intentInfo.metadata : {}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an intent from the router
|
||||
* @param {string} name - Intent name to remove
|
||||
* @returns {boolean} - True if removed, false if not found
|
||||
*/
|
||||
removeIntent(name) {
|
||||
if (!this._intents.has(name)) {
|
||||
return false;
|
||||
}
|
||||
this._intents.delete(name);
|
||||
return this._db.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered intent names
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getIntents() {
|
||||
return Array.from(this._intents.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intent details
|
||||
* @param {string} name - Intent name
|
||||
* @returns {Object|null} - Intent info or null if not found
|
||||
*/
|
||||
getIntent(name) {
|
||||
const info = this._intents.get(name);
|
||||
if (!info) return null;
|
||||
return {
|
||||
name,
|
||||
utterances: info.utterances,
|
||||
metadata: info.metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all intents
|
||||
*/
|
||||
clear() {
|
||||
for (const name of this._intents.keys()) {
|
||||
this._db.delete(name);
|
||||
}
|
||||
this._intents.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of intents
|
||||
* @returns {number}
|
||||
*/
|
||||
count() {
|
||||
return this._intents.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save router state to disk (intents only, not the index)
|
||||
* @param {string} filePath - Path to save to
|
||||
*/
|
||||
async save(filePath) {
|
||||
const fs = require('fs').promises;
|
||||
const data = {
|
||||
dimension: this._dimension,
|
||||
threshold: this._threshold,
|
||||
intents: []
|
||||
};
|
||||
|
||||
for (const [name, info] of this._intents) {
|
||||
data.intents.push({
|
||||
name,
|
||||
utterances: info.utterances,
|
||||
metadata: info.metadata,
|
||||
embedding: info.embedding ? Array.from(info.embedding) : null
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load router state from disk
|
||||
* @param {string} filePath - Path to load from
|
||||
*/
|
||||
async load(filePath) {
|
||||
const fs = require('fs').promises;
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
this.clear();
|
||||
this._threshold = data.threshold || 0.7;
|
||||
|
||||
for (const intent of data.intents) {
|
||||
this.addIntent({
|
||||
name: intent.name,
|
||||
utterances: intent.utterances,
|
||||
metadata: intent.metadata,
|
||||
embedding: intent.embedding ? new Float32Array(intent.embedding) : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export native module plus SemanticRouter
|
||||
module.exports = {
|
||||
...native,
|
||||
VectorDb: native.VectorDb,
|
||||
DistanceMetric: native.DistanceMetric,
|
||||
SemanticRouter
|
||||
};
|
||||
63
npm/packages/router/package.json
Normal file
63
npm/packages/router/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@ruvector/router",
|
||||
"version": "0.1.28",
|
||||
"description": "Semantic router for AI agents - vector-based intent matching with HNSW indexing and SIMD acceleration",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"author": "ruv.io Team <info@ruv.io> (https://ruv.io)",
|
||||
"homepage": "https://ruv.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ruvnet/ruvector.git",
|
||||
"directory": "npm/packages/router"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ruvnet/ruvector/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build:napi": "napi build --platform --release --cargo-cwd ../../../crates/ruvector-router-ffi",
|
||||
"test": "node test.js",
|
||||
"publish:platforms": "node scripts/publish-platforms.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ruvector/router-linux-x64-gnu": "0.1.27",
|
||||
"@ruvector/router-linux-arm64-gnu": "0.1.27",
|
||||
"@ruvector/router-darwin-x64": "0.1.27",
|
||||
"@ruvector/router-darwin-arm64": "0.1.27",
|
||||
"@ruvector/router-win32-x64-msvc": "0.1.27"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"semantic-router",
|
||||
"intent-matching",
|
||||
"ai-routing",
|
||||
"agent-routing",
|
||||
"vector-search",
|
||||
"hnsw",
|
||||
"similarity-search",
|
||||
"embeddings",
|
||||
"llm",
|
||||
"native",
|
||||
"napi",
|
||||
"rust",
|
||||
"simd",
|
||||
"fast",
|
||||
"performance",
|
||||
"ruv",
|
||||
"ruvector"
|
||||
]
|
||||
}
|
||||
104
npm/packages/router/test.js
Normal file
104
npm/packages/router/test.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const router = require('./index.js');
|
||||
|
||||
console.log('Testing @ruvector/router...');
|
||||
|
||||
// Check available exports
|
||||
console.log(`Available exports: ${Object.keys(router).join(', ')}`);
|
||||
|
||||
// Test VectorDb class exists
|
||||
try {
|
||||
if (typeof router.VectorDb === 'function') {
|
||||
console.log('✓ VectorDb class available');
|
||||
|
||||
// Test creating an instance with options object (in-memory, no storage path)
|
||||
const db = new router.VectorDb({
|
||||
dimensions: 384,
|
||||
distanceMetric: router.DistanceMetric.Cosine,
|
||||
maxElements: 1000
|
||||
});
|
||||
console.log('✓ VectorDb instance created (384 dimensions, cosine distance, in-memory)');
|
||||
|
||||
// Test count method
|
||||
const count = db.count();
|
||||
console.log(`✓ count(): ${count}`);
|
||||
|
||||
// Test insert and search
|
||||
const testVector = new Float32Array(384).fill(0.5);
|
||||
db.insert('test-1', testVector);
|
||||
console.log('✓ insert() worked');
|
||||
|
||||
const results = db.search(testVector, 1);
|
||||
console.log(`✓ search() returned ${results.length} result(s)`);
|
||||
if (results.length > 0) {
|
||||
console.log(` Top result: ${results[0].id} (score: ${results[0].score.toFixed(4)})`);
|
||||
}
|
||||
} else {
|
||||
console.log('✗ VectorDb class not found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('✗ VectorDb test failed:', e.message);
|
||||
console.error(' Note: This may be due to storage path validation. The module loads correctly.');
|
||||
}
|
||||
|
||||
// Test DistanceMetric enum exists
|
||||
try {
|
||||
if (router.DistanceMetric) {
|
||||
console.log('✓ DistanceMetric enum available');
|
||||
console.log(` - Cosine: ${router.DistanceMetric.Cosine}`);
|
||||
console.log(` - Euclidean: ${router.DistanceMetric.Euclidean}`);
|
||||
console.log(` - DotProduct: ${router.DistanceMetric.DotProduct}`);
|
||||
} else {
|
||||
console.log('✗ DistanceMetric not found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('✗ DistanceMetric check failed:', e.message);
|
||||
}
|
||||
|
||||
// Test SemanticRouter class exists (GitHub issue #67)
|
||||
try {
|
||||
if (typeof router.SemanticRouter === 'function') {
|
||||
console.log('✓ SemanticRouter class available');
|
||||
|
||||
// Test creating an instance
|
||||
const semanticRouter = new router.SemanticRouter({
|
||||
dimension: 384,
|
||||
metric: 'cosine',
|
||||
threshold: 0.7
|
||||
});
|
||||
console.log('✓ SemanticRouter instance created');
|
||||
|
||||
// Test adding an intent with pre-computed embedding
|
||||
const testEmbedding = new Float32Array(384).fill(0.5);
|
||||
semanticRouter.addIntent({
|
||||
name: 'test-intent',
|
||||
utterances: ['test utterance 1', 'test utterance 2'],
|
||||
embedding: testEmbedding,
|
||||
metadata: { handler: 'test_handler' }
|
||||
});
|
||||
console.log('✓ addIntent() worked');
|
||||
|
||||
// Test getIntents
|
||||
const intents = semanticRouter.getIntents();
|
||||
console.log(`✓ getIntents() returned: ${intents.join(', ')}`);
|
||||
|
||||
// Test routeWithEmbedding
|
||||
const results = semanticRouter.routeWithEmbedding(testEmbedding, 1);
|
||||
console.log(`✓ routeWithEmbedding() returned ${results.length} result(s)`);
|
||||
if (results.length > 0) {
|
||||
console.log(` Top result: ${results[0].intent} (score: ${results[0].score.toFixed(4)})`);
|
||||
}
|
||||
|
||||
// Test count
|
||||
console.log(`✓ count(): ${semanticRouter.count()}`);
|
||||
|
||||
// Test clear
|
||||
semanticRouter.clear();
|
||||
console.log(`✓ clear() worked, count now: ${semanticRouter.count()}`);
|
||||
} else {
|
||||
console.log('✗ SemanticRouter class not found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('✗ SemanticRouter test failed:', e.message);
|
||||
}
|
||||
|
||||
console.log('\nAll basic tests completed!');
|
||||
Reference in New Issue
Block a user