@larkup/vector-stores 0.1.20 → 0.1.21

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.
@@ -1,9 +1,5 @@
1
- import {
2
- Pinecone,
3
- type Index,
4
- type RecordSparseValues,
5
- } from "@pinecone-database/pinecone";
6
- import type { QueryHit, VectorRecord, VectorStoreAdapter } from "./base";
1
+ import { Pinecone, type Index, type RecordSparseValues } from '@pinecone-database/pinecone';
2
+ import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
7
3
 
8
4
  /**
9
5
  * Pinecone adapter — fully-managed cloud vector DB.
@@ -86,8 +82,8 @@ function is429(err: unknown): boolean {
86
82
  return (
87
83
  e?.status === 429 ||
88
84
  e?.statusCode === 429 ||
89
- String(e?.message ?? "").includes("429") ||
90
- String(e?.message ?? "").includes("RESOURCE_EXHAUSTED")
85
+ String(e?.message ?? '').includes('429') ||
86
+ String(e?.message ?? '').includes('RESOURCE_EXHAUSTED')
91
87
  );
92
88
  }
93
89
 
@@ -99,10 +95,10 @@ export class PineconeAdapter implements VectorStoreAdapter {
99
95
  private currentInterBatchDelayMs = 0;
100
96
 
101
97
  constructor(private readonly config: PineconeConfig) {
102
- if (!config.apiKey) throw new Error("Pinecone requires an API key.");
103
- if (!config.indexName) throw new Error("Pinecone requires an index name.");
98
+ if (!config.apiKey) throw new Error('Pinecone requires an API key.');
99
+ if (!config.indexName) throw new Error('Pinecone requires an index name.');
104
100
  this.indexName = config.indexName.trim();
105
- this.namespace = config.namespace?.trim() || "default";
101
+ this.namespace = config.namespace?.trim() || 'default';
106
102
  }
107
103
 
108
104
  private getClient() {
@@ -113,9 +109,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
113
109
  }
114
110
 
115
111
  private get needsSparse() {
116
- return (
117
- this.config.indexType === "lexical" || this.config.indexType === "hybrid"
118
- );
112
+ return this.config.indexType === 'lexical' || this.config.indexType === 'hybrid';
119
113
  }
120
114
 
121
115
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -126,12 +120,12 @@ export class PineconeAdapter implements VectorStoreAdapter {
126
120
  const exists = (indexes ?? []).some((i) => i.name === this.indexName);
127
121
 
128
122
  if (!exists) {
129
- const metric = this.needsSparse ? "dotproduct" : "cosine";
123
+ const metric = this.needsSparse ? 'dotproduct' : 'cosine';
130
124
  await pc.createIndex({
131
125
  name: this.indexName,
132
126
  dimension: dimensions,
133
127
  metric,
134
- spec: { serverless: { cloud: "aws", region: "us-east-1" } },
128
+ spec: { serverless: { cloud: 'aws', region: 'us-east-1' } },
135
129
  waitUntilReady: true,
136
130
  });
137
131
  }
@@ -151,6 +145,15 @@ export class PineconeAdapter implements VectorStoreAdapter {
151
145
  }
152
146
  }
153
147
 
148
+ async deleteByDocumentIds(documentIds: string[]): Promise<void> {
149
+ if (documentIds.length === 0) return;
150
+ try {
151
+ await this.ns().deleteMany({ filter: { documentId: { $in: documentIds } } });
152
+ } catch (err: any) {
153
+ console.error('Failed to delete vectors by documentIds in Pinecone:', err);
154
+ }
155
+ }
156
+
154
157
  // ── Sparse vector generation with rate-limit handling ─────────────────────
155
158
 
156
159
  /**
@@ -158,9 +161,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
158
161
  * Before each retry-sleep the `onRateLimit` hook is called so the indexer
159
162
  * can patch the run store with a warning that the UI polls.
160
163
  */
