Files
wifi-densepose/vendor/ruvector/npm/packages/router/index.js

345 lines
10 KiB
JavaScript

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
};