@dudousxd/nestjs-agent-rag 0.13.1 → 0.14.1

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/README.md CHANGED
@@ -197,6 +197,9 @@ await store.ensureSchema(); // CREATE EXTENSION vector + table + HNSW cosine ind
197
197
  `store.schemaStatements()` returns the same DDL as strings, for a migration instead of a boot-time
198
198
  side effect. Other options (all off by default, so existing tables behave exactly as before):
199
199
 
200
+ - **`vectorType`** (default `vector`) — `halfvec` stores 2 bytes per dimension and its HNSW index goes
201
+ up to 4000 dimensions, where `vector` stops at 2000. Needs a single width. Converting an existing
202
+ table is a migration of yours.
200
203
  - **`upsertBatchSize`** (default 100) — `upsert` writes multi-row `INSERT … ON CONFLICT` statements;
201
204
  duplicate ids in one call collapse to the last occurrence. NUL bytes are stripped from every
202
205
  text/jsonb binding (Postgres rejects them).
@@ -439,6 +442,70 @@ Had `PgVectorStore` simply grown `searchText`, every `isLexicalVectorStore(store
439
442
  started using it against an unindexed table on upgrade. Constructing `PgLexicalVectorStore` is the
440
443
  opt-in; the index is a migration you run and watch (`schemaStatements()` prints it).
441
444
 
445
+ ## RAG for a model: `RagRepository`
446
+
447
+ One class per model, as `nestjs-filter` does with filters. The entity or Drizzle table doesn't change.
448
+
449
+ ```ts
450
+ import { RagModule, RagRepository } from '@dudousxd/nestjs-agent-rag/nestjs';
451
+
452
+ // Once in the app
453
+ RagModule.forRoot({ store, embedder, onUsage: (usage) => meter.add(usage) });
454
+
455
+ @Injectable()
456
+ export class DocumentsRag extends RagRepository<ProjectDocument> {
457
+ protected readonly name = 'project_documents';
458
+ readonly scope = { projectId: (d: ProjectDocument) => d.project.id };
459
+
460
+ id(d: ProjectDocument) { return d.id; }
461
+ protected text(d: ProjectDocument) { return d.textContent; }
462
+ }
463
+
464
+ // In the feature module
465
+ RagModule.forFeature([DocumentsRag]);
466
+
467
+ // Injected by type
468
+ await documentsRag.index(doc);
469
+ await documentsRag.search('question', { scope: { projectId } });
470
+ await documentsRag.remove(doc);
471
+ ```
472
+
473
+ With Drizzle, only the row type changes: `type Row = typeof documents.$inferSelect`, then
474
+ `RagRepository<Row>` and `scope = { projectId: (d: Row) => d.projectId }`.
475
+
476
+ **The scope can't be skipped.** Leaving out `scope`, returning a nullable column, or calling `search`
477
+ without it doesn't compile. At runtime a missing, empty or malformed value throws
478
+ `RagScopeRequiredError` before any embedding is paid for, and `filter` can't override it. Values are
479
+ compared as strings. To search several, use `anyOf(ids)`, where an empty list matches nothing; a plain
480
+ array is refused so a request can't widen a search. A corpus shared by everyone says `scope = {}`.
481
+
482
+ **Saving doesn't index.** Embedding is slow and paid for, so call `index` from a job or wherever the
483
+ text changed. It embeds first, so a provider failure keeps the old chunks. On `PgVectorStore` the swap is
484
+ one transaction. On Redis, a search can briefly see both versions, but concurrent calls wait for each
485
+ other, so they can't leave a mix behind. On stores without `replaceDocument`, it is two statements.
486
+
487
+ **Deleting.** With Drizzle, use `.returning()` and call `remove`. With MikroORM, add
488
+ `provideRagDeleteSync(ProjectDocument, DocumentsRag)` from `@dudousxd/nestjs-agent-store-mikro-orm` to
489
+ your providers; it removes after the commit and sees only `em.remove` (on MikroORM 6, not inside a
490
+ nested transaction). For everything no hook sees
491
+ (`ON DELETE CASCADE`, `nativeDelete`, raw SQL), run `reconcile`:
492
+
493
+ ```ts
494
+ await documentsRag.reconcile(allLiveIds, { dryRun: true }); // { orphans, missing }
495
+ await documentsRag.reconcile(liveIdsOfProject, { scope: { projectId } });
496
+ ```
497
+
498
+ `liveIds` must be complete for the part compared: whatever it leaves out is removed.
499
+
500
+ **Agents.** `documentsRag.tool({ name, scope: (ctx) => ({ projectId: ctx.actor.tenantRef as string }) })`
501
+ gives a tool whose scope comes from the context, for `provideAgentTool`. `scoped(scope)` gives a
502
+ `Retriever` for composing. `retrieval: { mode: 'inject' }` passes no actor, so it can't scope per
503
+ request: use the tool for multi-tenant agents.
504
+
505
+ On Redis, declare the keys up front: `filterableFields: ragFilterableFields('projectId')`.
506
+
507
+ Without Nest, `RagIndex` from the main entry does the same with a plain definition.
508
+
442
509
  ## Tree navigation retrieval (PageIndex-style)
443
510
 
444
511
  Chunk search ranks passages; on a **long structured document** (a regulation, an RFP, a contract, a
@@ -592,6 +659,7 @@ await store.removeMany(['doc-a', 'doc-b']); // N documents, one pas
592
659
  |---|---|
593
660
  | `{ collection: 'handbook' }` | deletes that scope, returns the chunk count |
594
661
  | `{ audience: [] }` (or any empty array) | **deletes nothing**, returns `0` — the empty array is this package's deny primitive, and a deny must never be read as "no filter" |
662
+ | `{ tenantId: undefined }` | throws `UnsafeRemovalError` with reason `undefined-value` — a missing value is a bug, and on every read method it matches nothing |
595
663
  | `{}` | throws `UnsafeRemovalError` — an empty filter is far more often a filter built wrong than a deliberate request to wipe the store |
596
664
  | `{ notFilterable: 'x' }` (Redis) | throws `UnsafeRemovalError` — the key has no `meta_*` TAG to match on |
597
665