nexus-ai-pro 2.2.0 → 2.3.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/API_STABILITY.md +39 -0
- package/CHANGELOG.md +142 -1
- package/README.md +10 -5
- package/dist/agent/capabilities.d.ts +39 -0
- package/dist/agent/capabilities.js +36 -0
- package/dist/agent/create-agent.d.ts +61 -11
- package/dist/agent/create-agent.js +136 -22
- package/dist/agent/index.d.ts +3 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/middleware/approval.d.ts +26 -0
- package/dist/agent/middleware/approval.js +23 -0
- package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist/agent/middleware/context-editor.d.ts +52 -0
- package/dist/agent/middleware/context-editor.js +66 -0
- package/dist/agent/middleware/filesystem.d.ts +44 -0
- package/dist/agent/middleware/filesystem.js +83 -0
- package/dist/agent/middleware/index.d.ts +19 -0
- package/dist/agent/middleware/index.js +9 -0
- package/dist/agent/middleware/limits.d.ts +42 -0
- package/dist/agent/middleware/limits.js +52 -0
- package/dist/agent/middleware/models.d.ts +48 -0
- package/dist/agent/middleware/models.js +44 -0
- package/dist/agent/middleware/pii.d.ts +67 -0
- package/dist/agent/middleware/pii.js +173 -0
- package/dist/agent/middleware/retry.d.ts +66 -0
- package/dist/agent/middleware/retry.js +59 -0
- package/dist/agent/middleware/shared.d.ts +41 -0
- package/dist/agent/middleware/shared.js +73 -0
- package/dist/agent/middleware/tool-selector.d.ts +64 -0
- package/dist/agent/middleware/tool-selector.js +106 -0
- package/dist/agent/permissions.d.ts +113 -0
- package/dist/agent/permissions.js +180 -0
- package/dist/agent/sandbox.d.ts +151 -0
- package/dist/agent/sandbox.js +257 -0
- package/dist/agent/tool.d.ts +8 -3
- package/dist/agent/tool.js +14 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +25 -4
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/pipeline.d.ts +140 -0
- package/dist/rag/pipeline.js +187 -0
- package/dist/rag/rerankers.d.ts +92 -0
- package/dist/rag/rerankers.js +97 -0
- package/dist/rag/retrievers.d.ts +12 -1
- package/dist/server/assistant.d.ts +5 -1
- package/dist/server/assistant.js +4 -2
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/types/graph.d.ts +38 -0
- package/dist/types/messages.d.ts +36 -2
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/server.d.ts +2 -9
- package/dist/utils/tenant.d.ts +11 -0
- package/dist/utils/tenant.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/capabilities.d.ts +39 -0
- package/dist-cjs/agent/capabilities.js +41 -0
- package/dist-cjs/agent/create-agent.d.ts +61 -11
- package/dist-cjs/agent/create-agent.js +136 -22
- package/dist-cjs/agent/index.d.ts +3 -2
- package/dist-cjs/agent/index.js +4 -4
- package/dist-cjs/agent/middleware/approval.d.ts +26 -0
- package/dist-cjs/agent/middleware/approval.js +26 -0
- package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
- package/dist-cjs/agent/middleware/context-editor.js +70 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
- package/dist-cjs/agent/middleware/filesystem.js +86 -0
- package/dist-cjs/agent/middleware/index.d.ts +19 -0
- package/dist-cjs/agent/middleware/index.js +28 -0
- package/dist-cjs/agent/middleware/limits.d.ts +42 -0
- package/dist-cjs/agent/middleware/limits.js +57 -0
- package/dist-cjs/agent/middleware/models.d.ts +48 -0
- package/dist-cjs/agent/middleware/models.js +48 -0
- package/dist-cjs/agent/middleware/pii.d.ts +67 -0
- package/dist-cjs/agent/middleware/pii.js +178 -0
- package/dist-cjs/agent/middleware/retry.d.ts +66 -0
- package/dist-cjs/agent/middleware/retry.js +63 -0
- package/dist-cjs/agent/middleware/shared.d.ts +41 -0
- package/dist-cjs/agent/middleware/shared.js +83 -0
- package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
- package/dist-cjs/agent/middleware/tool-selector.js +109 -0
- package/dist-cjs/agent/permissions.d.ts +113 -0
- package/dist-cjs/agent/permissions.js +189 -0
- package/dist-cjs/agent/sandbox.d.ts +151 -0
- package/dist-cjs/agent/sandbox.js +297 -0
- package/dist-cjs/agent/tool.d.ts +8 -3
- package/dist-cjs/agent/tool.js +14 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +25 -4
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/pipeline.d.ts +140 -0
- package/dist-cjs/rag/pipeline.js +191 -0
- package/dist-cjs/rag/rerankers.d.ts +92 -0
- package/dist-cjs/rag/rerankers.js +107 -0
- package/dist-cjs/rag/retrievers.d.ts +12 -1
- package/dist-cjs/server/assistant.d.ts +5 -1
- package/dist-cjs/server/assistant.js +4 -2
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/types/graph.d.ts +38 -0
- package/dist-cjs/types/messages.d.ts +36 -2
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/server.d.ts +2 -9
- package/dist-cjs/utils/tenant.d.ts +11 -0
- package/dist-cjs/utils/tenant.js +15 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +4 -3
- package/package.json +118 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable ingestion: keeps a vector store, and any keyword indexes beside it, in step with a corpus
|
|
3
|
+
* that changes.
|
|
4
|
+
*
|
|
5
|
+
* A manifest in any `Store` records, for every document, the version of its content, the chunking,
|
|
6
|
+
* the embedding model, and each chunk's version. A run reads the corpus, embeds only the chunks that
|
|
7
|
+
* are new or changed, deletes the chunks a document no longer has and the documents the corpus no
|
|
8
|
+
* longer has, and writes every change idempotently, before the manifest records it. A run that dies
|
|
9
|
+
* halfway leaves a manifest that says exactly what finished; the next run skips it, and inside a
|
|
10
|
+
* durable operation it starts at the document it stopped on.
|
|
11
|
+
*/
|
|
12
|
+
import type { EmbeddingProvider, VectorStore } from '../hallucination/retrieval.js';
|
|
13
|
+
import type { OperationContext, OperationExecutor } from '../types/operations.js';
|
|
14
|
+
import type { Store } from '../types/store.js';
|
|
15
|
+
import { type DocumentSource, type IngestionOptions } from './ingestion.js';
|
|
16
|
+
import type { SparseRetriever } from './retrievers.js';
|
|
17
|
+
/** A source of documents: a loader, an array, or any iterable, read once per run. */
|
|
18
|
+
export type IngestionSource = AsyncIterable<DocumentSource> | Iterable<DocumentSource>;
|
|
19
|
+
/** Options for `createIngestionPipeline()`. */
|
|
20
|
+
export interface IngestionPipelineOptions {
|
|
21
|
+
/** Names the pipeline. Its manifest is kept under this name, so two pipelines never share one. */
|
|
22
|
+
name: string;
|
|
23
|
+
/**
|
|
24
|
+
* Where the manifest is kept: any `Store`, so it lives in memory, Redis, Postgres, or SQLite. It
|
|
25
|
+
* holds one item per document, and a `MemoryStore` keeps 10,000 items unless given a larger
|
|
26
|
+
* `maxItems`: a manifest that forgets a document re-embeds it.
|
|
27
|
+
*/
|
|
28
|
+
manifest: Store;
|
|
29
|
+
/** The vector store chunks are embedded into. */
|
|
30
|
+
vectors: VectorStore;
|
|
31
|
+
/**
|
|
32
|
+
* Keyword indexes kept in step, such as an `ElasticsearchKeywordIndex`. A `PostgresKeywordIndex`
|
|
33
|
+
* that is `shared` needs no place here: the vector store's writes already reach it.
|
|
34
|
+
*/
|
|
35
|
+
keywords?: readonly SparseRetriever[];
|
|
36
|
+
/** Embeds chunk texts. Called only for chunks that are new or changed. */
|
|
37
|
+
embed: EmbeddingProvider;
|
|
38
|
+
/** The embedding model, recorded with every document: a different one re-embeds everything. */
|
|
39
|
+
embeddingModel: string;
|
|
40
|
+
/** How documents are split. Different options re-chunk every document. */
|
|
41
|
+
chunking?: IngestionOptions;
|
|
42
|
+
/** Documents read, embedded, and recorded together. Defaults to 64. */
|
|
43
|
+
batchSize?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Options for one run. */
|
|
46
|
+
export interface IngestionRunOptions {
|
|
47
|
+
/** Stops the run between batches. Everything recorded so far stays recorded. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
/**
|
|
50
|
+
* Removes the documents the manifest has and this run did not see, with their chunks. Defaults to
|
|
51
|
+
* true: the source is the whole corpus. Turn it off when a run loads only part of it.
|
|
52
|
+
*/
|
|
53
|
+
deleteMissing?: boolean;
|
|
54
|
+
/** Called after each batch, with the run so far. */
|
|
55
|
+
onProgress?: (progress: IngestionReport) => void;
|
|
56
|
+
/**
|
|
57
|
+
* The durable operation the run belongs to, when it runs inside an `OperationRunner`. After every
|
|
58
|
+
* batch the run records how far it got with `heartbeat()`, and a later attempt reads it from
|
|
59
|
+
* `previousHeartbeat` and skips straight past what was finished.
|
|
60
|
+
*/
|
|
61
|
+
operation?: Pick<OperationContext, 'heartbeat' | 'previousHeartbeat'>;
|
|
62
|
+
}
|
|
63
|
+
/** What a run did. */
|
|
64
|
+
export interface IngestionReport {
|
|
65
|
+
/** Documents read. */
|
|
66
|
+
documents: number;
|
|
67
|
+
/** Documents whose content, chunking, and model were all as recorded, so nothing was done. */
|
|
68
|
+
unchanged: number;
|
|
69
|
+
/** Documents the manifest did not have. */
|
|
70
|
+
added: number;
|
|
71
|
+
/** Documents that changed. */
|
|
72
|
+
updated: number;
|
|
73
|
+
/** Documents removed because the corpus no longer has them. */
|
|
74
|
+
deleted: number;
|
|
75
|
+
/** Chunks embedded and written. */
|
|
76
|
+
chunksEmbedded: number;
|
|
77
|
+
/** Chunks of changed documents whose text did not change, so their embedding was kept. */
|
|
78
|
+
chunksKept: number;
|
|
79
|
+
/** Chunks removed. */
|
|
80
|
+
chunksDeleted: number;
|
|
81
|
+
/** Documents a resumed run skipped because an earlier attempt had finished them. */
|
|
82
|
+
resumed: number;
|
|
83
|
+
}
|
|
84
|
+
/** What the manifest records for one document. */
|
|
85
|
+
export interface IngestedDocument {
|
|
86
|
+
/** The document's id. */
|
|
87
|
+
id: string;
|
|
88
|
+
/** A hash of its text, source, and metadata. */
|
|
89
|
+
version: string;
|
|
90
|
+
/** A hash of the chunking options it was split with. */
|
|
91
|
+
chunking: string;
|
|
92
|
+
/** The embedding model its chunks were embedded with. */
|
|
93
|
+
model: string;
|
|
94
|
+
/** Each chunk's id, with the hash of its text: the chunk's version. */
|
|
95
|
+
chunks: Record<string, string>;
|
|
96
|
+
/** When it was last written, as ISO-8601. */
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
}
|
|
99
|
+
/** A pipeline: run it as often as the corpus changes. */
|
|
100
|
+
export interface IngestionPipeline {
|
|
101
|
+
/** Brings the stores in step with the documents a source yields. */
|
|
102
|
+
run(source: IngestionSource | readonly IngestionSource[], options?: IngestionRunOptions): Promise<IngestionReport>;
|
|
103
|
+
/** What the manifest records for a document, or `undefined`. */
|
|
104
|
+
document(id: string): Promise<IngestedDocument | undefined>;
|
|
105
|
+
/** Removes documents by id, with their chunks, from every store and the manifest. */
|
|
106
|
+
remove(ids: readonly string[]): Promise<number>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Builds a pipeline that keeps the stores in step with a corpus, re-embedding only what changed.
|
|
110
|
+
*
|
|
111
|
+
* Documents need an `id` (or a `source`), which is what ties a document to its earlier version. A
|
|
112
|
+
* chunk's id is its document's id and a hash of its text, so a chunk whose text did not change keeps
|
|
113
|
+
* its embedding wherever it moved. Chunks are cut by length, within each Markdown section when
|
|
114
|
+
* `splitOnMarkdownHeadings` is on, so an edit re-embeds the chunks of its section from the edit on,
|
|
115
|
+
* and a section added or removed leaves the others alone. One run at a time per pipeline; an
|
|
116
|
+
* operation's idempotency key enforces that across workers.
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* const pipeline = createIngestionPipeline({
|
|
120
|
+
* name: 'help-center',
|
|
121
|
+
* manifest: store,
|
|
122
|
+
* vectors: new PostgresVectorStore(pool, { dimensions: 1536 }),
|
|
123
|
+
* embed,
|
|
124
|
+
* embeddingModel: 'text-embedding-3-small',
|
|
125
|
+
* });
|
|
126
|
+
* const report = await pipeline.run(loadDirectory('./docs'));
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function createIngestionPipeline(options: IngestionPipelineOptions): IngestionPipeline;
|
|
130
|
+
/**
|
|
131
|
+
* The pipeline as the executor of a durable operation: submit it to an `OperationRunner`, and an
|
|
132
|
+
* attempt that dies halfway is recovered by another worker, which starts at the document the first
|
|
133
|
+
* one stopped on.
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* const runner = new OperationRunner({ store: new RedisOperationStore(redis), retry: { maxAttempts: 5 } });
|
|
137
|
+
* await runner.submit(ingestionExecutor(pipeline, () => loadDirectory('./docs')), { idempotencyKey: 'help-center' });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export declare function ingestionExecutor(pipeline: IngestionPipeline, source: () => IngestionSource | readonly IngestionSource[], options?: Omit<IngestionRunOptions, 'operation' | 'signal'>): OperationExecutor<IngestionReport>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { ingestDocuments } from './ingestion.js';
|
|
2
|
+
export function createIngestionPipeline(options) {
|
|
3
|
+
const batchSize = options.batchSize ?? 64;
|
|
4
|
+
if (!(Number.isInteger(batchSize) && batchSize > 0))
|
|
5
|
+
throw new RangeError('batchSize must be a positive integer');
|
|
6
|
+
const namespace = ['nexus', 'ingestion', options.name, 'documents'];
|
|
7
|
+
const keywords = options.keywords ?? [];
|
|
8
|
+
const chunkingVersion = stableJson(options.chunking ?? {});
|
|
9
|
+
const read = async (id) => (await options.manifest.get(namespace, id))?.value;
|
|
10
|
+
const removeChunks = async (ids) => {
|
|
11
|
+
if (ids.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
await Promise.all([options.vectors.delete(ids), ...keywords.map((index) => index.delete(ids))]);
|
|
14
|
+
};
|
|
15
|
+
const remove = async (ids) => {
|
|
16
|
+
let chunks = 0;
|
|
17
|
+
for (const id of ids) {
|
|
18
|
+
const recorded = await read(id);
|
|
19
|
+
if (!recorded)
|
|
20
|
+
continue;
|
|
21
|
+
const chunkIds = Object.keys(recorded.chunks);
|
|
22
|
+
await removeChunks(chunkIds);
|
|
23
|
+
await options.manifest.delete(namespace, id);
|
|
24
|
+
chunks += chunkIds.length;
|
|
25
|
+
}
|
|
26
|
+
return chunks;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
document: read,
|
|
30
|
+
async remove(ids) {
|
|
31
|
+
await remove(ids);
|
|
32
|
+
return ids.length;
|
|
33
|
+
},
|
|
34
|
+
async run(source, runOptions = {}) {
|
|
35
|
+
const report = {
|
|
36
|
+
documents: 0,
|
|
37
|
+
unchanged: 0,
|
|
38
|
+
added: 0,
|
|
39
|
+
updated: 0,
|
|
40
|
+
deleted: 0,
|
|
41
|
+
chunksEmbedded: 0,
|
|
42
|
+
chunksKept: 0,
|
|
43
|
+
chunksDeleted: 0,
|
|
44
|
+
resumed: 0,
|
|
45
|
+
};
|
|
46
|
+
const previous = runOptions.operation?.previousHeartbeat;
|
|
47
|
+
const skip = previous?.pipeline === options.name ? previous.documents : 0;
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
let batch = [];
|
|
50
|
+
const flush = async () => {
|
|
51
|
+
if (batch.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const current = batch;
|
|
54
|
+
batch = [];
|
|
55
|
+
const versions = await Promise.all(current.map(({ document }) => hashText(stableJson([document.text, document.source ?? null, document.metadata ?? null]))));
|
|
56
|
+
const recorded = await Promise.all(current.map(({ id }) => read(id)));
|
|
57
|
+
const writes = [];
|
|
58
|
+
const stale = [];
|
|
59
|
+
const toEmbed = [];
|
|
60
|
+
for (const [index, { document, id }] of current.entries()) {
|
|
61
|
+
const version = versions[index];
|
|
62
|
+
const before = recorded[index];
|
|
63
|
+
if (before &&
|
|
64
|
+
before.version === version &&
|
|
65
|
+
before.chunking === chunkingVersion &&
|
|
66
|
+
before.model === options.embeddingModel) {
|
|
67
|
+
report.unchanged += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (before)
|
|
71
|
+
report.updated += 1;
|
|
72
|
+
else
|
|
73
|
+
report.added += 1;
|
|
74
|
+
const split = ingestDocuments([{ ...document, id }], options.chunking).chunks;
|
|
75
|
+
const hashes = await Promise.all(split.map((chunk) => hashText(stableJson([chunk.content, document.metadata ?? null]))));
|
|
76
|
+
const used = new Map();
|
|
77
|
+
const chunks = split.map((chunk, position) => {
|
|
78
|
+
const base = `${id}#${hashes[position].slice(0, 12)}`;
|
|
79
|
+
const count = (used.get(base) ?? 0) + 1;
|
|
80
|
+
used.set(base, count);
|
|
81
|
+
return { ...chunk, id: count === 1 ? base : `${base}-${count}` };
|
|
82
|
+
});
|
|
83
|
+
const reusable = before && before.model === options.embeddingModel ? before.chunks : {};
|
|
84
|
+
for (const [position, chunk] of chunks.entries()) {
|
|
85
|
+
if (reusable[chunk.id] === hashes[position])
|
|
86
|
+
report.chunksKept += 1;
|
|
87
|
+
else
|
|
88
|
+
toEmbed.push(chunk);
|
|
89
|
+
}
|
|
90
|
+
const kept = new Set(chunks.map((chunk) => chunk.id));
|
|
91
|
+
if (before)
|
|
92
|
+
stale.push(...Object.keys(before.chunks).filter((chunkId) => !kept.has(chunkId) || !reusable[chunkId]));
|
|
93
|
+
writes.push({ id, version, chunks, hashes });
|
|
94
|
+
}
|
|
95
|
+
if (toEmbed.length > 0) {
|
|
96
|
+
const vectors = await options.embed(toEmbed.map((chunk) => chunk.content));
|
|
97
|
+
if (vectors.length !== toEmbed.length) {
|
|
98
|
+
throw new RangeError(`The embedding function returned ${vectors.length} vectors for ${toEmbed.length} texts`);
|
|
99
|
+
}
|
|
100
|
+
await Promise.all([
|
|
101
|
+
options.vectors.add(toEmbed.map((chunk, index) => ({ ...chunk, embedding: vectors[index] }))),
|
|
102
|
+
...keywords.map((index) => index.add(toEmbed)),
|
|
103
|
+
]);
|
|
104
|
+
report.chunksEmbedded += toEmbed.length;
|
|
105
|
+
}
|
|
106
|
+
const embeddedIds = new Set(toEmbed.map((chunk) => chunk.id));
|
|
107
|
+
const removable = stale.filter((chunkId) => !embeddedIds.has(chunkId));
|
|
108
|
+
await removeChunks(removable);
|
|
109
|
+
report.chunksDeleted += removable.length;
|
|
110
|
+
const updatedAt = new Date().toISOString();
|
|
111
|
+
await Promise.all(writes.map(({ id, version, chunks, hashes }) => options.manifest.put(namespace, id, {
|
|
112
|
+
id,
|
|
113
|
+
version,
|
|
114
|
+
chunking: chunkingVersion,
|
|
115
|
+
model: options.embeddingModel,
|
|
116
|
+
chunks: Object.fromEntries(chunks.map((chunk, index) => [chunk.id, hashes[index]])),
|
|
117
|
+
updatedAt,
|
|
118
|
+
})));
|
|
119
|
+
await runOptions.operation?.heartbeat({
|
|
120
|
+
pipeline: options.name,
|
|
121
|
+
documents: report.documents,
|
|
122
|
+
});
|
|
123
|
+
runOptions.onProgress?.({ ...report });
|
|
124
|
+
};
|
|
125
|
+
const sources = isSourceList(source) ? source : [source];
|
|
126
|
+
for (const loader of sources) {
|
|
127
|
+
for await (const document of loader) {
|
|
128
|
+
runOptions.signal?.throwIfAborted();
|
|
129
|
+
const id = document.id ?? document.source;
|
|
130
|
+
if (!id)
|
|
131
|
+
throw new TypeError('Durable ingestion needs a document id, or a source to use as one');
|
|
132
|
+
seen.add(id);
|
|
133
|
+
report.documents += 1;
|
|
134
|
+
if (report.documents <= skip) {
|
|
135
|
+
report.resumed += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
batch.push({ document, id });
|
|
139
|
+
if (batch.length >= batchSize)
|
|
140
|
+
await flush();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
runOptions.signal?.throwIfAborted();
|
|
144
|
+
await flush();
|
|
145
|
+
if (runOptions.deleteMissing !== false) {
|
|
146
|
+
const missing = [];
|
|
147
|
+
for (let offset = 0;; offset += 500) {
|
|
148
|
+
const page = await options.manifest.search(namespace, { limit: 500, offset });
|
|
149
|
+
for (const item of page)
|
|
150
|
+
if (!seen.has(item.key))
|
|
151
|
+
missing.push(item.key);
|
|
152
|
+
if (page.length < 500)
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
for (const id of missing) {
|
|
156
|
+
runOptions.signal?.throwIfAborted();
|
|
157
|
+
report.chunksDeleted += await remove([id]);
|
|
158
|
+
report.deleted += 1;
|
|
159
|
+
}
|
|
160
|
+
if (missing.length > 0)
|
|
161
|
+
runOptions.onProgress?.({ ...report });
|
|
162
|
+
}
|
|
163
|
+
return report;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function ingestionExecutor(pipeline, source, options = {}) {
|
|
168
|
+
return (context) => pipeline.run(source(), { ...options, signal: context.signal, operation: context });
|
|
169
|
+
}
|
|
170
|
+
async function hashText(text) {
|
|
171
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
172
|
+
let hex = '';
|
|
173
|
+
for (const byte of new Uint8Array(digest).subarray(0, 16))
|
|
174
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
175
|
+
return hex;
|
|
176
|
+
}
|
|
177
|
+
function stableJson(value) {
|
|
178
|
+
return JSON.stringify(value, (_key, entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
179
|
+
? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)))
|
|
180
|
+
: entry);
|
|
181
|
+
}
|
|
182
|
+
function isSourceList(value) {
|
|
183
|
+
if (!Array.isArray(value))
|
|
184
|
+
return false;
|
|
185
|
+
const first = value[0];
|
|
186
|
+
return first !== undefined && typeof first === 'object' && first !== null && !('text' in first);
|
|
187
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Reranker } from './retrievers.js';
|
|
2
|
+
/** Raised when a rerank API answers with an error. */
|
|
3
|
+
export declare class RerankerError extends Error {
|
|
4
|
+
/** The HTTP status the API answered with. */
|
|
5
|
+
readonly status: number;
|
|
6
|
+
/** The API's response body, as text. */
|
|
7
|
+
readonly body?: string | undefined;
|
|
8
|
+
constructor(message: string,
|
|
9
|
+
/** The HTTP status the API answered with. */
|
|
10
|
+
status: number,
|
|
11
|
+
/** The API's response body, as text. */
|
|
12
|
+
body?: string | undefined);
|
|
13
|
+
}
|
|
14
|
+
/** What every HTTP reranker takes. */
|
|
15
|
+
export interface RerankerHttpOptions {
|
|
16
|
+
/** Replaces the global `fetch`, for a proxy, retries, or tests. */
|
|
17
|
+
fetch?: typeof globalThis.fetch;
|
|
18
|
+
/** Headers added to every request. */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
/** Aborts a request that takes longer, in milliseconds. Defaults to 30 seconds. */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Characters of each chunk sent. Defaults to 4,000. */
|
|
23
|
+
maxChunkCharacters?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Options for a hosted rerank API. */
|
|
26
|
+
export interface HostedRerankerOptions extends RerankerHttpOptions {
|
|
27
|
+
/** The API key. */
|
|
28
|
+
apiKey: string;
|
|
29
|
+
/** The rerank model, such as `rerank-v3.5`. */
|
|
30
|
+
model: string;
|
|
31
|
+
/** Replaces the API's endpoint, for a regional or proxied one. */
|
|
32
|
+
url?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Options for `httpReranker()`: any rerank API, described by its request and its response. */
|
|
35
|
+
export interface HttpRerankerOptions extends RerankerHttpOptions {
|
|
36
|
+
/** The endpoint. */
|
|
37
|
+
url: string;
|
|
38
|
+
/** The request body for a query and its passages. */
|
|
39
|
+
body: (query: string, passages: string[]) => unknown;
|
|
40
|
+
/** Reads the response: each passage's index and score. Passages it leaves out score 0. */
|
|
41
|
+
scores: (response: unknown) => ReadonlyArray<{
|
|
42
|
+
index: number;
|
|
43
|
+
score: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A reranker over any HTTP rerank API: you describe the request body and how to read the scores,
|
|
48
|
+
* and it handles the rest — truncation, the timeout, cancellation, errors, and scores put back in
|
|
49
|
+
* the order of the chunks.
|
|
50
|
+
*/
|
|
51
|
+
export declare function httpReranker(options: HttpRerankerOptions): Reranker;
|
|
52
|
+
/** Cohere's rerank API (`/v2/rerank`), such as `rerank-v3.5`. */
|
|
53
|
+
export declare function cohereReranker(options: HostedRerankerOptions): Reranker;
|
|
54
|
+
/** Voyage AI's rerank API (`/v1/rerank`), such as `rerank-2`. */
|
|
55
|
+
export declare function voyageReranker(options: HostedRerankerOptions): Reranker;
|
|
56
|
+
/** Jina AI's rerank API (`/v1/rerank`), such as `jina-reranker-v2-base-multilingual`. */
|
|
57
|
+
export declare function jinaReranker(options: HostedRerankerOptions): Reranker;
|
|
58
|
+
/** Options for `teiReranker()`. */
|
|
59
|
+
export interface TeiRerankerOptions extends RerankerHttpOptions {
|
|
60
|
+
/** The server, such as `http://localhost:8080`. */
|
|
61
|
+
url: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A cross-encoder served by Hugging Face's text-embeddings-inference, on your own machine or
|
|
65
|
+
* cluster: `docker run ... --model-id BAAI/bge-reranker-base`, then `teiReranker({ url })`. The
|
|
66
|
+
* scores are the model's own, from 0 to 1.
|
|
67
|
+
*/
|
|
68
|
+
export declare function teiReranker(options: TeiRerankerOptions): Reranker;
|
|
69
|
+
/**
|
|
70
|
+
* A cross-encoder run in process: one score per (query, passage) pair, higher meaning more relevant.
|
|
71
|
+
* A model from an ONNX runtime or a transformers library fits it in a few lines, so the package
|
|
72
|
+
* depends on neither.
|
|
73
|
+
*/
|
|
74
|
+
export interface CrossEncoder {
|
|
75
|
+
/** Scores each pair, in order. */
|
|
76
|
+
score(pairs: ReadonlyArray<readonly [query: string, passage: string]>): Promise<readonly number[]> | readonly number[];
|
|
77
|
+
}
|
|
78
|
+
/** Options for `crossEncoderReranker()`. */
|
|
79
|
+
export interface CrossEncoderRerankerOptions {
|
|
80
|
+
/** Pairs scored in one call. Defaults to 32. */
|
|
81
|
+
batchSize?: number;
|
|
82
|
+
/** Characters of each chunk scored. Defaults to 2,000. */
|
|
83
|
+
maxChunkCharacters?: number;
|
|
84
|
+
/** Maps raw logits to 0–1 with a sigmoid, for a model that returns logits. Defaults to false. */
|
|
85
|
+
sigmoid?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A reranker over a cross-encoder you run in process: no network call, no per-query price, and the
|
|
89
|
+
* passages never leave the machine. Pairs are scored in batches, and the run's signal is checked
|
|
90
|
+
* between batches.
|
|
91
|
+
*/
|
|
92
|
+
export declare function crossEncoderReranker(model: CrossEncoder, options?: CrossEncoderRerankerOptions): Reranker;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { linkSignals } from '../utils/signals.js';
|
|
2
|
+
export class RerankerError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.name = 'RerankerError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function httpReranker(options) {
|
|
13
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
14
|
+
const limit = options.maxChunkCharacters ?? 4_000;
|
|
15
|
+
return async (query, chunks, signal) => {
|
|
16
|
+
if (chunks.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const passages = chunks.map((chunk) => chunk.content.slice(0, limit));
|
|
19
|
+
const linked = linkSignals([AbortSignal.timeout(options.timeoutMs ?? 30_000), signal]);
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetchImpl(options.url, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'content-type': 'application/json', ...options.headers },
|
|
24
|
+
body: JSON.stringify(options.body(query, passages)),
|
|
25
|
+
signal: linked.signal,
|
|
26
|
+
});
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new RerankerError(`The rerank API answered ${response.status}`, response.status, text);
|
|
30
|
+
const scores = new Array(chunks.length).fill(0);
|
|
31
|
+
for (const { index, score } of options.scores(JSON.parse(text))) {
|
|
32
|
+
if (Number.isInteger(index) && index >= 0 && index < scores.length)
|
|
33
|
+
scores[index] = Number(score);
|
|
34
|
+
}
|
|
35
|
+
return scores;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
linked.dispose();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const bearer = (options) => ({ authorization: `Bearer ${options.apiKey}`, ...options.headers });
|
|
43
|
+
const readScored = (list) => (list ?? []).map((item) => ({ index: item.index, score: item.relevance_score }));
|
|
44
|
+
export function cohereReranker(options) {
|
|
45
|
+
return httpReranker({
|
|
46
|
+
...options,
|
|
47
|
+
url: options.url ?? 'https://api.cohere.com/v2/rerank',
|
|
48
|
+
headers: bearer(options),
|
|
49
|
+
body: (query, documents) => ({ model: options.model, query, documents, top_n: documents.length }),
|
|
50
|
+
scores: (response) => readScored(response.results),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function voyageReranker(options) {
|
|
54
|
+
return httpReranker({
|
|
55
|
+
...options,
|
|
56
|
+
url: options.url ?? 'https://api.voyageai.com/v1/rerank',
|
|
57
|
+
headers: bearer(options),
|
|
58
|
+
body: (query, documents) => ({ model: options.model, query, documents, top_k: documents.length }),
|
|
59
|
+
scores: (response) => readScored(response.data),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function jinaReranker(options) {
|
|
63
|
+
return httpReranker({
|
|
64
|
+
...options,
|
|
65
|
+
url: options.url ?? 'https://api.jina.ai/v1/rerank',
|
|
66
|
+
headers: bearer(options),
|
|
67
|
+
body: (query, documents) => ({ model: options.model, query, documents, top_n: documents.length }),
|
|
68
|
+
scores: (response) => readScored(response.results),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function teiReranker(options) {
|
|
72
|
+
return httpReranker({
|
|
73
|
+
...options,
|
|
74
|
+
url: `${options.url.replace(/\/+$/, '')}/rerank`,
|
|
75
|
+
body: (query, texts) => ({ query, texts, raw_scores: false, truncate: true }),
|
|
76
|
+
scores: (response) => response,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export function crossEncoderReranker(model, options = {}) {
|
|
80
|
+
const batchSize = Math.max(1, options.batchSize ?? 32);
|
|
81
|
+
const limit = options.maxChunkCharacters ?? 2_000;
|
|
82
|
+
return async (query, chunks, signal) => {
|
|
83
|
+
const pairs = chunks.map((chunk) => [query, chunk.content.slice(0, limit)]);
|
|
84
|
+
const scores = [];
|
|
85
|
+
for (let start = 0; start < pairs.length; start += batchSize) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
const batch = pairs.slice(start, start + batchSize);
|
|
88
|
+
const result = await model.score(batch);
|
|
89
|
+
if (result.length !== batch.length) {
|
|
90
|
+
throw new RangeError(`The cross-encoder returned ${result.length} scores for ${batch.length} pairs`);
|
|
91
|
+
}
|
|
92
|
+
for (const raw of result)
|
|
93
|
+
scores.push(options.sigmoid ? 1 / (1 + Math.exp(-raw)) : raw);
|
|
94
|
+
}
|
|
95
|
+
return scores;
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/rag/retrievers.d.ts
CHANGED
|
@@ -19,6 +19,17 @@ export interface Retriever {
|
|
|
19
19
|
/** The chunks that best answer a query, best first. */
|
|
20
20
|
retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Keyword search that holds its own chunks: an in-memory BM25 `KeywordIndex`, Postgres full-text
|
|
24
|
+
* search, a search engine. It is a `Retriever`, so `hybridRetriever()` fuses it with vector search,
|
|
25
|
+
* and it takes writes, so ingestion keeps it in step with a vector store.
|
|
26
|
+
*/
|
|
27
|
+
export interface SparseRetriever extends Retriever {
|
|
28
|
+
/** Adds chunks, or replaces those whose id exists. */
|
|
29
|
+
add(chunks: readonly RagChunk[]): Promise<void> | void;
|
|
30
|
+
/** Removes chunks by id. An id that is not held is ignored. */
|
|
31
|
+
delete(ids: readonly string[]): Promise<void> | void;
|
|
32
|
+
}
|
|
22
33
|
/** A client that runs one completion — a `NexusAI` instance, or anything with the same `complete()`. */
|
|
23
34
|
export interface RetrievalModelClient {
|
|
24
35
|
/** Runs one completion and returns its text. */
|
|
@@ -47,7 +58,7 @@ export declare function tokenize(text: string): string[];
|
|
|
47
58
|
* codes, error numbers, names — that keyword search finds; `hybridRetriever()` fuses the two.
|
|
48
59
|
* Adding a chunk whose id exists replaces it, as in a vector store.
|
|
49
60
|
*/
|
|
50
|
-
export declare class KeywordIndex implements
|
|
61
|
+
export declare class KeywordIndex implements SparseRetriever {
|
|
51
62
|
private readonly chunks;
|
|
52
63
|
private readonly postings;
|
|
53
64
|
private totalLength;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphStreamProjection } from '../types/graph.js';
|
|
2
|
-
import type { AssistantRunContext, ServerAssistant } from '../types/server.js';
|
|
2
|
+
import type { AssistantRunContext, Principal, ServerAssistant } from '../types/server.js';
|
|
3
3
|
/**
|
|
4
4
|
* The part of a compiled graph the adapter uses.
|
|
5
5
|
*
|
|
@@ -13,6 +13,7 @@ export interface GraphLike {
|
|
|
13
13
|
signal?: AbortSignal;
|
|
14
14
|
metadata?: Record<string, unknown>;
|
|
15
15
|
tenantId?: string;
|
|
16
|
+
principal?: Principal;
|
|
16
17
|
control?: DrainSwitch;
|
|
17
18
|
}): AsyncIterable<unknown>;
|
|
18
19
|
/** Answers an interrupt and continues the thread. */
|
|
@@ -20,6 +21,7 @@ export interface GraphLike {
|
|
|
20
21
|
signal?: AbortSignal;
|
|
21
22
|
metadata?: Record<string, unknown>;
|
|
22
23
|
tenantId?: string;
|
|
24
|
+
principal?: Principal;
|
|
23
25
|
control?: DrainSwitch;
|
|
24
26
|
}): AsyncIterable<unknown>;
|
|
25
27
|
/** The thread's checkpoint, at its latest step or at one given. */
|
|
@@ -34,6 +36,7 @@ export interface GraphLike {
|
|
|
34
36
|
signal?: AbortSignal;
|
|
35
37
|
metadata?: Record<string, unknown>;
|
|
36
38
|
tenantId?: string;
|
|
39
|
+
principal?: Principal;
|
|
37
40
|
control?: DrainSwitch;
|
|
38
41
|
}): AsyncIterable<unknown>;
|
|
39
42
|
/** Writes values into a thread's state, which is how a rollback is applied. */
|
|
@@ -51,6 +54,7 @@ export interface GraphLike {
|
|
|
51
54
|
type GraphEventOptionsLike = {
|
|
52
55
|
threadId?: string;
|
|
53
56
|
tenantId?: string;
|
|
57
|
+
principal?: Principal;
|
|
54
58
|
signal?: AbortSignal;
|
|
55
59
|
metadata?: Record<string, unknown>;
|
|
56
60
|
control?: DrainSwitch;
|
package/dist/server/assistant.js
CHANGED
|
@@ -81,8 +81,10 @@ export function functionAssistant(run, options = {}) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
function tenantOf(context) {
|
|
84
|
-
const
|
|
85
|
-
|
|
84
|
+
const principal = context.principal;
|
|
85
|
+
if (!principal)
|
|
86
|
+
return {};
|
|
87
|
+
return { ...(principal.tenantId ? { tenantId: principal.tenantId } : {}), principal };
|
|
86
88
|
}
|
|
87
89
|
async function* withResult(events) {
|
|
88
90
|
for await (const event of events)
|