161
- private async embedSparseWithRetry(
162
- texts: string[],
163
- ): Promise<RecordSparseValues[]> {
164
+ private async embedSparseWithRetry(texts: string[]): Promise<RecordSparseValues[]> {
164
165
  const pc = this.getClient();
165
166
 
166
167
  for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES + 1; attempt++) {
@@ -168,7 +169,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
168
169
  const result = await pc.inference.embed({
169
170
  model: this.config.sparseModel!,
170
171
  inputs: texts,
171
- parameters: { input_type: "passage", truncate: "END" },
172
+ parameters: { input_type: 'passage', truncate: 'END' },
172
173
  });
173
174
  return result.data.map((emb) => {
174
175
  const s = emb as any;
@@ -182,7 +183,6 @@ export class PineconeAdapter implements VectorStoreAdapter {
182
183
  const waitSecs = Math.round(RATE_LIMIT_WAIT_MS / 1000);
183
184
  await this.config.onRateLimit?.(waitSecs, attempt);
184
185
  await sleep(RATE_LIMIT_WAIT_MS);
185
- // Slow down future batches to avoid hitting the rate limit again
186
186
  this.currentInterBatchDelayMs = Math.max(
187
187
  this.currentInterBatchDelayMs,
188
188
  BASE_INTER_BATCH_DELAY_MS,
@@ -193,12 +193,10 @@ export class PineconeAdapter implements VectorStoreAdapter {
193
193
  }
194
194
  }
195
195
  // Unreachable but TypeScript needs it
196
- throw new Error("Sparse embedding failed after max retries.");
196
+ throw new Error('Sparse embedding failed after max retries.');
197
197
  }
198
198
 
199
- private async buildSparseVectors(
200
- texts: string[],
201
- ): Promise<RecordSparseValues[]> {
199
+ private async buildSparseVectors(texts: string[]): Promise<RecordSparseValues[]> {
202
200
  const result: RecordSparseValues[] = [];
203
201
 
204
202
  for (let i = 0; i < texts.length; i += SPARSE_BATCH) {
@@ -206,11 +204,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
206
204
  const vecs = await this.embedSparseWithRetry(batch);
207
205
  result.push(...vecs);
208
206
 
209
- // Rate-limit guard: delay between batches (skip after the last one)
210
- if (
211
- i + SPARSE_BATCH < texts.length &&
212
- this.currentInterBatchDelayMs > 0
213
- ) {
207
+ if (i + SPARSE_BATCH < texts.length && this.currentInterBatchDelayMs > 0) {
214
208
  await sleep(this.currentInterBatchDelayMs);
215
209
  }
216
210
  }
@@ -218,9 +212,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
218
212
  return result;
219
213
  }
220
214
 
221
- private async buildQuerySparseVector(
222
- text: string,
223
- ): Promise<RecordSparseValues> {
215
+ private async buildQuerySparseVector(text: string): Promise<RecordSparseValues> {
224
216
  const pc = this.getClient();
225
217
 
226
218
  for (let attempt = 1; attempt <= RATE_LIMIT_MAX_RETRIES + 1; attempt++) {
@@ -228,7 +220,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
228
220
  const result = await pc.inference.embed({
229
221
  model: this.config.sparseModel!,
230
222
  inputs: [text],
231
- parameters: { input_type: "query", truncate: "END" },
223
+ parameters: { input_type: 'query', truncate: 'END' },
232
224
  });
233
225
  const s = result.data[0] as any;
234
226
  return {
@@ -243,7 +235,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
243
235
  throw err;
244
236
  }
245
237
  }
246
- throw new Error("Sparse query embedding failed after max retries.");
238
+ throw new Error('Sparse query embedding failed after max retries.');
247
239
  }
248
240
 
249
241
  // ── Upsert ─────────────────────────────────────────────────────────────────
@@ -256,16 +248,9 @@ export class PineconeAdapter implements VectorStoreAdapter {
256
248
  const safe: Record<string, any> = {};
257
249
  for (const [k, v] of Object.entries(meta)) {
258
250
  if (v === null || v === undefined) continue;
259
- if (
260
- typeof v === "string" ||
261
- typeof v === "number" ||
262
- typeof v === "boolean"
263
- ) {
251
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
264
252
  safe[k] = v;
265
- } else if (
266
- Array.isArray(v) &&
267
- v.every((item) => typeof item === "string")
268
- ) {
253
+ } else if (Array.isArray(v) && v.every((item) => typeof item === 'string')) {
269
254
  safe[k] = v;
270
255
  } else {
271
256
  safe[k] = JSON.stringify(v);
@@ -278,7 +263,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
278
263
  ...sanitizeMeta(r.metadata),
279
264
  text: r.text,
280
265
  title: r.title,
281
- url: r.url ?? "",
266
+ url: r.url ?? '',
282
267
  source: r.source,
283
268
  documentId: r.documentId,
284
269
  chunkIndex: r.chunkIndex,
@@ -297,9 +282,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
297
282
  }
298
283
 
299
284
  // ── Hybrid / Lexical ───────────────────────────────────────────────────
300
- const sparseVecs = await this.buildSparseVectors(
301
- records.map((r) => r.text),
302
- );
285
+ const sparseVecs = await this.buildSparseVectors(records.map((r) => r.text));
303
286
 
304
287
  await this.ns().upsert({
305
288
  records: records.map((r, i) => ({
@@ -313,11 +296,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
313
296
 
314
297
  // ── Query ──────────────────────────────────────────────────────────────────
315
298
 
316
- async query(
317
- vector: number[],
318
- topK: number,
319
- queryText?: string,
320
- ): Promise<QueryHit[]> {
299
+ async query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]> {
321
300
  const canHybrid = this.needsSparse && this.config.sparseModel && queryText;
322
301
 
323
302
  if (!canHybrid) {
@@ -333,10 +312,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
333
312
  return rrfMerge(denseHits, sparseHits, topK);
334
313
  }
335
314
 
336
- private async denseQuery(
337
- vector: number[],
338
- topK: number,
339
- ): Promise<QueryHit[]> {
315
+ private async denseQuery(vector: number[], topK: number): Promise<QueryHit[]> {
340
316
  const res = await this.ns().query({ vector, topK, includeMetadata: true });
341
317
  return (res.matches ?? []).map(hitFromMatch);
342
318
  }
@@ -370,11 +346,7 @@ export class PineconeAdapter implements VectorStoreAdapter {
370
346
  async count(): Promise<number | null> {
371
347
  try {
372
348
  const stats = await this.ns().describeIndexStats();
373
- return (
374
- stats.namespaces?.[this.namespace]?.recordCount ??
375
- stats.totalRecordCount ??
376
- 0
377
- );
349
+ return stats.namespaces?.[this.namespace]?.recordCount ?? stats.totalRecordCount ?? 0;
378
350
  } catch {
379
351
  return null;
380
352
  }
@@ -389,18 +361,16 @@ export class PineconeAdapter implements VectorStoreAdapter {
389
361
  indexList = await pc.listIndexes();
390
362
  } catch (err: any) {
391
363
  if (
392
- err.message?.toLowerCase().includes("api key") ||
393
- err.name === "PineconeAuthorizationError" ||
364
+ err.message?.toLowerCase().includes('api key') ||
365
+ err.name === 'PineconeAuthorizationError' ||
394
366
  err.status === 401
395
367
  ) {
396
- throw new Error("Invalid Pinecone API key.");
368
+ throw new Error('Invalid Pinecone API key.');
397
369
  }
398
370
  throw new Error(`Failed to connect to Pinecone: ${err.message}`);
399
371
  }
400
372
 
401
- const indexModel = (indexList.indexes ?? []).find(
402
- (i) => i.name === this.indexName,
403
- );
373
+ const indexModel = (indexList.indexes ?? []).find((i) => i.name === this.indexName);
404
374
  if (!indexModel) {
405
375
  throw new Error(
406
376
  `Index "${this.indexName}" does not exist in your Pinecone project. Please create it first.`,
@@ -415,14 +385,11 @@ export class PineconeAdapter implements VectorStoreAdapter {
415
385
 
416
386
  if (this.needsSparse) {
417
387
  if (!this.config.sparseModel) {
418
- throw new Error(
419
- "Hybrid/lexical search requires a sparse model to be selected.",
420
- );
388
+ throw new Error('Hybrid/lexical search requires a sparse model to be selected.');
421
389
  }
422
390
 
423
- const metric =
424
- (indexModel as any).metric ?? (indexModel as any).spec?.metric;
425
- if (metric && metric !== "dotproduct") {
391
+ const metric = (indexModel as any).metric ?? (indexModel as any).spec?.metric;
392
+ if (metric && metric !== 'dotproduct') {
426
393
  throw new Error(
427
394
  `Hybrid/lexical requires your Pinecone index to use the "dotproduct" metric, ` +
428
395
  `but "${this.indexName}" uses "${metric}". ` +
@@ -437,33 +404,20 @@ export class PineconeAdapter implements VectorStoreAdapter {
437
404
 
438
405
  function hitFromMatch(m: any): QueryHit {
439
406
  const meta = (m.metadata ?? {}) as PineMeta;
440
- const {
441
- text,
442
- title,
443
- url,
444
- source,
445
- documentId,
446
- chunkIndex,
447
- ...customMetadata
448
- } = meta;
407
+ const { text, title, url, source, documentId, chunkIndex, ...customMetadata } = meta;
449
408
 
450
409
  return {
451
410
  id: m.id,
452
411
  score: m.score ?? 0,
453
- text: (text as string) ?? "",
454
- title: (title as string) ?? "Untitled",
412
+ text: (text as string) ?? '',
413
+ title: (title as string) ?? 'Untitled',
455
414
  url: (url as string) || undefined,
456
- documentId: (documentId as string) ?? "",
457
- metadata:
458
- Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
415
+ documentId: (documentId as string) ?? '',
416
+ metadata: Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
459
417
  };
460
418
  }
461
419
 
462
- function rrfMerge(
463
- denseHits: QueryHit[],
464
- sparseHits: QueryHit[],
465
- topK: number,
466
- ): QueryHit[] {
420
+ function rrfMerge(denseHits: QueryHit[], sparseHits: QueryHit[], topK: number): QueryHit[] {
467
421
  const scores = new Map<string, { hit: QueryHit; score: number }>();
468
422
 
469
423
  const addList = (hits: QueryHit[]) =>
package/src/factory.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { RagConfig } from "@larkup/core/types";
2
- import type { VectorStoreAdapter } from "./adapters/base";
1
+ import type { RagConfig } from '@larkup/core/types';
2
+ import type { VectorStoreAdapter } from './adapters/base';
3
+ import { getActiveServer } from '@larkup/core/workspace';
3
4
 
4
5
  /**
5
6
  * Build the right adapter from the persisted config. Centralizing this keeps
@@ -14,42 +15,61 @@ export async function createAdapter(
14
15
  config: RagConfig,
15
16
  onRateLimit?: (waitSecs: number, attempt: number) => void | Promise<void>,
16
17
  ): Promise<VectorStoreAdapter> {
18
+ const overrides = { ...config.storeConfig };
19
+ const server = await getActiveServer();
20
+ const serverId = server?.id;
21
+
22
+ if (serverId) {
23
+ const safeId = serverId.replace(/[^a-zA-Z0-9_-]/g, '_');
24
+ if (config.vectorStore === 'pinecone') {
25
+ overrides.namespace = serverId;
26
+ } else if (config.vectorStore === 'chroma') {
27
+ overrides.collectionName = `documents_${safeId}`;
28
+ } else if (config.vectorStore === 'lancedb' || !config.vectorStore) {
29
+ overrides.tableName = `documents_${safeId}`;
30
+ }
31
+ }
32
+
17
33
  switch (config.vectorStore) {
18
- case "pinecone": {
19
- const { PineconeAdapter } = await import("./adapters/pinecone");
34
+ case 'pinecone': {
35
+ const { PineconeAdapter } = await import('./adapters/pinecone');
20
36
  return new PineconeAdapter({
21
- apiKey: config.storeConfig.apiKey,
22
- indexName: config.storeConfig.indexName,
23
- namespace: config.storeConfig.namespace,
24
- sparseModel: config.storeConfig.sparseModel,
37
+ apiKey: overrides.apiKey,
38
+ indexName: overrides.indexName,
39
+ namespace: overrides.namespace,
40
+ sparseModel: overrides.sparseModel,
25
41
  indexType: config.indexType,
26
42
  onRateLimit,
27
43
  });
28
44
  }
29
- case "chroma": {
30
- const { ChromaAdapter } = await import("./adapters/chroma");
45
+ case 'chroma': {
46
+ const { ChromaAdapter } = await import('./adapters/chroma');
31
47
  return new ChromaAdapter({
32
- mode: config.storeConfig.mode,
33
- host: config.storeConfig.host,
34
- authToken: config.storeConfig.authToken,
35
- apiKey: config.storeConfig.apiKey,
36
- tenant: config.storeConfig.tenant,
37
- database: config.storeConfig.database,
38
- collectionName: config.storeConfig.collectionName,
48
+ mode: overrides.mode,
49
+ host: overrides.host,
50
+ authToken: overrides.authToken,
51
+ apiKey: overrides.apiKey,
52
+ tenant: overrides.tenant,
53
+ database: overrides.database,
54
+ collectionName: overrides.collectionName,
39
55
  indexType: config.indexType,
40
56
  });
41
57
  }
42
- case "lancedb":
58
+ case 'lancedb':
43
59
  default: {
44
- const { LanceDBAdapter } = await import("./adapters/lancedb");
60
+ const { LanceDBAdapter } = await import('./adapters/lancedb');
45
61
  return new LanceDBAdapter({
46
- mode: config.storeConfig.mode,
47
- dbPath: config.storeConfig.dbPath,
48
- uri: config.storeConfig.uri,
49
- apiKey: config.storeConfig.apiKey,
50
- tableName: config.storeConfig.tableName,
62
+ mode: overrides.mode,
63
+ dbPath: overrides.dbPath,
64
+ uri: overrides.uri,
65
+ apiKey: overrides.apiKey,
66
+ s3Uri: overrides.s3Uri,
67
+ s3Endpoint: overrides.s3Endpoint,
68
+ s3Region: overrides.s3Region,
69
+ s3AccessKeyId: overrides.s3AccessKeyId,
70
+ s3SecretAccessKey: overrides.s3SecretAccessKey,
71
+ tableName: overrides.tableName,
51
72
  });
52
73
  }
53
74
  }
54
-
55
75
  }