@betterdb/semantic-cache 0.1.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +233 -124
  2. package/dist/SemanticCache.d.ts +127 -7
  3. package/dist/SemanticCache.js +867 -48
  4. package/dist/adapters/ai.js +6 -1
  5. package/dist/adapters/anthropic.d.ts +32 -0
  6. package/dist/adapters/anthropic.js +94 -0
  7. package/dist/adapters/langchain.js +6 -1
  8. package/dist/adapters/langgraph.d.ts +104 -0
  9. package/dist/adapters/langgraph.js +271 -0
  10. package/dist/adapters/llamaindex.d.ts +32 -0
  11. package/dist/adapters/llamaindex.js +76 -0
  12. package/dist/adapters/openai-responses.d.ts +31 -0
  13. package/dist/adapters/openai-responses.js +112 -0
  14. package/dist/adapters/openai.d.ts +42 -0
  15. package/dist/adapters/openai.js +97 -0
  16. package/dist/analytics.d.ts +24 -0
  17. package/dist/analytics.js +116 -0
  18. package/dist/cluster.d.ts +10 -0
  19. package/dist/cluster.js +43 -0
  20. package/dist/defaultCostTable.d.ts +11 -0
  21. package/dist/defaultCostTable.js +1976 -0
  22. package/dist/discovery.d.ts +67 -0
  23. package/dist/discovery.js +140 -0
  24. package/dist/embed/bedrock.d.ts +32 -0
  25. package/dist/embed/bedrock.js +109 -0
  26. package/dist/embed/cohere.d.ts +34 -0
  27. package/dist/embed/cohere.js +37 -0
  28. package/dist/embed/ollama.d.ts +30 -0
  29. package/dist/embed/ollama.js +24 -0
  30. package/dist/embed/openai.d.ts +31 -0
  31. package/dist/embed/openai.js +66 -0
  32. package/dist/embed/voyage.d.ts +31 -0
  33. package/dist/embed/voyage.js +32 -0
  34. package/dist/index.d.ts +8 -1
  35. package/dist/index.js +13 -1
  36. package/dist/normalizer.d.ts +68 -0
  37. package/dist/normalizer.js +102 -0
  38. package/dist/telemetry.d.ts +5 -0
  39. package/dist/telemetry.js +30 -0
  40. package/dist/types.d.ts +128 -7
  41. package/dist/utils.d.ts +58 -0
  42. package/dist/utils.js +30 -0
  43. package/package.json +81 -6
