@betterdb/semantic-cache 0.6.0 → 0.7.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 +19 -0
- package/dist/SemanticCache.d.ts +0 -2
- package/dist/SemanticCache.js +5 -56
- package/dist/utils.d.ts +1 -28
- package/dist/utils.js +5 -77
- package/package.json +12 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026-present BetterDB Inc.
|
|
2
|
+
|
|
3
|
+
Portions of this software are licensed as follows:
|
|
4
|
+
|
|
5
|
+
- All content residing under the "doc/" directory of this repository is licensed under the "Creative Commons: CC BY-SA 4.0 license".
|
|
6
|
+
|
|
7
|
+
- All content that resides under the "proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "proprietary/LICENSE".
|
|
8
|
+
|
|
9
|
+
- All third-party components incorporated into the BetterDB Software are licensed under the original license provided by the owner of the applicable component.
|
|
10
|
+
|
|
11
|
+
- Content outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
|
12
|
+
|
|
13
|
+
MIT License
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/SemanticCache.d.ts
CHANGED
|
@@ -175,8 +175,6 @@ export declare class SemanticCache {
|
|
|
175
175
|
private applyCostToPendingMiss;
|
|
176
176
|
private assertInitialized;
|
|
177
177
|
private assertDimension;
|
|
178
|
-
private isIndexNotFoundError;
|
|
179
|
-
private parseDimensionFromInfo;
|
|
180
178
|
}
|
|
181
179
|
export interface ThresholdEffectivenessResult {
|
|
182
180
|
category: string;
|
package/dist/SemanticCache.js
CHANGED
|
@@ -6,6 +6,7 @@ const node_crypto_2 = require("node:crypto");
|
|
|
6
6
|
const api_1 = require("@opentelemetry/api");
|
|
7
7
|
const errors_1 = require("./errors");
|
|
8
8
|
const telemetry_1 = require("./telemetry");
|
|
9
|
+
const valkey_search_kit_1 = require("@betterdb/valkey-search-kit");
|
|
9
10
|
const utils_1 = require("./utils");
|
|
10
11
|
const defaultCostTable_1 = require("./defaultCostTable");
|
|
11
12
|
const cluster_1 = require("./cluster");
|
|
@@ -151,7 +152,7 @@ class SemanticCache {
|
|
|
151
152
|
await this.client.call('FT.DROPINDEX', this.indexName);
|
|
152
153
|
}
|
|
153
154
|
catch (err) {
|
|
154
|
-
if (!
|
|
155
|
+
if (!(0, valkey_search_kit_1.isIndexNotFoundError)(err)) {
|
|
155
156
|
throw new errors_1.ValkeyCommandError('FT.DROPINDEX', err);
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -833,16 +834,7 @@ class SemanticCache {
|
|
|
833
834
|
catch (err) {
|
|
834
835
|
throw new errors_1.ValkeyCommandError('FT.INFO', err);
|
|
835
836
|
}
|
|
836
|
-
const
|
|
837
|
-
let numDocs = 0;
|
|
838
|
-
let indexingState = 'unknown';
|
|
839
|
-
for (let i = 0; i < info.length - 1; i += 2) {
|
|
840
|
-
const key = String(info[i]);
|
|
841
|
-
if (key === 'num_docs')
|
|
842
|
-
numDocs = parseInt(String(info[i + 1]), 10) || 0;
|
|
843
|
-
else if (key === 'indexing')
|
|
844
|
-
indexingState = String(info[i + 1]);
|
|
845
|
-
}
|
|
837
|
+
const { numDocs, indexingState } = (0, valkey_search_kit_1.parseFtInfoStats)(raw);
|
|
846
838
|
return { name: this.indexName, numDocs, dimension: this._dimension, indexingState };
|
|
847
839
|
}
|
|
848
840
|
/**
|
|
@@ -1173,7 +1165,7 @@ class SemanticCache {
|
|
|
1173
1165
|
// Try reading an existing index
|
|
1174
1166
|
try {
|
|
1175
1167
|
const info = (await this.client.call('FT.INFO', this.indexName));
|
|
1176
|
-
const dim =
|
|
1168
|
+
const dim = (0, valkey_search_kit_1.parseDimensionFromInfo)(info);
|
|
1177
1169
|
const hasBinaryRefs = this.parseHasBinaryRefsFromInfo(info);
|
|
1178
1170
|
if (dim > 0)
|
|
1179
1171
|
return { dim, hasBinaryRefs };
|
|
@@ -1184,7 +1176,7 @@ class SemanticCache {
|
|
|
1184
1176
|
catch (err) {
|
|
1185
1177
|
if (err instanceof errors_1.EmbeddingError)
|
|
1186
1178
|
throw err;
|
|
1187
|
-
if (!
|
|
1179
|
+
if (!(0, valkey_search_kit_1.isIndexNotFoundError)(err)) {
|
|
1188
1180
|
throw new errors_1.ValkeyCommandError('FT.INFO', err);
|
|
1189
1181
|
}
|
|
1190
1182
|
}
|
|
@@ -1417,49 +1409,6 @@ class SemanticCache {
|
|
|
1417
1409
|
throw new errors_1.SemanticCacheUsageError(`Embedding dimension mismatch: index expects ${this._dimension}, embedFn returned ${embedding.length}. Call flush() then initialize() to rebuild.`);
|
|
1418
1410
|
}
|
|
1419
1411
|
}
|
|
1420
|
-
isIndexNotFoundError(err) {
|
|
1421
|
-
const msg = err instanceof Error ? err.message.toLowerCase() : '';
|
|
1422
|
-
return (msg.includes('unknown index name') ||
|
|
1423
|
-
msg.includes('no such index') ||
|
|
1424
|
-
msg.includes('not found'));
|
|
1425
|
-
}
|
|
1426
|
-
parseDimensionFromInfo(info) {
|
|
1427
|
-
for (let i = 0; i < info.length - 1; i += 2) {
|
|
1428
|
-
const key = String(info[i]);
|
|
1429
|
-
if (key !== 'attributes' && key !== 'fields')
|
|
1430
|
-
continue;
|
|
1431
|
-
const attributes = info[i + 1];
|
|
1432
|
-
if (!Array.isArray(attributes))
|
|
1433
|
-
continue;
|
|
1434
|
-
for (const attr of attributes) {
|
|
1435
|
-
if (!Array.isArray(attr))
|
|
1436
|
-
continue;
|
|
1437
|
-
let isVector = false;
|
|
1438
|
-
let dim = 0;
|
|
1439
|
-
for (let j = 0; j < attr.length - 1; j++) {
|
|
1440
|
-
const attrKey = String(attr[j]);
|
|
1441
|
-
if (attrKey === 'type' && String(attr[j + 1]) === 'VECTOR')
|
|
1442
|
-
isVector = true;
|
|
1443
|
-
if (attrKey.toLowerCase() === 'dim')
|
|
1444
|
-
dim = parseInt(String(attr[j + 1]), 10) || 0;
|
|
1445
|
-
// Valkey Search 1.2 nests dimension inside an 'index' sub-array
|
|
1446
|
-
if (attrKey === 'index' && Array.isArray(attr[j + 1])) {
|
|
1447
|
-
const indexArr = attr[j + 1];
|
|
1448
|
-
for (let k = 0; k < indexArr.length - 1; k++) {
|
|
1449
|
-
if (String(indexArr[k]) === 'dimensions') {
|
|
1450
|
-
const d = parseInt(String(indexArr[k + 1]), 10) || 0;
|
|
1451
|
-
if (d > 0)
|
|
1452
|
-
dim = d;
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (isVector && dim > 0)
|
|
1458
|
-
return dim;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
return 0;
|
|
1462
|
-
}
|
|
1463
1412
|
}
|
|
1464
1413
|
exports.SemanticCache = SemanticCache;
|
|
1465
1414
|
// --- Judge helpers ---
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/** SHA-256 hex digest of a string. */
|
|
2
2
|
export declare function sha256(text: string): string;
|
|
3
|
-
|
|
4
|
-
* Spaces are included because Valkey Search treats unescaped spaces as term
|
|
5
|
-
* separators (OR semantics), which would broaden the filter unintentionally.
|
|
6
|
-
*/
|
|
7
|
-
export declare function escapeTag(value: string): string;
|
|
3
|
+
export { escapeTag, encodeFloat32, parseFtSearchResponse } from '@betterdb/valkey-search-kit';
|
|
8
4
|
export type ContentBlock = TextBlock | BinaryBlock | ToolCallBlock | ToolResultBlock | ReasoningBlock;
|
|
9
5
|
export interface TextBlock {
|
|
10
6
|
type: 'text';
|
|
@@ -58,26 +54,3 @@ export declare function extractText(blocks: ContentBlock[]): string;
|
|
|
58
54
|
* Used for the binary_refs TAG field on cache entries.
|
|
59
55
|
*/
|
|
60
56
|
export declare function extractBinaryRefs(blocks: ContentBlock[]): string[];
|
|
61
|
-
/**
|
|
62
|
-
* Encode number[] as a little-endian Float32 Buffer.
|
|
63
|
-
* Used to store embeddings as binary HSET field values.
|
|
64
|
-
*/
|
|
65
|
-
export declare function encodeFloat32(vec: number[]): Buffer;
|
|
66
|
-
/**
|
|
67
|
-
* Parse a raw FT.SEARCH response from iovalkey's client.call().
|
|
68
|
-
*
|
|
69
|
-
* iovalkey returns FT.SEARCH results in the following shape:
|
|
70
|
-
* [totalCount, key1, [field1, val1, field2, val2, ...], key2, [...], ...]
|
|
71
|
-
*
|
|
72
|
-
* - totalCount is a string (e.g. "2")
|
|
73
|
-
* - Each key is a string
|
|
74
|
-
* - Each field list is a flat string array: [fieldName, value, fieldName, value, ...]
|
|
75
|
-
*
|
|
76
|
-
* Returns an array of { key: string, fields: Record<string, string> }.
|
|
77
|
-
* Returns [] if totalCount is "0" or the response is empty/malformed.
|
|
78
|
-
* Never throws — on any parse error, returns [].
|
|
79
|
-
*/
|
|
80
|
-
export declare function parseFtSearchResponse(raw: unknown): Array<{
|
|
81
|
-
key: string;
|
|
82
|
-
fields: Record<string, string>;
|
|
83
|
-
}>;
|
package/dist/utils.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFtSearchResponse = exports.encodeFloat32 = exports.escapeTag = void 0;
|
|
3
4
|
exports.sha256 = sha256;
|
|
4
|
-
exports.escapeTag = escapeTag;
|
|
5
5
|
exports.extractText = extractText;
|
|
6
6
|
exports.extractBinaryRefs = extractBinaryRefs;
|
|
7
|
-
exports.encodeFloat32 = encodeFloat32;
|
|
8
|
-
exports.parseFtSearchResponse = parseFtSearchResponse;
|
|
9
7
|
const node_crypto_1 = require("node:crypto");
|
|
10
8
|
/** SHA-256 hex digest of a string. */
|
|
11
9
|
function sha256(text) {
|
|
12
10
|
return (0, node_crypto_1.createHash)('sha256').update(text).digest('hex');
|
|
13
11
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function escapeTag(value) {
|
|
19
|
-
return value.replace(/[,.<>{}[\]"':;!@#$%^&*()\-+=~|/\\ ]/g, '\\$&');
|
|
20
|
-
}
|
|
12
|
+
var valkey_search_kit_1 = require("@betterdb/valkey-search-kit");
|
|
13
|
+
Object.defineProperty(exports, "escapeTag", { enumerable: true, get: function () { return valkey_search_kit_1.escapeTag; } });
|
|
14
|
+
Object.defineProperty(exports, "encodeFloat32", { enumerable: true, get: function () { return valkey_search_kit_1.encodeFloat32; } });
|
|
15
|
+
Object.defineProperty(exports, "parseFtSearchResponse", { enumerable: true, get: function () { return valkey_search_kit_1.parseFtSearchResponse; } });
|
|
21
16
|
/**
|
|
22
17
|
* Extract all text from a ContentBlock array, joining TextBlock.text values with a space.
|
|
23
18
|
* Used to derive the embedding text from a multi-modal prompt.
|
|
@@ -38,70 +33,3 @@ function extractBinaryRefs(blocks) {
|
|
|
38
33
|
.map((b) => b.ref)
|
|
39
34
|
.sort();
|
|
40
35
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Encode number[] as a little-endian Float32 Buffer.
|
|
43
|
-
* Used to store embeddings as binary HSET field values.
|
|
44
|
-
*/
|
|
45
|
-
function encodeFloat32(vec) {
|
|
46
|
-
const buf = Buffer.alloc(vec.length * 4);
|
|
47
|
-
for (let i = 0; i < vec.length; i++) {
|
|
48
|
-
buf.writeFloatLE(vec[i], i * 4);
|
|
49
|
-
}
|
|
50
|
-
return buf;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Parse a raw FT.SEARCH response from iovalkey's client.call().
|
|
54
|
-
*
|
|
55
|
-
* iovalkey returns FT.SEARCH results in the following shape:
|
|
56
|
-
* [totalCount, key1, [field1, val1, field2, val2, ...], key2, [...], ...]
|
|
57
|
-
*
|
|
58
|
-
* - totalCount is a string (e.g. "2")
|
|
59
|
-
* - Each key is a string
|
|
60
|
-
* - Each field list is a flat string array: [fieldName, value, fieldName, value, ...]
|
|
61
|
-
*
|
|
62
|
-
* Returns an array of { key: string, fields: Record<string, string> }.
|
|
63
|
-
* Returns [] if totalCount is "0" or the response is empty/malformed.
|
|
64
|
-
* Never throws — on any parse error, returns [].
|
|
65
|
-
*/
|
|
66
|
-
function parseFtSearchResponse(raw) {
|
|
67
|
-
try {
|
|
68
|
-
if (!Array.isArray(raw) || raw.length < 1) {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
const totalCount = typeof raw[0] === 'string' ? parseInt(raw[0], 10) : Number(raw[0]);
|
|
72
|
-
if (!totalCount || totalCount <= 0) {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
const results = [];
|
|
76
|
-
let i = 1;
|
|
77
|
-
while (i < raw.length) {
|
|
78
|
-
const key = raw[i];
|
|
79
|
-
if (typeof key !== 'string') {
|
|
80
|
-
i++;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const fieldList = raw[i + 1];
|
|
84
|
-
const fields = {};
|
|
85
|
-
if (Array.isArray(fieldList)) {
|
|
86
|
-
const len = fieldList.length - (fieldList.length % 2);
|
|
87
|
-
for (let j = 0; j < len; j += 2) {
|
|
88
|
-
const fieldName = String(fieldList[j]);
|
|
89
|
-
const fieldValue = String(fieldList[j + 1]);
|
|
90
|
-
fields[fieldName] = fieldValue;
|
|
91
|
-
}
|
|
92
|
-
i += 2;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
// No field list follows the key (e.g. RETURN 0 mode)
|
|
96
|
-
results.push({ key, fields });
|
|
97
|
-
i++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
results.push({ key, fields });
|
|
101
|
-
}
|
|
102
|
-
return results;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@betterdb/semantic-cache",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Valkey-native semantic cache for LLM applications with built-in OpenTelemetry and Prometheus instrumentation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"valkey",
|
|
@@ -93,18 +93,11 @@
|
|
|
93
93
|
"dist",
|
|
94
94
|
"README.md"
|
|
95
95
|
],
|
|
96
|
-
"scripts": {
|
|
97
|
-
"build": "tsc && node scripts/inject-telemetry-defaults.mjs",
|
|
98
|
-
"typecheck": "tsc --noEmit",
|
|
99
|
-
"test": "vitest run",
|
|
100
|
-
"test:watch": "vitest",
|
|
101
|
-
"clean": "rm -rf dist",
|
|
102
|
-
"update:pricing": "node scripts/update-model-prices.mjs"
|
|
103
|
-
},
|
|
104
96
|
"dependencies": {
|
|
105
97
|
"@opentelemetry/api": "^1.9.0",
|
|
106
98
|
"posthog-node": ">=4.0.0",
|
|
107
|
-
"prom-client": "^15.1.3"
|
|
99
|
+
"prom-client": "^15.1.3",
|
|
100
|
+
"@betterdb/valkey-search-kit": "0.1.0"
|
|
108
101
|
},
|
|
109
102
|
"engines": {
|
|
110
103
|
"node": ">=20.0.0"
|
|
@@ -137,5 +130,13 @@
|
|
|
137
130
|
"openai": {
|
|
138
131
|
"optional": true
|
|
139
132
|
}
|
|
133
|
+
},
|
|
134
|
+
"scripts": {
|
|
135
|
+
"build": "tsc && node scripts/inject-telemetry-defaults.mjs",
|
|
136
|
+
"typecheck": "tsc --noEmit",
|
|
137
|
+
"test": "vitest run",
|
|
138
|
+
"test:watch": "vitest",
|
|
139
|
+
"clean": "rm -rf dist",
|
|
140
|
+
"update:pricing": "node scripts/update-model-prices.mjs"
|
|
140
141
|
}
|
|
141
|
-
}
|
|
142
|
+
}
|