@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 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.
@@ -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;
@@ -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 (!this.isIndexNotFoundError(err)) {
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 info = raw;
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 = this.parseDimensionFromInfo(info);
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 (!this.isIndexNotFoundError(err)) {
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
- /** Escape a string for safe use as a Valkey Search TAG filter value.
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
- /** Escape a string for safe use as a Valkey Search TAG filter value.
15
- * Spaces are included because Valkey Search treats unescaped spaces as term
16
- * separators (OR semantics), which would broaden the filter unintentionally.
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.6.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
+ }