@@ -69,10 +69,15 @@ function createSemanticCacheMiddleware(opts) {
69
69
  // LanguageModelV3GenerateResult is imported transitively via the
70
70
  // LanguageModelMiddleware type — we construct it inline to avoid
71
71
  // depending on @ai-sdk/provider directly.
72
+ //
73
+ // AI SDK v6 (specificationVersion v3) uses inputTokens/outputTokens
74
+ // (not promptTokens/completionTokens). request is required to avoid
75
+ // "Cannot read properties of undefined" in the SDK pipeline.
72
76
  return {
73
77
  content: [{ type: 'text', text: cached.response }],
74
78
  finishReason: 'stop',
75
- usage: { promptTokens: 0, completionTokens: 0 },
79
+ usage: { inputTokens: 0, outputTokens: 0 },
80
+ request: { body: '' },
76
81
  warnings: [],
77
82
  };
78
83
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Anthropic Messages API adapter for @betterdb/semantic-cache.
3
+ *
4
+ * Extracts the text to embed from Anthropic Messages API request params.
5
+ * Semantic caching keys on the last user message's text content because that
6
+ * is the actual query. See openai.ts for the full rationale.
7
+ *
8
+ * Usage:
9
+ * import { prepareSemanticParams } from '@betterdb/semantic-cache/anthropic';
10
+ * const { text, model } = await prepareSemanticParams(params);
11
+ * const result = await cache.check(text);
12
+ */
13
+ import type { MessageCreateParamsNonStreaming } from '@anthropic-ai/sdk/resources';
14
+ import type { BinaryBlock, TextBlock } from '../utils';
15
+ import type { BinaryNormalizer } from '../normalizer';
16
+ export interface AnthropicSemanticPrepareOptions {
17
+ /** Binary content normalizer. Default: passthrough. */
18
+ normalizer?: BinaryNormalizer;
19
+ }
20
+ export interface SemanticParams {
21
+ text: string;
22
+ blocks?: (TextBlock | BinaryBlock)[];
23
+ model?: string;
24
+ }
25
+ /**
26
+ * Extract semantic cache params from Anthropic Messages API request params.
27
+ *
28
+ * Extracts the last user message text for semantic similarity matching.
29
+ * The system prompt is not included in the cache key because it rarely changes
30
+ * within a deployment and would prevent hits across conversations.
31
+ */
32
+ export declare function prepareSemanticParams(params: MessageCreateParamsNonStreaming, opts?: AnthropicSemanticPrepareOptions): Promise<SemanticParams>;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareSemanticParams = prepareSemanticParams;
4
+ const normalizer_1 = require("../normalizer");
5
+ async function normalizeBlock(block, normalizer) {
6
+ const type = block.type;
7
+ if (type === 'text') {
8
+ const b = block;
9
+ return { type: 'text', text: b.text };
10
+ }
11
+ if (type === 'image') {
12
+ const b = block;
13
+ const src = b.source;
14
+ let source;
15
+ let mediaType = 'image/*';
16
+ if (src.type === 'base64') {
17
+ source = { type: 'base64', data: src.data };
18
+ mediaType = src.media_type ?? 'image/*';
19
+ }
20
+ else if (src.type === 'url') {
21
+ source = { type: 'url', url: src.url };
22
+ }
23
+ else if (src.type === 'file') {
24
+ source = { type: 'fileId', fileId: src.file_id, provider: 'anthropic' };
25
+ }
26
+ else {
27
+ return null;
28
+ }
29
+ const ref = await normalizer({ kind: 'image', source });
30
+ return { type: 'binary', kind: 'image', mediaType, ref };
31
+ }
32
+ if (type === 'document') {
33
+ const b = block;
34
+ const src = b.source;
35
+ let source;
36
+ let mediaType = 'application/octet-stream';
37
+ if (src.type === 'base64') {
38
+ source = { type: 'base64', data: src.data };
39
+ mediaType = src.media_type ?? 'application/pdf';
40
+ }
41
+ else if (src.type === 'text') {
42
+ const encoded = Buffer.from(src.text).toString('base64');
43
+ source = { type: 'base64', data: encoded };
44
+ mediaType = 'text/plain';
45
+ }
46
+ else if (src.type === 'url') {
47
+ source = { type: 'url', url: src.url };
48
+ mediaType = 'application/pdf';
49
+ }
50
+ else if (src.type === 'file') {
51
+ source = { type: 'fileId', fileId: src.file_id, provider: 'anthropic' };
52
+ }
53
+ else {
54
+ return null;
55
+ }
56
+ const ref = await normalizer({ kind: 'document', source });
57
+ return { type: 'binary', kind: 'document', mediaType, ref };
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Extract semantic cache params from Anthropic Messages API request params.
63
+ *
64
+ * Extracts the last user message text for semantic similarity matching.
65
+ * The system prompt is not included in the cache key because it rarely changes
66
+ * within a deployment and would prevent hits across conversations.
67
+ */
68
+ async function prepareSemanticParams(params, opts) {
69
+ const normalizer = opts?.normalizer ?? normalizer_1.defaultNormalizer;
70
+ // Find last user message
71
+ const userMessages = params.messages.filter((m) => m.role === 'user');
72
+ if (userMessages.length === 0) {
73
+ return { text: '', model: params.model };
74
+ }
75
+ const lastUser = userMessages[userMessages.length - 1];
76
+ const content = lastUser.content;
77
+ if (typeof content === 'string') {
78
+ return { text: content, model: params.model };
79
+ }
80
+ if (Array.isArray(content)) {
81
+ const blocks = [];
82
+ for (const part of content) {
83
+ const block = await normalizeBlock(part, normalizer);
84
+ if (block)
85
+ blocks.push(block);
86
+ }
87
+ const text = blocks
88
+ .filter((b) => b.type === 'text')
89
+ .map((b) => b.text)
90
+ .join(' ');
91
+ return { text, blocks, model: params.model };
92
+ }
93
+ return { text: '', model: params.model };
94
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BetterDBSemanticCache = void 0;
4
4
  const caches_1 = require("@langchain/core/caches");
5
+ const messages_1 = require("@langchain/core/messages");
5
6
  const utils_1 = require("../utils");
6
7
  class BetterDBSemanticCache extends caches_1.BaseCache {
7
8
  cache;
@@ -35,7 +36,11 @@ class BetterDBSemanticCache extends caches_1.BaseCache {
35
36
  const result = await this.cache.check(prompt, opts);
36
37
  if (!result.hit || !result.response)
37
38
  return null;
38
- return [{ text: result.response }];
39
+ // Return a ChatGeneration-shaped object with a proper AIMessage so that
40
+ // ChatOpenAI and other chat models can unwrap it correctly on cache hit.
41
+ // Plain { text } without a message causes "Cannot read properties of undefined"
42
+ // when the model tries to access response.content.
43
+ return [{ text: result.response, message: new messages_1.AIMessage(result.response) }];
39
44
  }
40
45
  async update(prompt, llm_string, return_val) {
41
46
  await this.ensureInitialized();
@@ -0,0 +1,104 @@
1
+ /**
2
+ * LangGraph BaseStore adapter for @betterdb/semantic-cache.
3
+ *
4
+ * BetterDBSemanticStore implements the LangGraph BaseStore interface,
5
+ * enabling similarity-based memory retrieval from a SemanticCache instance.
6
+ *
7
+ * When to use this vs @betterdb/agent-cache/langgraph:
8
+ * - Use @betterdb/agent-cache/langgraph (BetterDBSaver) for exact-match checkpoint
9
+ * persistence - storing and retrieving agent state snapshots by checkpoint ID.
10
+ * - Use BetterDBSemanticStore (this class) for similarity-based memory retrieval -
11
+ * finding the most semantically relevant past observations, documents, or facts
12
+ * for a given query.
13
+ * They can coexist on the same Valkey instance with different key prefixes.
14
+ *
15
+ * Storage layout:
16
+ * {name}:entry:{uuid} - HSET entry per item (via SemanticCache.store)
17
+ * namespace is stored as the 'category' tag on entries for filtered recall
18
+ *
19
+ * Limitations:
20
+ * - get() uses a Valkey SCAN for the deterministic key and may be slow on large stores.
21
+ * For high-frequency get() patterns, prefer a dedicated hash store (agent-cache session tier).
22
+ * - delete() uses invalidate() which is limited to 1000 entries per call and requires
23
+ * FT.SEARCH to find matching keys.
24
+ */
25
+ import type { SemanticCache } from '../SemanticCache';
26
+ /** A stored item in the semantic memory store. */
27
+ export interface Item {
28
+ /** Dot-separated namespace path (e.g. ['user', 'alice', 'memories']). */
29
+ namespace: string[];
30
+ /** Unique key within the namespace. */
31
+ key: string;
32
+ /** The stored value. */
33
+ value: Record<string, unknown>;
34
+ /** Creation timestamp as ISO string. */
35
+ createdAt: string;
36
+ /** Last update timestamp as ISO string. */
37
+ updatedAt: string;
38
+ }
39
+ /** Options for the search() method. */
40
+ export interface SearchOptions {
41
+ /** Natural-language query for similarity search. */
42
+ query?: string;
43
+ /** Maximum number of results. Default: 10. */
44
+ limit?: number;
45
+ /** Similarity threshold override (cosine distance, 0-2). */
46
+ threshold?: number;
47
+ }
48
+ export interface BetterDBSemanticStoreOptions {
49
+ /**
50
+ * A pre-configured SemanticCache instance.
51
+ * The cache must be initialized before calling store methods.
52
+ */
53
+ cache: SemanticCache;
54
+ /**
55
+ * Field to embed from stored values when no explicit query is provided.
56
+ * Default: 'content'. The value of this field (if a string) is used as
57
+ * the embedding text when put() is called without an explicit embed field.
58
+ */
59
+ embedField?: string;
60
+ }
61
+ /**
62
+ * LangGraph-compatible semantic memory store backed by SemanticCache.
63
+ *
64
+ * Implements a subset of the LangGraph BaseStore interface sufficient for
65
+ * use as an in-memory store with similarity recall.
66
+ */
67
+ export declare class BetterDBSemanticStore {
68
+ private readonly cache;
69
+ private readonly embedField;
70
+ constructor(opts: BetterDBSemanticStoreOptions);
71
+ /**
72
+ * Store a value at namespace/key.
73
+ * The embedField value (if present and a string) is used as the embedding text.
74
+ * Falls back to JSON stringified value if embedField is absent.
75
+ */
76
+ put(namespace: string[], key: string, value: Record<string, unknown>): Promise<void>;
77
+ /**
78
+ * Retrieve a value by exact namespace and key.
79
+ * Paginates through all entries in the namespace using stable SORTBY ordering.
80
+ */
81
+ get(namespace: string[], key: string): Promise<Item | null>;
82
+ /**
83
+ * Search the namespace using similarity.
84
+ * When query is provided, embeds it and returns the k most similar entries.
85
+ * When query is absent, returns all entries in the namespace (up to limit).
86
+ */
87
+ search(namespace: string[], options?: SearchOptions): Promise<Item[]>;
88
+ /**
89
+ * Delete the specific entry at namespace/key.
90
+ * Pages through the namespace using stable SORTBY ordering; adjusts the offset
91
+ * for each deleted entry so no entries are skipped in large namespaces.
92
+ */
93
+ delete(namespace: string[], key: string): Promise<void>;
94
+ /**
95
+ * Batch put/delete multiple items.
96
+ * Executes sequentially to avoid races when the same (namespace, key) appears
97
+ * more than once: concurrent delete+put pairs can interleave and leave duplicates.
98
+ */
99
+ batch(writes: Array<{
100
+ namespace: string[];
101
+ key: string;
102
+ value: Record<string, unknown> | null;
103
+ }>): Promise<void>;
104
+ }
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ /**
3
+ * LangGraph BaseStore adapter for @betterdb/semantic-cache.
4
+ *
5
+ * BetterDBSemanticStore implements the LangGraph BaseStore interface,
6
+ * enabling similarity-based memory retrieval from a SemanticCache instance.
7
+ *
8
+ * When to use this vs @betterdb/agent-cache/langgraph:
9
+ * - Use @betterdb/agent-cache/langgraph (BetterDBSaver) for exact-match checkpoint
10
+ * persistence - storing and retrieving agent state snapshots by checkpoint ID.
11
+ * - Use BetterDBSemanticStore (this class) for similarity-based memory retrieval -
12
+ * finding the most semantically relevant past observations, documents, or facts
13
+ * for a given query.
14
+ * They can coexist on the same Valkey instance with different key prefixes.
15
+ *
16
+ * Storage layout:
17
+ * {name}:entry:{uuid} - HSET entry per item (via SemanticCache.store)
18
+ * namespace is stored as the 'category' tag on entries for filtered recall
19
+ *
20
+ * Limitations:
21
+ * - get() uses a Valkey SCAN for the deterministic key and may be slow on large stores.
22
+ * For high-frequency get() patterns, prefer a dedicated hash store (agent-cache session tier).
23
+ * - delete() uses invalidate() which is limited to 1000 entries per call and requires
24
+ * FT.SEARCH to find matching keys.
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.BetterDBSemanticStore = void 0;
61
+ const utils_1 = require("../utils");
62
+ function namespaceKey(namespace) {
63
+ return namespace.join(':');
64
+ }
65
+ function namespaceToCategory(namespace) {
66
+ // Replace path separators (. and /) only, leaving : intact as the namespace-segment
67
+ // separator. Matches the Python implementation for cross-language compatibility.
68
+ return namespaceKey(namespace).replace(/[./]/g, '_');
69
+ }
70
+ /**
71
+ * LangGraph-compatible semantic memory store backed by SemanticCache.
72
+ *
73
+ * Implements a subset of the LangGraph BaseStore interface sufficient for
74
+ * use as an in-memory store with similarity recall.
75
+ */
76
+ class BetterDBSemanticStore {
77
+ cache;
78
+ embedField;
79
+ constructor(opts) {
80
+ this.cache = opts.cache;
81
+ this.embedField = opts.embedField ?? 'content';
82
+ }
83
+ /**
84
+ * Store a value at namespace/key.
85
+ * The embedField value (if present and a string) is used as the embedding text.
86
+ * Falls back to JSON stringified value if embedField is absent.
87
+ */
88
+ async put(namespace, key, value) {
89
+ // Upsert: remove any existing entry for this key before writing so repeated
90
+ // put() calls don't accumulate stale duplicates.
91
+ await this.delete(namespace, key);
92
+ const embedText = typeof value[this.embedField] === 'string'
93
+ ? value[this.embedField]
94
+ : JSON.stringify(value);
95
+ const category = namespaceToCategory(namespace);
96
+ const now = new Date().toISOString();
97
+ const item = {
98
+ namespace,
99
+ key,
100
+ value,
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ };
104
+ await this.cache.store(embedText, JSON.stringify(item), {
105
+ category,
106
+ metadata: { key, namespace: namespaceKey(namespace) },
107
+ });
108
+ }
109
+ /**
110
+ * Retrieve a value by exact namespace and key.
111
+ * Paginates through all entries in the namespace using stable SORTBY ordering.
112
+ */
113
+ async get(namespace, key) {
114
+ const category = namespaceToCategory(namespace);
115
+ const catFilter = `@category:{${(0, utils_1.escapeTag)(category)}}`;
116
+ const { parseFtSearchResponse } = await Promise.resolve().then(() => __importStar(require('../utils')));
117
+ const BATCH = 100;
118
+ let offset = 0;
119
+ while (true) {
120
+ let raw;
121
+ try {
122
+ raw = await this.cache._searchEntries(catFilter, BATCH, offset);
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ const entries = parseFtSearchResponse(raw);
128
+ if (entries.length === 0)
129
+ break;
130
+ for (const entry of entries) {
131
+ const responseStr = entry.fields['response'];
132
+ if (!responseStr)
133
+ continue;
134
+ try {
135
+ const item = JSON.parse(responseStr);
136
+ if (item.key === key)
137
+ return item;
138
+ }
139
+ catch { /* skip corrupt */ }
140
+ }
141
+ if (entries.length < BATCH)
142
+ break;
143
+ offset += BATCH;
144
+ }
145
+ return null;
146
+ }
147
+ /**
148
+ * Search the namespace using similarity.
149
+ * When query is provided, embeds it and returns the k most similar entries.
150
+ * When query is absent, returns all entries in the namespace (up to limit).
151
+ */
152
+ async search(namespace, options) {
153
+ const limit = options?.limit ?? 10;
154
+ const query = options?.query;
155
+ const category = namespaceToCategory(namespace);
156
+ if (query) {
157
+ const { encodeFloat32, parseFtSearchResponse } = await Promise.resolve().then(() => __importStar(require('../utils')));
158
+ const threshold = options?.threshold ?? this.cache._defaultThreshold;
159
+ const { vector } = await this.cache._embedText(query);
160
+ const filterExpr = `(@category:{${(0, utils_1.escapeTag)(category)}})`;
161
+ const knnQuery = `${filterExpr}=>[KNN ${limit} @embedding $vec AS __score]`;
162
+ let raw;
163
+ try {
164
+ raw = await this.cache.client.call('FT.SEARCH', this.cache.indexName, knnQuery, 'PARAMS', '2', 'vec', encodeFloat32(vector), 'LIMIT', '0', String(limit), 'DIALECT', '2');
165
+ }
166
+ catch {
167
+ return [];
168
+ }
169
+ const items = [];
170
+ for (const entry of parseFtSearchResponse(raw)) {
171
+ const scoreVal = parseFloat(entry.fields['__score'] ?? 'NaN');
172
+ if (isNaN(scoreVal) || scoreVal > threshold)
173
+ continue;
174
+ const responseStr = entry.fields['response'];
175
+ if (responseStr) {
176
+ try {
177
+ items.push(JSON.parse(responseStr));
178
+ }
179
+ catch { /* skip */ }
180
+ }
181
+ }
182
+ return items;
183
+ }
184
+ // No query — return all entries in namespace (up to limit) using _searchEntries
185
+ const { parseFtSearchResponse } = await Promise.resolve().then(() => __importStar(require('../utils')));
186
+ try {
187
+ const result = await this.cache._searchEntries(`@category:{${(0, utils_1.escapeTag)(category)}}`, limit, 0);
188
+ const items = [];
189
+ for (const entry of parseFtSearchResponse(result)) {
190
+ const responseStr = entry.fields['response'];
191
+ if (responseStr) {
192
+ try {
193
+ items.push(JSON.parse(responseStr));
194
+ }
195
+ catch { /* skip */ }
196
+ }
197
+ }
198
+ return items;
199
+ }
200
+ catch {
201
+ return [];
202
+ }
203
+ }
204
+ /**
205
+ * Delete the specific entry at namespace/key.
206
+ * Pages through the namespace using stable SORTBY ordering; adjusts the offset
207
+ * for each deleted entry so no entries are skipped in large namespaces.
208
+ */
209
+ async delete(namespace, key) {
210
+ const category = namespaceToCategory(namespace);
211
+ const catFilter = `@category:{${(0, utils_1.escapeTag)(category)}}`;
212
+ const client = this.cache.client;
213
+ const { parseFtSearchResponse } = await Promise.resolve().then(() => __importStar(require('../utils')));
214
+ const BATCH = 100;
215
+ let offset = 0;
216
+ while (true) {
217
+ let raw;
218
+ try {
219
+ raw = await this.cache._searchEntries(catFilter, BATCH, offset);
220
+ }
221
+ catch {
222
+ return;
223
+ }
224
+ const entries = parseFtSearchResponse(raw);
225
+ if (entries.length === 0)
226
+ break;
227
+ let deletedCount = 0;
228
+ for (const entry of entries) {
229
+ const responseStr = entry.fields['response'];
230
+ if (!responseStr)
231
+ continue;
232
+ try {
233
+ const item = JSON.parse(responseStr);
234
+ if (item.key === key) {
235
+ await client.del(entry.key).catch(() => { });
236
+ deletedCount++;
237
+ }
238
+ }
239
+ catch { /* skip corrupt */ }
240
+ }
241
+ if (deletedCount > 0) {
242
+ // After deletions, Valkey Search async indexing may keep deleted entries
243
+ // visible briefly — offset arithmetic is unreliable. Re-scan from 0 to
244
+ // catch any remaining copies (mirrors the Python implementation).
245
+ offset = 0;
246
+ }
247
+ else if (entries.length < BATCH) {
248
+ break; // Last page with no matches — done
249
+ }
250
+ else {
251
+ offset += BATCH; // No deletions this page, advance safely
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Batch put/delete multiple items.
257
+ * Executes sequentially to avoid races when the same (namespace, key) appears
258
+ * more than once: concurrent delete+put pairs can interleave and leave duplicates.
259
+ */
260
+ async batch(writes) {
261
+ for (const w of writes) {
262
+ if (w.value === null) {
263
+ await this.delete(w.namespace, w.key);
264
+ }
265
+ else {
266
+ await this.put(w.namespace, w.key, w.value);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ exports.BetterDBSemanticStore = BetterDBSemanticStore;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * LlamaIndex adapter for @betterdb/semantic-cache.
3
+ *
4
+ * Extracts the text to embed from LlamaIndex ChatMessage arrays.
5
+ * Semantic caching keys on the last user message's text content.
6
+ * See openai.ts for the full rationale.
7
+ *
8
+ * Usage:
9
+ * import { prepareSemanticParams } from '@betterdb/semantic-cache/llamaindex';
10
+ * const { text, model } = prepareSemanticParams(messages, { model: 'gpt-4o' });
11
+ * const result = await cache.check(text);
12
+ */
13
+ import type { ChatMessage } from '@llamaindex/core/llms';
14
+ import type { BinaryBlock, TextBlock } from '../utils';
15
+ import type { BinaryNormalizer } from '../normalizer';
16
+ export interface LlamaIndexSemanticPrepareOptions {
17
+ /** Model name to include in the returned SemanticParams. */
18
+ model?: string;
19
+ /** Binary content normalizer. Default: passthrough. */
20
+ normalizer?: BinaryNormalizer;
21
+ }
22
+ export interface SemanticParams {
23
+ text: string;
24
+ blocks?: (TextBlock | BinaryBlock)[];
25
+ model?: string;
26
+ }
27
+ /**
28
+ * Extract semantic cache params from a LlamaIndex ChatMessage array.
29
+ *
30
+ * Extracts the last user message for semantic similarity matching.
31
+ */
32
+ export declare function prepareSemanticParams(messages: ChatMessage[], opts?: LlamaIndexSemanticPrepareOptions): Promise<SemanticParams>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareSemanticParams = prepareSemanticParams;
4
+ const normalizer_1 = require("../normalizer");
5
+ async function normalizeDetail(part, normalizer) {
6
+ if (part.type === 'text') {
7
+ return { type: 'text', text: part.text ?? '' };
8
+ }
9
+ if (part.type === 'image_url' && part.image_url) {
10
+ const url = part.image_url.url;
11
+ let source;
12
+ let mediaType = 'image/*';
13
+ if (url.startsWith('data:')) {
14
+ const semi = url.indexOf(';');
15
+ if (semi > 5)
16
+ mediaType = url.slice(5, semi);
17
+ source = { type: 'base64', data: url };
18
+ }
19
+ else {
20
+ source = { type: 'url', url };
21
+ }
22
+ const ref = await normalizer({ kind: 'image', source });
23
+ return { type: 'binary', kind: 'image', mediaType, ref };
24
+ }
25
+ if (part.type === 'file' && part.data) {
26
+ const ref = await normalizer({ kind: 'document', source: { type: 'base64', data: part.data } });
27
+ return {
28
+ type: 'binary',
29
+ kind: 'document',
30
+ mediaType: part.mimeType ?? 'application/octet-stream',
31
+ ref,
32
+ };
33
+ }
34
+ if ((part.type === 'audio' || part.type === 'image') && part.data) {
35
+ const kind = part.type === 'audio' ? 'audio' : 'image';
36
+ const ref = await normalizer({ kind, source: { type: 'base64', data: part.data } });
37
+ return {
38
+ type: 'binary',
39
+ kind,
40
+ mediaType: part.mimeType ?? (kind === 'audio' ? 'audio/*' : 'image/*'),
41
+ ref,
42
+ };
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Extract semantic cache params from a LlamaIndex ChatMessage array.
48
+ *
49
+ * Extracts the last user message for semantic similarity matching.
50
+ */
51
+ async function prepareSemanticParams(messages, opts) {
52
+ const normalizer = opts?.normalizer ?? normalizer_1.defaultNormalizer;
53
+ // Find last user message
54
+ const userMessages = messages.filter((m) => m.role === 'user');
55
+ if (userMessages.length === 0) {
56
+ return { text: '', model: opts?.model };
57
+ }
58
+ const lastUser = userMessages[userMessages.length - 1];
59
+ if (typeof lastUser.content === 'string') {
60
+ return { text: lastUser.content, model: opts?.model };
61
+ }
62
+ if (Array.isArray(lastUser.content)) {
63
+ const blocks = [];
64
+ for (const part of lastUser.content) {
65
+ const block = await normalizeDetail(part, normalizer);
66
+ if (block)
67
+ blocks.push(block);
68
+ }
69
+ const text = blocks
70
+ .filter((b) => b.type === 'text')
71
+ .map((b) => b.text)
72
+ .join(' ');
73
+ return { text, blocks, model: opts?.model };
74
+ }
75
+ return { text: '', model: opts?.model };
76
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OpenAI Responses API adapter for @betterdb/semantic-cache.
3
+ *
4
+ * Extracts the text to embed from OpenAI Responses API request params.
5
+ * Semantic caching keys on the last user input text. See openai.ts for
6
+ * the rationale for keying on the last user message rather than full history.
7
+ *
8
+ * Usage:
9
+ * import { prepareSemanticParams } from '@betterdb/semantic-cache/openai-responses';
10
+ * const { text, model } = await prepareSemanticParams(params);
11
+ * const result = await cache.check(text);
12
+ */
13
+ import type { ResponseCreateParams } from 'openai/resources/responses/responses';
14
+ import type { BinaryBlock, TextBlock } from '../utils';
15
+ import type { BinaryNormalizer } from '../normalizer';
16
+ export interface OpenAIResponsesSemanticPrepareOptions {
17
+ /** Binary content normalizer. Default: passthrough. */
18
+ normalizer?: BinaryNormalizer;
19
+ }
20
+ export interface SemanticParams {
21
+ text: string;
22
+ blocks?: (TextBlock | BinaryBlock)[];
23
+ model?: string;
24
+ }
25
+ /**
26
+ * Extract semantic cache params from OpenAI Responses API request params.
27
+ *
28
+ * Extracts the last user input text (or the instructions if no user input exists)
29
+ * for semantic similarity matching.
30
+ */
31
+ export declare function prepareSemanticParams(params: ResponseCreateParams, opts?: OpenAIResponsesSemanticPrepareOptions): Promise<SemanticParams>;