@dudousxd/nestjs-agent-rag 0.11.0 → 0.13.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/NOTICE +31 -0
- package/README.md +109 -4
- package/dist/index.cjs +2539 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +862 -11
- package/dist/index.d.ts +862 -11
- package/dist/index.js +2514 -67
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/NOTICE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@dudousxd/nestjs-agent-rag
|
|
2
|
+
Copyright (c) Davide Carvalho. Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
The tree navigation retrieval in src/tree/ (buildDocumentTree, TreeNavigationRetriever,
|
|
5
|
+
TwoStageRetriever, navigate_document) is an independent TypeScript implementation of the method of
|
|
6
|
+
PageIndex ("vectorless", reasoning-based RAG: an LLM-navigated table-of-contents tree per document).
|
|
7
|
+
Its tree-search and document-selection prompts are adapted from PageIndex's.
|
|
8
|
+
|
|
9
|
+
PageIndex — https://github.com/VectifyAI/PageIndex
|
|
10
|
+
|
|
11
|
+
MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2025 Vectify AI
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -230,9 +230,13 @@ const store = new PgLexicalVectorStore(db, {
|
|
|
230
230
|
});
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
A question is searched by its meaningful terms: its stop words are dropped (English, Portuguese and
|
|
234
|
+
Spanish lists by default, only the language(s) the question is written in; `stopWords` to change or
|
|
235
|
+
`false` to keep every word), and rows holding any remaining term are ranked by the sum of the
|
|
236
|
+
matched terms' IDF (BM25 without term frequencies), `ts_rank_cd` breaking ties, so three rare terms
|
|
237
|
+
beat a row full of "the/of/which". Quoted phrases and `-word` go through `websearch_to_tsquery` as
|
|
238
|
+
written first; `anyTermFallback: false` keeps only that (every word must appear). `keywordTerms()`
|
|
239
|
+
is exported for other stores. Without `column`, `ensureSchema`/`schemaStatements` add a GIN index over
|
|
236
240
|
`to_tsvector(config, text)` — the exact expression the query uses. On a populated table create it
|
|
237
241
|
`CONCURRENTLY` in a migration rather than at boot (see below).
|
|
238
242
|
|
|
@@ -435,6 +439,90 @@ Had `PgVectorStore` simply grown `searchText`, every `isLexicalVectorStore(store
|
|
|
435
439
|
started using it against an unindexed table on upgrade. Constructing `PgLexicalVectorStore` is the
|
|
436
440
|
opt-in; the index is a migration you run and watch (`schemaStatements()` prints it).
|
|
437
441
|
|
|
442
|
+
## Tree navigation retrieval (PageIndex-style)
|
|
443
|
+
|
|
444
|
+
Chunk search ranks passages; on a **long structured document** (a regulation, an RFP, a contract, a
|
|
445
|
+
10-K, a manual) it usually finds the right document and then loses the right page among hundreds of
|
|
446
|
+
look-alike clause or financial-statement chunks. Tree navigation ("vectorless", reasoning-based, the
|
|
447
|
+
method of [PageIndex](https://github.com/VectifyAI/PageIndex)) builds a table of contents per long
|
|
448
|
+
document at ingestion — sections with page ranges and summaries — and at query time an LLM reads that
|
|
449
|
+
outline and walks to the sections that answer, returning their text with the path it took.
|
|
450
|
+
|
|
451
|
+
Measured on Flippy (149 questions, same answering model): on long GovCon regulations it beat hybrid
|
|
452
|
+
dense+lexical search by **+24 points**, on FinanceBench 10-Ks by **+30**, and gave nothing on short
|
|
453
|
+
wiki notes — at seconds of latency and LLM spend per query instead of 0.3 s. Hence the recommended
|
|
454
|
+
shape: **hybrid first, tree navigation only inside the top long documents**.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import {
|
|
458
|
+
PgDocumentTreeStore, TreeNavigationRetriever, TwoStageRetriever, HybridRetriever,
|
|
459
|
+
createNavigateDocumentTool, createRetrievalTool, indexDocumentTree, openAiChatTreeLlm,
|
|
460
|
+
} from '@dudousxd/nestjs-agent-rag';
|
|
461
|
+
|
|
462
|
+
const trees = new PgDocumentTreeStore(db); // pg Pool, Drizzle db, postgres.js sql — like PgVectorStore
|
|
463
|
+
await trees.ensureSchema(); // or copy trees.schemaStatements() into a migration
|
|
464
|
+
const llm = openAiChatTreeLlm({ model: 'gpt-4.1-mini', apiKey }); // or treeLlmFromModelProvider(provider)
|
|
465
|
+
|
|
466
|
+
// At upload, next to chunk + embed: only long documents get a tree (default ≥ 20 pages or ≥ 60k chars).
|
|
467
|
+
await indexDocumentTree(
|
|
468
|
+
{ pages, title: file.name }, // or { sections: [{ title, level, text, page }] }, + outline
|
|
469
|
+
{ store: trees, documentId: file.id, llm, metadata: { tenant }, source: file.name },
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const navigator = new TreeNavigationRetriever({ store: trees, llm });
|
|
473
|
+
// Hybrid picks documents; long ones among the top 3 get navigated (at most 2), short ones keep chunks.
|
|
474
|
+
const retriever = new TwoStageRetriever(hybrid, navigator, { navigateDocuments: 2 });
|
|
475
|
+
// …or let the agent decide: search first, then open a long document.
|
|
476
|
+
providers: [
|
|
477
|
+
provideAgentTool(createRetrievalTool(hybrid)),
|
|
478
|
+
provideAgentTool(createNavigateDocumentTool(navigator, { filter: (ctx) => ({ tenant: ctx.actor.tenantRef }) })),
|
|
479
|
+
];
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Building a tree** (`buildDocumentTree` / `indexDocumentTree`) takes the cheapest structure that works
|
|
483
|
+
and never loops:
|
|
484
|
+
|
|
485
|
+
1. the document's own `outline` (PDF bookmarks), when you pass one;
|
|
486
|
+
2. section titles/levels (`sections` input) or headings found in the text — markdown headers and the
|
|
487
|
+
keyword/numbered headings of regulations and filings (`PART 52`, `Subpart 19.8`, `Item 7.`,
|
|
488
|
+
`52.236–5 Material and Workmanship.`), with running page headers and table-of-contents listings
|
|
489
|
+
dropped;
|
|
490
|
+
3. only when there is no usable structure, the LLM proposes the sections window by window — and only
|
|
491
|
+
if the whole document fits `maxStructureCalls`;
|
|
492
|
+
4. otherwise, or on any failure, page groups (`Pages 21–25: <first line>`), grouped again when wide.
|
|
493
|
+
|
|
494
|
+
Summaries go top-down (top levels matter most for navigation) until the **hard per-document budget**
|
|
495
|
+
runs out (`budget: { maxCalls, maxInputTokens, maxOutputTokens, timeoutMs, callTimeoutMs }`, default 60
|
|
496
|
+
calls / 400k input tokens / 5 min); the rest summarize themselves with their opening text. Every call
|
|
497
|
+
is checked against the budget before it is made and nothing is retried, so a document the LLM cannot
|
|
498
|
+
structure costs a bounded amount and still gets a usable tree (`tree.stats` records calls, tokens and
|
|
499
|
+
`fallbackReason`). Builds are deterministic for a deterministic model and fingerprinted: re-indexing an
|
|
500
|
+
unchanged document costs one store read and no LLM call, a changed one re-summarizes only the sections
|
|
501
|
+
whose text changed (`reusedSummaries`), and `cachedTreeLlm(llm, cache)` makes repeated prompts free.
|
|
502
|
+
|
|
503
|
+
**Navigating** (`TreeNavigationRetriever`): a tree whose outline fits `singlePassMaxChars` (default
|
|
504
|
+
40k chars) is navigated in one call that sees the whole outline; a larger one is walked level by level
|
|
505
|
+
(a beam of `beamWidth` nodes, at most `maxSteps` levels), the model choosing to `expand` or `read` each
|
|
506
|
+
candidate. Each navigation has its own hard budget (default 6 calls / 60k input tokens / 60 s); running
|
|
507
|
+
out returns what was chosen so far. `navigate()` / `search()` return the audit trail with the passages
|
|
508
|
+
— every step's candidates, choices and stated reasoning, and each node's path — and every passage
|
|
509
|
+
carries `documentId`, `nodeId`, `title`, `pageStart`/`pageEnd` and `path` in its metadata. As a plain
|
|
510
|
+
`Retriever` it navigates every tree passing `filter` (the LLM picks `maxDocuments` of them from their
|
|
511
|
+
descriptions when there are more).
|
|
512
|
+
|
|
513
|
+
**Access control.** `DocumentTreeStore` filters (`get`, `getMany`, `list`) have exactly the vector
|
|
514
|
+
stores' semantics — scalar equality, array match-any, empty-array deny — and the navigator, the
|
|
515
|
+
two-stage retriever and the tool pass the caller's filter to every tree read.
|
|
516
|
+
|
|
517
|
+
**LLM.** `TreeLlm` is one prompt in, one text reply out: `openAiChatTreeLlm` (any OpenAI-compatible
|
|
518
|
+
`/chat/completions`; pass `body: { enable_thinking: false }` or similar to keep a reasoning model from
|
|
519
|
+
spending the reply budget on thoughts), `treeLlmFromModelProvider` (the agent's `ModelProvider`), or
|
|
520
|
+
your own function. Each request also carries its `task` as data, which is what `keywordTreeLlm()` — a
|
|
521
|
+
deterministic, model-free stand-in for tests — answers from.
|
|
522
|
+
|
|
523
|
+
PageIndex (© 2025 Vectify AI, MIT) is the method and the prompts' origin; this is an independent
|
|
524
|
+
TypeScript implementation. See [NOTICE](./NOTICE).
|
|
525
|
+
|
|
438
526
|
## API
|
|
439
527
|
|
|
440
528
|
- `chunkText(text, { chunkSize?, overlap?, separator? })` — overlapping, boundary-aware chunks;
|
|
@@ -460,7 +548,24 @@ opt-in; the index is a migration you run and watch (`schemaStatements()` prints
|
|
|
460
548
|
- `instrumentRetriever(retriever, { describe?, collection? })` — wrap any `Retriever` so each
|
|
461
549
|
retrieval publishes on `aviary:rag:retrieval` (`RAG_RETRIEVAL_CHANNEL`).
|
|
462
550
|
- `openAiEmbeddings({ model, baseUrl?, apiKey?, dimensions?, batchSize?, maxInputChars?, headers?,
|
|
463
|
-
timeoutMs?, onUsage?, fetch? })` — an `EmbeddingProvider` over any OpenAI-compatible
|
|
551
|
+
timeoutMs?, onUsage?, onWarn?, fetch? })` — an `EmbeddingProvider` over any OpenAI-compatible
|
|
552
|
+
`/embeddings`. A batch the server refuses as too large (413, or TEI's `batch size N > maximum
|
|
553
|
+
allowed batch size M`; see `isBatchTooLarge`) is split in halves and retried, and later requests to
|
|
554
|
+
that server and model use the size that worked; each split is reported through `onWarn`.
|
|
555
|
+
- `buildDocumentTree(input, { documentId, llm?, maxDepth?, budget?, summaries?, describe?,
|
|
556
|
+
detectHeadings?, llmStructure?, maxStructureCalls?, groupSize?, maxLeafUnits?, previous?, metadata?,
|
|
557
|
+
source? })` — a document's navigation tree (`{ tree, units, unchanged }`). `indexDocumentTree(input,
|
|
558
|
+
{ store, minUnits?, minChars?, …build options })` builds, stores and skips short/unchanged documents.
|
|
559
|
+
- `DocumentTreeStore` (SPI), `MemoryDocumentTreeStore`, `PgDocumentTreeStore(client, { table?,
|
|
560
|
+
unitsTable? })` with `schemaStatements()` / `ensureSchema()`.
|
|
561
|
+
- `TreeNavigationRetriever({ store, llm, singlePassMaxChars?, beamWidth?, maxSteps?, maxNodes?, budget?,
|
|
562
|
+
maxPassageChars?, maxContextChars?, maxDocuments?, concurrency? })` — `retrieve`, `search` (with the
|
|
563
|
+
trail), `navigate(query, documentIds, { filter?, budget? })`.
|
|
564
|
+
- `TwoStageRetriever(firstStage, navigator, { fetchTopK?, considerDocuments?, navigateDocuments?,
|
|
565
|
+
budget?, keepFirstStageChunks?, documentIdOf?, onNavigation? })`.
|
|
566
|
+
- `createNavigateDocumentTool(navigator, { name?, description?, filter?, budget? })` — the
|
|
567
|
+
`navigate_document({ documentId, question })` tool.
|
|
568
|
+
- `openAiChatTreeLlm`, `treeLlmFromModelProvider`, `cachedTreeLlm`, `keywordTreeLlm` — `TreeLlm`s.
|
|
464
569
|
- `HttpReranker({ url, model?, apiKey?, format?, headers?, timeoutMs?, fetch? })` — a `Reranker` over
|
|
465
570
|
a Cohere/Jina/Voyage/TEI-style `/rerank` endpoint.
|
|
466
571
|
|