@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 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();