@betterdb/semantic-cache 0.6.0 → 0.8.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/analytics.d.ts +23 -2
- package/dist/analytics.js +98 -12
- package/dist/types.d.ts +0 -4
- 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/analytics.d.ts
CHANGED
|
@@ -11,14 +11,35 @@ export interface ValkeyLike {
|
|
|
11
11
|
export interface Analytics {
|
|
12
12
|
init(client: ValkeyLike, name: string, configProps?: Record<string, unknown>): Promise<void>;
|
|
13
13
|
capture(event: string, properties?: Record<string, unknown>): void;
|
|
14
|
+
flush(): Promise<void>;
|
|
14
15
|
shutdown(): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
export interface AnalyticsOptions {
|
|
17
|
-
apiKey?: string;
|
|
18
|
-
host?: string;
|
|
19
18
|
disabled?: boolean;
|
|
20
19
|
/** Interval in ms for periodic stats snapshots. Default: 300_000 (5 min). 0 to disable. */
|
|
21
20
|
statsIntervalMs?: number;
|
|
22
21
|
}
|
|
23
22
|
export declare const NOOP_ANALYTICS: Analytics;
|
|
23
|
+
type PostHogClient = {
|
|
24
|
+
capture: (opts: {
|
|
25
|
+
distinctId?: string;
|
|
26
|
+
event: string;
|
|
27
|
+
properties?: Record<string, unknown>;
|
|
28
|
+
}) => void;
|
|
29
|
+
flush: () => Promise<void>;
|
|
30
|
+
shutdown: () => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
export declare class PostHogAnalytics implements Analytics {
|
|
33
|
+
private posthog;
|
|
34
|
+
private distinctId;
|
|
35
|
+
private deploymentId;
|
|
36
|
+
private readonly flushOnExit;
|
|
37
|
+
constructor(posthog: PostHogClient);
|
|
38
|
+
init(client: ValkeyLike, name: string, configProps?: Record<string, unknown>): Promise<void>;
|
|
39
|
+
private resolveDeploymentId;
|
|
40
|
+
capture(event: string, properties?: Record<string, unknown>): void;
|
|
41
|
+
flush(): Promise<void>;
|
|
42
|
+
shutdown(): Promise<void>;
|
|
43
|
+
}
|
|
24
44
|
export declare function createAnalytics(opts?: AnalyticsOptions): Promise<Analytics>;
|
|
45
|
+
export {};
|
package/dist/analytics.js
CHANGED
|
@@ -39,8 +39,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.NOOP_ANALYTICS = void 0;
|
|
42
|
+
exports.PostHogAnalytics = exports.NOOP_ANALYTICS = void 0;
|
|
43
43
|
exports.createAnalytics = createAnalytics;
|
|
44
|
+
const node_fs_1 = require("node:fs");
|
|
45
|
+
const node_os_1 = require("node:os");
|
|
46
|
+
const node_path_1 = require("node:path");
|
|
44
47
|
const EVENT_PREFIX = 'semantic_cache:';
|
|
45
48
|
// Build-time placeholders — replaced by scripts/inject-telemetry-defaults.mjs
|
|
46
49
|
// When the placeholder is NOT replaced, the startsWith('__') guard treats it as unset.
|
|
@@ -49,41 +52,126 @@ const BAKED_POSTHOG_HOST = '__BETTERDB_POSTHOG_HOST__';
|
|
|
49
52
|
exports.NOOP_ANALYTICS = {
|
|
50
53
|
async init() { },
|
|
51
54
|
capture() { },
|
|
55
|
+
async flush() { },
|
|
52
56
|
async shutdown() { },
|
|
53
57
|
};
|
|
54
58
|
function isTelemetryOptedOut() {
|
|
55
59
|
const val = process.env.BETTERDB_TELEMETRY;
|
|
56
60
|
return val !== undefined && ['false', '0', 'no', 'off'].includes(val.toLowerCase());
|
|
57
61
|
}
|
|
62
|
+
const INSTALL_ID_ENV = 'BETTERDB_INSTANCE_ID';
|
|
63
|
+
// Holds a minted id for the rest of the process when persistence fails, so
|
|
64
|
+
// repeated calls (or parallel init) return one stable ephemeral identity.
|
|
65
|
+
let ephemeralInstallId;
|
|
66
|
+
function installIdPath() {
|
|
67
|
+
const base = process.env.XDG_STATE_HOME;
|
|
68
|
+
const root = base ? base : (0, node_path_1.join)((0, node_os_1.homedir)(), '.betterdb');
|
|
69
|
+
return (0, node_path_1.join)(root, 'instance_id');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Stable per-install identity for product analytics. Persisted on the local
|
|
73
|
+
* machine (not in Valkey), so a fleet of processes sharing one Valkey is
|
|
74
|
+
* counted as many installs rather than collapsing to one. Pin it via
|
|
75
|
+
* BETTERDB_INSTANCE_ID for ephemeral containers that would otherwise mint a
|
|
76
|
+
* fresh id every run. Falls back to an ephemeral per-process id when no
|
|
77
|
+
* writable location is available.
|
|
78
|
+
*/
|
|
79
|
+
function getInstallId() {
|
|
80
|
+
const override = process.env[INSTALL_ID_ENV];
|
|
81
|
+
if (override)
|
|
82
|
+
return override;
|
|
83
|
+
const path = installIdPath();
|
|
84
|
+
try {
|
|
85
|
+
const existing = (0, node_fs_1.readFileSync)(path, 'utf8').trim();
|
|
86
|
+
if (existing)
|
|
87
|
+
return existing;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// no existing id
|
|
91
|
+
}
|
|
92
|
+
const newId = ephemeralInstallId ?? crypto.randomUUID();
|
|
93
|
+
try {
|
|
94
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
95
|
+
(0, node_fs_1.writeFileSync)(path, newId);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Persistence failed — hold the id for the rest of this process so
|
|
99
|
+
// repeated calls return a stable ephemeral identity.
|
|
100
|
+
ephemeralInstallId = newId;
|
|
101
|
+
}
|
|
102
|
+
return newId;
|
|
103
|
+
}
|
|
58
104
|
class PostHogAnalytics {
|
|
59
105
|
posthog;
|
|
60
106
|
distinctId = '';
|
|
107
|
+
deploymentId = '';
|
|
108
|
+
// Library consumers are frequently short-lived scripts that never call
|
|
109
|
+
// shutdown(), so PostHog's buffered events (flushAt=20, flushInterval=10s)
|
|
110
|
+
// would be dropped when the process exits before the queue drains. Flush
|
|
111
|
+
// when the event loop empties so lifecycle events are actually delivered.
|
|
112
|
+
// Only enabled instances reach here — the opt-out path returns
|
|
113
|
+
// NOOP_ANALYTICS and registers nothing, keeping disabled consumers silent.
|
|
114
|
+
flushOnExit = () => {
|
|
115
|
+
void this.flush();
|
|
116
|
+
};
|
|
61
117
|
constructor(posthog) {
|
|
62
118
|
this.posthog = posthog;
|
|
119
|
+
process.once('beforeExit', this.flushOnExit);
|
|
63
120
|
}
|
|
64
121
|
async init(client, name, configProps) {
|
|
122
|
+
this.distinctId = getInstallId();
|
|
123
|
+
this.deploymentId = await this.resolveDeploymentId(client, name);
|
|
124
|
+
const merged = { ...(configProps ?? {}) };
|
|
125
|
+
if (this.deploymentId)
|
|
126
|
+
merged.deployment_id = this.deploymentId;
|
|
127
|
+
this.capture('cache_init', merged);
|
|
128
|
+
// Flush the start event immediately so it lands even for processes that exit
|
|
129
|
+
// before the flush interval or the beforeExit hook fires.
|
|
130
|
+
await this.flush();
|
|
131
|
+
}
|
|
132
|
+
async resolveDeploymentId(client, name) {
|
|
133
|
+
// The Valkey-scoped id groups all clients pointed at the same store, so a
|
|
134
|
+
// shared-Valkey fleet can still be rolled up into one deployment.
|
|
65
135
|
const idKey = `${name}:__instance_id`;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
136
|
+
try {
|
|
137
|
+
const existing = await client.get(idKey);
|
|
138
|
+
if (existing)
|
|
139
|
+
return existing;
|
|
140
|
+
const id = crypto.randomUUID();
|
|
69
141
|
await client.set(idKey, id);
|
|
142
|
+
return id;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return '';
|
|
70
146
|
}
|
|
71
|
-
this.distinctId = id;
|
|
72
|
-
this.capture('cache_init', configProps);
|
|
73
147
|
}
|
|
74
148
|
capture(event, properties) {
|
|
75
149
|
try {
|
|
150
|
+
const props = { ...(properties ?? {}) };
|
|
151
|
+
if (this.deploymentId && props.deployment_id === undefined) {
|
|
152
|
+
props.deployment_id = this.deploymentId;
|
|
153
|
+
}
|
|
76
154
|
this.posthog.capture({
|
|
77
155
|
distinctId: this.distinctId,
|
|
78
156
|
event: `${EVENT_PREFIX}${event}`,
|
|
79
|
-
properties,
|
|
157
|
+
properties: props,
|
|
80
158
|
});
|
|
81
159
|
}
|
|
82
160
|
catch {
|
|
83
161
|
// never throw from analytics
|
|
84
162
|
}
|
|
85
163
|
}
|
|
164
|
+
async flush() {
|
|
165
|
+
try {
|
|
166
|
+
await this.posthog.flush();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// swallow
|
|
170
|
+
}
|
|
171
|
+
}
|
|
86
172
|
async shutdown() {
|
|
173
|
+
// Explicit shutdown supersedes the beforeExit backstop.
|
|
174
|
+
process.removeListener('beforeExit', this.flushOnExit);
|
|
87
175
|
try {
|
|
88
176
|
await this.posthog.shutdown();
|
|
89
177
|
}
|
|
@@ -92,18 +180,16 @@ class PostHogAnalytics {
|
|
|
92
180
|
}
|
|
93
181
|
}
|
|
94
182
|
}
|
|
183
|
+
exports.PostHogAnalytics = PostHogAnalytics;
|
|
95
184
|
async function createAnalytics(opts) {
|
|
96
185
|
if (opts?.disabled || isTelemetryOptedOut()) {
|
|
97
186
|
return exports.NOOP_ANALYTICS;
|
|
98
187
|
}
|
|
99
|
-
|
|
100
|
-
const bakedKey = BAKED_POSTHOG_API_KEY.startsWith('__') ? undefined : BAKED_POSTHOG_API_KEY;
|
|
101
|
-
const apiKey = opts?.apiKey ?? process.env.BETTERDB_POSTHOG_API_KEY ?? bakedKey;
|
|
188
|
+
const apiKey = BAKED_POSTHOG_API_KEY.startsWith('__') ? undefined : BAKED_POSTHOG_API_KEY;
|
|
102
189
|
if (!apiKey) {
|
|
103
190
|
return exports.NOOP_ANALYTICS;
|
|
104
191
|
}
|
|
105
|
-
const
|
|
106
|
-
const host = opts?.host ?? process.env.BETTERDB_POSTHOG_HOST ?? bakedHost;
|
|
192
|
+
const host = BAKED_POSTHOG_HOST.startsWith('__') ? undefined : BAKED_POSTHOG_HOST;
|
|
107
193
|
try {
|
|
108
194
|
// @ts-ignore — posthog-node is an optional peer dep
|
|
109
195
|
const { PostHog } = await Promise.resolve().then(() => __importStar(require('posthog-node')));
|
package/dist/types.d.ts
CHANGED
|
@@ -90,10 +90,6 @@ export interface SemanticCacheOptions {
|
|
|
90
90
|
registry?: Registry;
|
|
91
91
|
};
|
|
92
92
|
analytics?: {
|
|
93
|
-
/** PostHog API key. Overrides the build-time baked key if set. */
|
|
94
|
-
apiKey?: string;
|
|
95
|
-
/** PostHog host. Overrides the build-time baked host if set. */
|
|
96
|
-
host?: string;
|
|
97
93
|
/** Disable analytics. Also controlled by BETTERDB_TELEMETRY env var. */
|
|
98
94
|
disabled?: boolean;
|
|
99
95
|
/** Interval in ms for periodic stats snapshots. Default: 300_000 (5 min). 0 to disable. */
|
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.8.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
|
+
}
|