@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 +68 -0
- package/dist/index.cjs +435 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -14
- package/dist/index.d.ts +207 -14
- package/dist/index.js +425 -25
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +174 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +60 -0
- package/dist/nestjs/index.d.ts +60 -0
- package/dist/nestjs/index.js +149 -0
- package/dist/nestjs/index.js.map +1 -0
- package/package.json +24 -4
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
|
|