@malipetek/semantic-router 0.0.1
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/README.md +57 -0
- package/package.json +16 -0
- package/src/semantic-router.js +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Semantic Router JS
|
|
2
|
+
|
|
3
|
+
A semantic router using fastembed. WIP but looks like working fine.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can install the package using npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @malipetek/semantic-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
```javascript
|
|
15
|
+
import semantic from'@malipetek/semantic-router';
|
|
16
|
+
// mimicing express syntax not very useful tho
|
|
17
|
+
const app = semantic();
|
|
18
|
+
|
|
19
|
+
app.on('toolcall', [
|
|
20
|
+
'use the tool',
|
|
21
|
+
'retrieve data from database',
|
|
22
|
+
'look up from files',
|
|
23
|
+
'read the file'
|
|
24
|
+
], () => {
|
|
25
|
+
console.log('A tool was called');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await app.route('we need to query database for this information, we might need to use a tool'); // callback gets called
|
|
29
|
+
```
|
|
30
|
+
Alternate usage:
|
|
31
|
+
```javascript
|
|
32
|
+
import semantic from'@malipetek/semantic-router';
|
|
33
|
+
// mimicing express syntax not very useful tho
|
|
34
|
+
const app = semantic();
|
|
35
|
+
|
|
36
|
+
app.on('toolcall', [
|
|
37
|
+
'use the tool',
|
|
38
|
+
'retrieve data from database',
|
|
39
|
+
'look up from files',
|
|
40
|
+
'read the file'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const result = await app.route('we need to query database for this information, we might need to use a tool');
|
|
44
|
+
|
|
45
|
+
console.log(result); // result will be Route onject with name, do whatever you want with it
|
|
46
|
+
/**
|
|
47
|
+
* {
|
|
48
|
+
* name: 'toolcall',
|
|
49
|
+
* data: [
|
|
50
|
+
* 'use the tool',
|
|
51
|
+
* 'retrieve data from database',
|
|
52
|
+
* 'look up from files',
|
|
53
|
+
* 'read the file'
|
|
54
|
+
* ]
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malipetek/semantic-router",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Semantic router for nodejs for my personal use, pretty straightforward, inspired by semantic router by aureliolabs",
|
|
5
|
+
"main": "src/semantic-router.js",
|
|
6
|
+
"author": "m.ali petek",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"private": false,
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"fastembed": "^1.14.1"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node tests/*.js"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import { EmbeddingModel, FlagEmbedding } from "fastembed";
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{ name: string, documents: string[], callback: Function, definitionVectors: Float32Array[] }} Route
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculates the similarity between the query vector and the index vectors.
|
|
9
|
+
*
|
|
10
|
+
* @param {Float32Array} xq - The query vector
|
|
11
|
+
* @param {Float32Array[]} index - The array dxszawof index vectors
|
|
12
|
+
* @return {Number} The maximum similarity score
|
|
13
|
+
*/
|
|
14
|
+
function similarity(xq, index) {
|
|
15
|
+
const indexNorm = index.map(vec => Math.sqrt(vec.reduce((sum, val) => sum + val ** 2, 0)));
|
|
16
|
+
const xqNorm = Math.sqrt(xq.reduce((sum, val) => sum + val ** 2, 0));
|
|
17
|
+
const sim = index.map((vec, i) => vec.reduce((sum, val, i) => sum + val * xq[i], 0) / (indexNorm[i] * xqNorm));
|
|
18
|
+
return Math.max(...sim); // Return the maximum similarity score
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EMITTER = new EventEmitter();
|
|
22
|
+
export class SemanticRouter {
|
|
23
|
+
constructor({ fastEmbedOptions } = {}) {
|
|
24
|
+
fastEmbedOptions = {
|
|
25
|
+
model: EmbeddingModel.BGESmallENV15,
|
|
26
|
+
showDownloadProgress: true,
|
|
27
|
+
...fastEmbedOptions
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @type {FlagEmbedding} */
|
|
31
|
+
this.model;
|
|
32
|
+
this.loaded = false;
|
|
33
|
+
/** @type {Route[]} */
|
|
34
|
+
this.routes = [];
|
|
35
|
+
|
|
36
|
+
FlagEmbedding.init(fastEmbedOptions).then(
|
|
37
|
+
/** @type {FlagEmbedding} */
|
|
38
|
+
model => {
|
|
39
|
+
this.model = model;
|
|
40
|
+
this.loaded = true;
|
|
41
|
+
EMITTER.emit('model-loaded');
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Waits for the model to be loaded and resolves with the model.
|
|
47
|
+
*
|
|
48
|
+
* @return {Promise<FlagEmbedding>} A promise that resolves with the loaded model.
|
|
49
|
+
*/
|
|
50
|
+
waitForModelLoad() {
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
if (this.loaded) {
|
|
53
|
+
resolve(this.model);
|
|
54
|
+
} else {
|
|
55
|
+
EMITTER.on('model-loaded', () => resolve(this.model));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* onModelLoaded - A function to register a callback for when the model is loaded.
|
|
62
|
+
*
|
|
63
|
+
* @param {(...args: any[]) => void} callback - The callback function to be executed when the model is loaded.
|
|
64
|
+
* @return {void}
|
|
65
|
+
*/
|
|
66
|
+
onModelLoaded(callback) {
|
|
67
|
+
// make sure callback is a function
|
|
68
|
+
if (typeof callback !== 'function') {
|
|
69
|
+
throw new Error('Callback must be a function');
|
|
70
|
+
}
|
|
71
|
+
EMITTER.prependListener('model-loaded', callback);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Asynchronously embeds the given text using the model after waiting for the model to load.
|
|
75
|
+
*
|
|
76
|
+
* @param {string[]} texts - the text to embed
|
|
77
|
+
* @return {Promise<Float32Array[]>} the embedded result
|
|
78
|
+
*/
|
|
79
|
+
async embed(texts) {
|
|
80
|
+
await this.waitForModelLoad();
|
|
81
|
+
const embeddings = this.model.embed(texts);
|
|
82
|
+
const embeddingsRes = [];
|
|
83
|
+
for await (const batch of embeddings) {
|
|
84
|
+
// make sure batch is Float32Array
|
|
85
|
+
embeddingsRes.push(batch);
|
|
86
|
+
}
|
|
87
|
+
/** @type {Float32Array[]} */
|
|
88
|
+
const result = embeddingsRes.flat();
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Adds a new route to the routes object.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} name - the name of the route
|
|
97
|
+
* @param {string[]} documents - the documents associated with the route
|
|
98
|
+
* @param {function} callback - the callback function for the route
|
|
99
|
+
* @return {Promise<undefined>}
|
|
100
|
+
*/
|
|
101
|
+
async on(name, documents = [], callback = () => {}) {
|
|
102
|
+
const vector = await this.embed(documents);
|
|
103
|
+
this.routes.push({
|
|
104
|
+
name,
|
|
105
|
+
documents,
|
|
106
|
+
callback,
|
|
107
|
+
definitionVectors: vector,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Asynchronously routes a query to the most similar route.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} query - the query to be routed
|
|
115
|
+
* @return {Promise<any>} the result of the routed query
|
|
116
|
+
*/
|
|
117
|
+
async route(query) {
|
|
118
|
+
const queryVector = await this.embed([query]);
|
|
119
|
+
/** @type {number[]} */
|
|
120
|
+
const similarityScores = [];
|
|
121
|
+
for (let index in this.routes) {
|
|
122
|
+
const route = this.routes[index];
|
|
123
|
+
if(!route.definitionVectors) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const similarityScore = similarity(queryVector[0], route.definitionVectors);
|
|
127
|
+
similarityScores.push(similarityScore);
|
|
128
|
+
}
|
|
129
|
+
const mostSimilarIndex = similarityScores.indexOf(similarityScores.find((x, i) => x === Math.max(...similarityScores)));
|
|
130
|
+
const likelyRoute = this.routes[mostSimilarIndex];
|
|
131
|
+
// if top score has very low score return null
|
|
132
|
+
if (similarityScores[mostSimilarIndex] < 0.1) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
likelyRoute.callback(query);
|
|
137
|
+
|
|
138
|
+
return likelyRoute;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export default () => new SemanticRouter();
|