nexus-ai-pro 2.1.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 +77 -0
- package/CHANGELOG.md +286 -1
- package/README.md +19 -9
- 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/cache/memory-cache.d.ts +8 -0
- package/dist/cache/memory-cache.js +9 -0
- package/dist/cache/semantic-cache.d.ts +6 -3
- package/dist/cache/semantic-cache.js +5 -2
- package/dist/cli/args.js +3 -0
- package/dist/cli/db.d.ts +16 -4
- package/dist/cli/db.js +169 -22
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +53 -0
- package/dist/cli.js +12 -1
- package/dist/core/lifecycle.js +11 -6
- package/dist/core/nexus.d.ts +6 -0
- package/dist/core/nexus.js +103 -22
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/doctor/index.d.ts +90 -0
- package/dist/doctor/index.js +237 -0
- package/dist/graph/functional.d.ts +89 -7
- package/dist/graph/functional.js +156 -36
- package/dist/graph/graph.d.ts +15 -5
- package/dist/graph/graph.js +95 -22
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/images/evals.js +4 -2
- package/dist/operations/adapters.d.ts +52 -4
- package/dist/operations/adapters.js +194 -7
- package/dist/operations/runner.js +1 -0
- package/dist/operations/stats.js +8 -1
- package/dist/operations/store.d.ts +5 -1
- package/dist/operations/store.js +7 -0
- package/dist/ops/circuit-breaker.d.ts +52 -0
- package/dist/ops/circuit-breaker.js +4 -0
- package/dist/ops/circuit-store.d.ts +13 -2
- package/dist/ops/circuit-store.js +68 -0
- package/dist/ops/circuit-sync.d.ts +17 -0
- package/dist/ops/circuit-sync.js +86 -0
- package/dist/ops/rate-limiter.d.ts +2 -0
- package/dist/ops/rate-limiter.js +2 -0
- package/dist/postgres/circuits.d.ts +21 -4
- package/dist/postgres/circuits.js +65 -4
- package/dist/postgres/client.d.ts +0 -2
- package/dist/postgres/client.js +0 -4
- package/dist/postgres/evaluate.d.ts +16 -4
- package/dist/postgres/evaluate.js +17 -5
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/postgres/index.d.ts +32 -11
- package/dist/postgres/index.js +65 -31
- package/dist/postgres/migrations.d.ts +60 -0
- package/dist/postgres/migrations.js +121 -0
- package/dist/postgres/operations.d.ts +14 -3
- package/dist/postgres/operations.js +45 -15
- package/dist/postgres/prompts.d.ts +11 -2
- package/dist/postgres/prompts.js +14 -3
- package/dist/postgres/rollups.d.ts +35 -0
- package/dist/postgres/rollups.js +129 -0
- package/dist/postgres/store.d.ts +11 -2
- package/dist/postgres/store.js +14 -3
- package/dist/postgres/traces.d.ts +13 -3
- package/dist/postgres/traces.js +25 -3
- package/dist/postgres/vectors.d.ts +11 -2
- package/dist/postgres/vectors.js +14 -3
- 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/router/auto-router.d.ts +34 -7
- package/dist/router/auto-router.js +201 -68
- package/dist/server/assistant.d.ts +9 -1
- package/dist/server/assistant.js +10 -0
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/runs.d.ts +24 -1
- package/dist/server/runs.js +90 -14
- package/dist/sqlite/index.d.ts +16 -3
- package/dist/sqlite/index.js +16 -3
- package/dist/sqlite/migrations.d.ts +26 -0
- package/dist/sqlite/migrations.js +63 -0
- package/dist/sqlite/operations.d.ts +14 -3
- package/dist/sqlite/operations.js +42 -15
- package/dist/sqlite/store.d.ts +9 -2
- package/dist/sqlite/store.js +13 -3
- package/dist/sqlite/vectors.d.ts +11 -2
- package/dist/sqlite/vectors.js +13 -2
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/store/tenant.d.ts +12 -0
- package/dist/store/tenant.js +29 -0
- package/dist/tenancy/index.d.ts +113 -0
- package/dist/tenancy/index.js +233 -0
- package/dist/tracing/instrument.js +2 -0
- package/dist/types/config.d.ts +24 -2
- package/dist/types/graph.d.ts +49 -1
- package/dist/types/messages.d.ts +41 -2
- package/dist/types/operations.d.ts +6 -0
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/response.d.ts +20 -0
- package/dist/types/server.d.ts +12 -10
- package/dist/utils/schema-migrations.d.ts +97 -0
- package/dist/utils/schema-migrations.js +91 -0
- package/dist/utils/signals.d.ts +17 -0
- package/dist/utils/signals.js +26 -0
- package/dist/utils/tenant.d.ts +25 -0
- package/dist/utils/tenant.js +30 -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/cache/memory-cache.d.ts +8 -0
- package/dist-cjs/cache/memory-cache.js +9 -0
- package/dist-cjs/cache/semantic-cache.d.ts +6 -3
- package/dist-cjs/cache/semantic-cache.js +5 -2
- package/dist-cjs/cli/args.js +3 -0
- package/dist-cjs/cli/db.d.ts +16 -4
- package/dist-cjs/cli/db.js +200 -20
- package/dist-cjs/cli/doctor.d.ts +17 -0
- package/dist-cjs/cli/doctor.js +56 -0
- package/dist-cjs/core/lifecycle.js +11 -6
- package/dist-cjs/core/nexus.d.ts +6 -0
- package/dist-cjs/core/nexus.js +103 -22
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/doctor/index.d.ts +90 -0
- package/dist-cjs/doctor/index.js +276 -0
- package/dist-cjs/graph/functional.d.ts +89 -7
- package/dist-cjs/graph/functional.js +155 -35
- package/dist-cjs/graph/graph.d.ts +15 -5
- package/dist-cjs/graph/graph.js +95 -22
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/images/evals.js +4 -2
- package/dist-cjs/operations/adapters.d.ts +52 -4
- package/dist-cjs/operations/adapters.js +193 -6
- package/dist-cjs/operations/runner.js +1 -0
- package/dist-cjs/operations/stats.js +8 -1
- package/dist-cjs/operations/store.d.ts +5 -1
- package/dist-cjs/operations/store.js +7 -0
- package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
- package/dist-cjs/ops/circuit-breaker.js +4 -0
- package/dist-cjs/ops/circuit-store.d.ts +13 -2
- package/dist-cjs/ops/circuit-store.js +68 -0
- package/dist-cjs/ops/circuit-sync.d.ts +17 -0
- package/dist-cjs/ops/circuit-sync.js +87 -0
- package/dist-cjs/ops/rate-limiter.d.ts +2 -0
- package/dist-cjs/ops/rate-limiter.js +2 -0
- package/dist-cjs/postgres/circuits.d.ts +21 -4
- package/dist-cjs/postgres/circuits.js +98 -3
- package/dist-cjs/postgres/client.d.ts +0 -2
- package/dist-cjs/postgres/client.js +0 -5
- package/dist-cjs/postgres/evaluate.d.ts +16 -4
- package/dist-cjs/postgres/evaluate.js +50 -4
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/postgres/index.d.ts +32 -11
- package/dist-cjs/postgres/index.js +69 -20
- package/dist-cjs/postgres/migrations.d.ts +60 -0
- package/dist-cjs/postgres/migrations.js +129 -0
- package/dist-cjs/postgres/operations.d.ts +14 -3
- package/dist-cjs/postgres/operations.js +78 -14
- package/dist-cjs/postgres/prompts.d.ts +11 -2
- package/dist-cjs/postgres/prompts.js +47 -2
- package/dist-cjs/postgres/rollups.d.ts +35 -0
- package/dist-cjs/postgres/rollups.js +168 -0
- package/dist-cjs/postgres/store.d.ts +11 -2
- package/dist-cjs/postgres/store.js +47 -2
- package/dist-cjs/postgres/traces.d.ts +13 -3
- package/dist-cjs/postgres/traces.js +58 -2
- package/dist-cjs/postgres/vectors.d.ts +11 -2
- package/dist-cjs/postgres/vectors.js +47 -2
- 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/router/auto-router.d.ts +34 -7
- package/dist-cjs/router/auto-router.js +201 -68
- package/dist-cjs/server/assistant.d.ts +9 -1
- package/dist-cjs/server/assistant.js +10 -0
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/server/errors.d.ts +12 -0
- package/dist-cjs/server/errors.js +9 -1
- package/dist-cjs/server/index.d.ts +1 -1
- package/dist-cjs/server/index.js +2 -1
- package/dist-cjs/server/runs.d.ts +24 -1
- package/dist-cjs/server/runs.js +89 -13
- package/dist-cjs/sqlite/index.d.ts +16 -3
- package/dist-cjs/sqlite/index.js +31 -10
- package/dist-cjs/sqlite/migrations.d.ts +26 -0
- package/dist-cjs/sqlite/migrations.js +71 -0
- package/dist-cjs/sqlite/operations.d.ts +14 -3
- package/dist-cjs/sqlite/operations.js +76 -15
- package/dist-cjs/sqlite/store.d.ts +9 -2
- package/dist-cjs/sqlite/store.js +47 -3
- package/dist-cjs/sqlite/vectors.d.ts +11 -2
- package/dist-cjs/sqlite/vectors.js +47 -2
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/store/tenant.d.ts +12 -0
- package/dist-cjs/store/tenant.js +33 -0
- package/dist-cjs/tenancy/index.d.ts +113 -0
- package/dist-cjs/tenancy/index.js +250 -0
- package/dist-cjs/tracing/instrument.js +2 -0
- package/dist-cjs/types/config.d.ts +24 -2
- package/dist-cjs/types/graph.d.ts +49 -1
- package/dist-cjs/types/messages.d.ts +41 -2
- package/dist-cjs/types/operations.d.ts +6 -0
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/response.d.ts +20 -0
- package/dist-cjs/types/server.d.ts +12 -10
- package/dist-cjs/utils/schema-migrations.d.ts +97 -0
- package/dist-cjs/utils/schema-migrations.js +102 -0
- package/dist-cjs/utils/signals.d.ts +17 -0
- package/dist-cjs/utils/signals.js +29 -0
- package/dist-cjs/utils/tenant.d.ts +25 -0
- package/dist-cjs/utils/tenant.js +37 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +6 -4
- package/package.json +188 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EmbeddingProvider, type VectorDocument, type VectorSearchOptions, type VectorSearchResult, type VectorStore } from '../hallucination/retrieval.js';
|
|
2
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
2
3
|
import { type PostgresLikeClient } from './client.js';
|
|
4
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
3
5
|
/** Options for the pgvector store. */
|
|
4
6
|
export interface PostgresVectorStoreOptions {
|
|
5
7
|
/** Width of every vector. Must match the embedding function's output. */
|
|
@@ -15,6 +17,10 @@ export interface PostgresVectorStoreOptions {
|
|
|
15
17
|
*/
|
|
16
18
|
index?: 'hnsw' | 'none';
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* The versioned schema, which `migrate()` and `nexus db migrate` apply. Version 1 is the 2.0 schema.
|
|
22
|
+
*/
|
|
23
|
+
export declare function vectorStoreMigrations(options: Omit<PostgresVectorStoreOptions, 'embed'>): SchemaMigration[];
|
|
18
24
|
/** The schema, as statements: the pgvector extension, the table, and its index. */
|
|
19
25
|
export declare function vectorStoreMigration(options: Omit<PostgresVectorStoreOptions, 'embed'>): string[];
|
|
20
26
|
/**
|
|
@@ -30,8 +36,11 @@ export declare class PostgresVectorStore implements VectorStore {
|
|
|
30
36
|
private readonly table;
|
|
31
37
|
private readonly embed;
|
|
32
38
|
constructor(client: PostgresLikeClient, options: PostgresVectorStoreOptions);
|
|
33
|
-
/**
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Applies this store's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
41
|
+
* Never runs implicitly. Safe on a database an older release created: what exists is kept.
|
|
42
|
+
*/
|
|
43
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
35
44
|
/** Adds chunks, or replaces those whose id exists, embedding those without a vector in one batch. */
|
|
36
45
|
add(documents: VectorDocument[]): Promise<void>;
|
|
37
46
|
/** The chunks most similar to a query, best first. */
|
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.PostgresVectorStore = void 0;
|
|
37
|
+
exports.vectorStoreMigrations = vectorStoreMigrations;
|
|
4
38
|
exports.vectorStoreMigration = vectorStoreMigration;
|
|
5
39
|
const retrieval_js_1 = require("../hallucination/retrieval.js");
|
|
6
40
|
const client_js_1 = require("./client.js");
|
|
7
41
|
const sql_js_1 = require("./sql.js");
|
|
8
42
|
const DEFAULT_TABLE = 'nexus_vectors';
|
|
43
|
+
function vectorStoreMigrations(options) {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
component: `vectors:${options.table ?? DEFAULT_TABLE}`,
|
|
47
|
+
version: 1,
|
|
48
|
+
name: 'create the vectors table',
|
|
49
|
+
statements: vectorStoreMigration(options),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
9
53
|
function vectorStoreMigration(options) {
|
|
10
54
|
const dimensions = options.dimensions;
|
|
11
55
|
if (!(Number.isInteger(dimensions) && dimensions > 0))
|
|
@@ -42,8 +86,9 @@ class PostgresVectorStore {
|
|
|
42
86
|
this.table = (0, client_js_1.quoteTable)(options.table ?? DEFAULT_TABLE);
|
|
43
87
|
this.embed = options.embed ?? ((texts) => (0, retrieval_js_1.createHashEmbeddings)(texts, options.dimensions));
|
|
44
88
|
}
|
|
45
|
-
async migrate() {
|
|
46
|
-
await (
|
|
89
|
+
async migrate(options = {}) {
|
|
90
|
+
const { applyPostgresMigrations } = await Promise.resolve().then(() => __importStar(require('./migrations.js')));
|
|
91
|
+
return applyPostgresMigrations(this.client, vectorStoreMigrations(this.options), options);
|
|
47
92
|
}
|
|
48
93
|
async add(documents) {
|
|
49
94
|
if (documents.length === 0)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RagChunk } from '../hallucination/rag.js';
|
|
2
|
+
import type { VectorSearchResult } from '../hallucination/retrieval.js';
|
|
3
|
+
import type { RetrieveOptions, SparseRetriever } from './retrievers.js';
|
|
4
|
+
/** Options for the Elasticsearch keyword index. */
|
|
5
|
+
export interface ElasticsearchKeywordIndexOptions {
|
|
6
|
+
/** The cluster, such as `http://localhost:9200`, an Elastic Cloud endpoint, or an OpenSearch domain. */
|
|
7
|
+
url: string;
|
|
8
|
+
/** The index chunks are kept in. */
|
|
9
|
+
index: string;
|
|
10
|
+
/** Sent as `Authorization: ApiKey <key>`. */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
/** Sent as basic authentication, for a cluster with users instead of API keys. */
|
|
13
|
+
username?: string;
|
|
14
|
+
/** The password for `username`. */
|
|
15
|
+
password?: string;
|
|
16
|
+
/** The analyzer `migrate()` gives the `content` field, such as `english`. Defaults to the cluster's standard one. */
|
|
17
|
+
analyzer?: string;
|
|
18
|
+
/**
|
|
19
|
+
* When a write becomes searchable. `wait_for` (the default) returns once a search would see it, as
|
|
20
|
+
* the vector stores do; `false` returns sooner and leaves it to the index's refresh interval.
|
|
21
|
+
*/
|
|
22
|
+
refresh?: 'wait_for' | boolean;
|
|
23
|
+
/** Replaces the global `fetch`, for a proxy, retries, signed requests, or tests. */
|
|
24
|
+
fetch?: typeof globalThis.fetch;
|
|
25
|
+
/** Headers added to every request. */
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
/** Aborts a request that takes longer, in milliseconds. Defaults to 30 seconds. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Raised when the cluster answers with an error. */
|
|
31
|
+
export declare class ElasticsearchError extends Error {
|
|
32
|
+
/** The HTTP status the cluster answered with. */
|
|
33
|
+
readonly status: number;
|
|
34
|
+
/** The cluster's response body, as text. */
|
|
35
|
+
readonly body?: string | undefined;
|
|
36
|
+
constructor(message: string,
|
|
37
|
+
/** The HTTP status the cluster answered with. */
|
|
38
|
+
status: number,
|
|
39
|
+
/** The cluster's response body, as text. */
|
|
40
|
+
body?: string | undefined);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Keyword search in Elasticsearch or OpenSearch, through their REST API.
|
|
44
|
+
*
|
|
45
|
+
* No client library is a dependency: the index speaks HTTP through `fetch`, as the vector stores do,
|
|
46
|
+
* so it runs wherever `fetch` does. Scores are the engine's own BM25, ranked in the cluster, so only
|
|
47
|
+
* the best matches come back. A `SparseRetriever`, so `hybridRetriever()` fuses it with any vector
|
|
48
|
+
* store. A metadata filter is an exact match on top-level fields, as in the vector stores.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ElasticsearchKeywordIndex implements SparseRetriever {
|
|
51
|
+
private readonly options;
|
|
52
|
+
private readonly base;
|
|
53
|
+
private readonly fetchImpl;
|
|
54
|
+
constructor(options: ElasticsearchKeywordIndexOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Creates the index unless it exists: `content` as analyzed text, `id` and `source` as keywords,
|
|
57
|
+
* and every string under `metadata` as a keyword, so a filter matches it exactly. Never runs
|
|
58
|
+
* implicitly.
|
|
59
|
+
*/
|
|
60
|
+
migrate(): Promise<void>;
|
|
61
|
+
/** Adds chunks, or replaces those whose id exists, in one bulk request. */
|
|
62
|
+
add(chunks: readonly RagChunk[]): Promise<void>;
|
|
63
|
+
/** Removes chunks by id. An id that is not held is ignored. */
|
|
64
|
+
delete(ids: readonly string[]): Promise<void>;
|
|
65
|
+
/** The chunks that best match a query's terms, best first, scored by the engine's BM25. */
|
|
66
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
|
|
67
|
+
/** Chunks held. */
|
|
68
|
+
size(): Promise<number>;
|
|
69
|
+
private bulk;
|
|
70
|
+
private request;
|
|
71
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ElasticsearchKeywordIndex = exports.ElasticsearchError = void 0;
|
|
4
|
+
const signals_js_1 = require("../utils/signals.js");
|
|
5
|
+
class ElasticsearchError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
body;
|
|
8
|
+
constructor(message, status, body) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.name = 'ElasticsearchError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.ElasticsearchError = ElasticsearchError;
|
|
16
|
+
class ElasticsearchKeywordIndex {
|
|
17
|
+
options;
|
|
18
|
+
base;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(options.index)) {
|
|
23
|
+
throw new RangeError(`"${options.index}" is not a valid index name: use lowercase letters, digits, ".", "_", and "-"`);
|
|
24
|
+
}
|
|
25
|
+
this.base = options.url.replace(/\/+$/, '');
|
|
26
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
27
|
+
}
|
|
28
|
+
async migrate() {
|
|
29
|
+
const existing = await this.request('HEAD', `/${this.options.index}`, undefined, { allowNotFound: true });
|
|
30
|
+
if (existing !== undefined)
|
|
31
|
+
return;
|
|
32
|
+
await this.request('PUT', `/${this.options.index}`, {
|
|
33
|
+
mappings: {
|
|
34
|
+
dynamic_templates: [
|
|
35
|
+
{
|
|
36
|
+
metadata_strings: { path_match: 'metadata.*', match_mapping_type: 'string', mapping: { type: 'keyword' } },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
properties: {
|
|
40
|
+
id: { type: 'keyword' },
|
|
41
|
+
content: { type: 'text', ...(this.options.analyzer ? { analyzer: this.options.analyzer } : {}) },
|
|
42
|
+
source: { type: 'keyword' },
|
|
43
|
+
metadata: { type: 'object', dynamic: true },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async add(chunks) {
|
|
49
|
+
if (chunks.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const lines = chunks.flatMap((chunk) => [
|
|
52
|
+
{ index: { _index: this.options.index, _id: chunk.id } },
|
|
53
|
+
{ id: chunk.id, content: chunk.content, source: chunk.source ?? null, metadata: chunk.metadata ?? null },
|
|
54
|
+
]);
|
|
55
|
+
await this.bulk(lines);
|
|
56
|
+
}
|
|
57
|
+
async delete(ids) {
|
|
58
|
+
if (ids.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
await this.bulk(ids.map((id) => ({ delete: { _index: this.options.index, _id: id } })));
|
|
61
|
+
}
|
|
62
|
+
async retrieve(query, options = {}) {
|
|
63
|
+
const filter = Object.entries(options.filter ?? {});
|
|
64
|
+
const clauses = [];
|
|
65
|
+
for (const [key, value] of filter) {
|
|
66
|
+
if (value === null)
|
|
67
|
+
clauses.push({ bool: { must_not: { exists: { field: `metadata.${key}` } } } });
|
|
68
|
+
else if (['string', 'number', 'boolean'].includes(typeof value))
|
|
69
|
+
clauses.push({ term: { [`metadata.${key}`]: value } });
|
|
70
|
+
else
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const result = (await this.request('POST', `/${this.options.index}/_search`, {
|
|
74
|
+
size: options.topK ?? 5,
|
|
75
|
+
query: { bool: { must: [{ match: { content: { query, operator: 'or' } } }], filter: clauses } },
|
|
76
|
+
_source: ['id', 'content', 'source', 'metadata'],
|
|
77
|
+
}, { signal: options.signal }));
|
|
78
|
+
return (result.hits?.hits ?? []).map((hit) => ({
|
|
79
|
+
id: hit._source?.id ?? hit._id,
|
|
80
|
+
content: hit._source?.content ?? '',
|
|
81
|
+
...(hit._source?.source ? { source: hit._source.source } : {}),
|
|
82
|
+
...(hit._source?.metadata ? { metadata: hit._source.metadata } : {}),
|
|
83
|
+
score: hit._score ?? 0,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
async size() {
|
|
87
|
+
const result = (await this.request('GET', `/${this.options.index}/_count`));
|
|
88
|
+
return result.count ?? 0;
|
|
89
|
+
}
|
|
90
|
+
async bulk(lines) {
|
|
91
|
+
const refresh = this.options.refresh ?? 'wait_for';
|
|
92
|
+
const result = (await this.request('POST', `/_bulk${refresh === false ? '' : `?refresh=${refresh === true ? 'true' : 'wait_for'}`}`, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`));
|
|
93
|
+
if (!result.errors)
|
|
94
|
+
return;
|
|
95
|
+
const failed = (result.items ?? [])
|
|
96
|
+
.map((item) => Object.entries(item)[0])
|
|
97
|
+
.map((entry) => (entry?.[1]?.error && !(entry[0] === 'delete' && entry[1].status === 404) ? entry[1] : undefined))
|
|
98
|
+
.find((outcome) => outcome !== undefined);
|
|
99
|
+
if (failed)
|
|
100
|
+
throw new ElasticsearchError(`A bulk write failed: ${failed.error?.reason ?? 'unknown reason'}`, failed.status ?? 500);
|
|
101
|
+
}
|
|
102
|
+
async request(method, path, body, options = {}) {
|
|
103
|
+
const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 30_000);
|
|
104
|
+
const linked = (0, signals_js_1.linkSignals)([timeout, options.signal]);
|
|
105
|
+
const ndjson = typeof body === 'string';
|
|
106
|
+
const authorization = this.options.apiKey
|
|
107
|
+
? `ApiKey ${this.options.apiKey}`
|
|
108
|
+
: this.options.username !== undefined
|
|
109
|
+
? `Basic ${btoa(`${this.options.username}:${this.options.password ?? ''}`)}`
|
|
110
|
+
: undefined;
|
|
111
|
+
try {
|
|
112
|
+
const response = await this.fetchImpl(`${this.base}${path}`, {
|
|
113
|
+
method,
|
|
114
|
+
headers: {
|
|
115
|
+
...(body === undefined ? {} : { 'content-type': ndjson ? 'application/x-ndjson' : 'application/json' }),
|
|
116
|
+
...(authorization ? { authorization } : {}),
|
|
117
|
+
...this.options.headers,
|
|
118
|
+
},
|
|
119
|
+
...(body === undefined ? {} : { body: ndjson ? body : JSON.stringify(body) }),
|
|
120
|
+
signal: linked.signal,
|
|
121
|
+
});
|
|
122
|
+
if (response.status === 404 && options.allowNotFound)
|
|
123
|
+
return undefined;
|
|
124
|
+
const text = method === 'HEAD' ? '' : await response.text();
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new ElasticsearchError(`Elasticsearch ${method} ${path} failed with ${response.status}`, response.status, text);
|
|
127
|
+
}
|
|
128
|
+
return text ? JSON.parse(text) : {};
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
linked.dispose();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.ElasticsearchKeywordIndex = ElasticsearchKeywordIndex;
|
|
@@ -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,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createIngestionPipeline = createIngestionPipeline;
|
|
4
|
+
exports.ingestionExecutor = ingestionExecutor;
|
|
5
|
+
const ingestion_js_1 = require("./ingestion.js");
|
|
6
|
+
function createIngestionPipeline(options) {
|
|
7
|
+
const batchSize = options.batchSize ?? 64;
|
|
8
|
+
if (!(Number.isInteger(batchSize) && batchSize > 0))
|
|
9
|
+
throw new RangeError('batchSize must be a positive integer');
|
|
10
|
+
const namespace = ['nexus', 'ingestion', options.name, 'documents'];
|
|
11
|
+
const keywords = options.keywords ?? [];
|
|
12
|
+
const chunkingVersion = stableJson(options.chunking ?? {});
|
|
13
|
+
const read = async (id) => (await options.manifest.get(namespace, id))?.value;
|
|
14
|
+
const removeChunks = async (ids) => {
|
|
15
|
+
if (ids.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
await Promise.all([options.vectors.delete(ids), ...keywords.map((index) => index.delete(ids))]);
|
|
18
|
+
};
|
|
19
|
+
const remove = async (ids) => {
|
|
20
|
+
let chunks = 0;
|
|
21
|
+
for (const id of ids) {
|
|
22
|
+
const recorded = await read(id);
|
|
23
|
+
if (!recorded)
|
|
24
|
+
continue;
|
|
25
|
+
const chunkIds = Object.keys(recorded.chunks);
|
|
26
|
+
await removeChunks(chunkIds);
|
|
27
|
+
await options.manifest.delete(namespace, id);
|
|
28
|
+
chunks += chunkIds.length;
|
|
29
|
+
}
|
|
30
|
+
return chunks;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
document: read,
|
|
34
|
+
async remove(ids) {
|
|
35
|
+
await remove(ids);
|
|
36
|
+
return ids.length;
|
|
37
|
+
},
|
|
38
|
+
async run(source, runOptions = {}) {
|
|
39
|
+
const report = {
|
|
40
|
+
documents: 0,
|
|
41
|
+
unchanged: 0,
|
|
42
|
+
added: 0,
|
|
43
|
+
updated: 0,
|
|
44
|
+
deleted: 0,
|
|
45
|
+
chunksEmbedded: 0,
|
|
46
|
+
chunksKept: 0,
|
|
47
|
+
chunksDeleted: 0,
|
|
48
|
+
resumed: 0,
|
|
49
|
+
};
|
|
50
|
+
const previous = runOptions.operation?.previousHeartbeat;
|
|
51
|
+
const skip = previous?.pipeline === options.name ? previous.documents : 0;
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
let batch = [];
|
|
54
|
+
const flush = async () => {
|
|
55
|
+
if (batch.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const current = batch;
|
|
58
|
+
batch = [];
|
|
59
|
+
const versions = await Promise.all(current.map(({ document }) => hashText(stableJson([document.text, document.source ?? null, document.metadata ?? null]))));
|
|
60
|
+
const recorded = await Promise.all(current.map(({ id }) => read(id)));
|
|
61
|
+
const writes = [];
|
|
62
|
+
const stale = [];
|
|
63
|
+
const toEmbed = [];
|
|
64
|
+
for (const [index, { document, id }] of current.entries()) {
|
|
65
|
+
const version = versions[index];
|
|
66
|
+
const before = recorded[index];
|
|
67
|
+
if (before &&
|
|
68
|
+
before.version === version &&
|
|
69
|
+
before.chunking === chunkingVersion &&
|
|
70
|
+
before.model === options.embeddingModel) {
|
|
71
|
+
report.unchanged += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (before)
|
|
75
|
+
report.updated += 1;
|
|
76
|
+
else
|
|
77
|
+
report.added += 1;
|
|
78
|
+
const split = (0, ingestion_js_1.ingestDocuments)([{ ...document, id }], options.chunking).chunks;
|
|
79
|
+
const hashes = await Promise.all(split.map((chunk) => hashText(stableJson([chunk.content, document.metadata ?? null]))));
|
|
80
|
+
const used = new Map();
|
|
81
|
+
const chunks = split.map((chunk, position) => {
|
|
82
|
+
const base = `${id}#${hashes[position].slice(0, 12)}`;
|
|
83
|
+
const count = (used.get(base) ?? 0) + 1;
|
|
84
|
+
used.set(base, count);
|
|
85
|
+
return { ...chunk, id: count === 1 ? base : `${base}-${count}` };
|
|
86
|
+
});
|
|
87
|
+
const reusable = before && before.model === options.embeddingModel ? before.chunks : {};
|
|
88
|
+
for (const [position, chunk] of chunks.entries()) {
|
|
89
|
+
if (reusable[chunk.id] === hashes[position])
|
|
90
|
+
report.chunksKept += 1;
|
|
91
|
+
else
|
|
92
|
+
toEmbed.push(chunk);
|
|
93
|
+
}
|
|
94
|
+
const kept = new Set(chunks.map((chunk) => chunk.id));
|
|
95
|
+
if (before)
|
|
96
|
+
stale.push(...Object.keys(before.chunks).filter((chunkId) => !kept.has(chunkId) || !reusable[chunkId]));
|
|
97
|
+
writes.push({ id, version, chunks, hashes });
|
|
98
|
+
}
|
|
99
|
+
if (toEmbed.length > 0) {
|
|
100
|
+
const vectors = await options.embed(toEmbed.map((chunk) => chunk.content));
|
|
101
|
+
if (vectors.length !== toEmbed.length) {
|
|
102
|
+
throw new RangeError(`The embedding function returned ${vectors.length} vectors for ${toEmbed.length} texts`);
|
|
103
|
+
}
|
|
104
|
+
await Promise.all([
|
|
105
|
+
options.vectors.add(toEmbed.map((chunk, index) => ({ ...chunk, embedding: vectors[index] }))),
|
|
106
|
+
...keywords.map((index) => index.add(toEmbed)),
|
|
107
|
+
]);
|
|
108
|
+
report.chunksEmbedded += toEmbed.length;
|
|
109
|
+
}
|
|
110
|
+
const embeddedIds = new Set(toEmbed.map((chunk) => chunk.id));
|
|
111
|
+
const removable = stale.filter((chunkId) => !embeddedIds.has(chunkId));
|
|
112
|
+
await removeChunks(removable);
|
|
113
|
+
report.chunksDeleted += removable.length;
|
|
114
|
+
const updatedAt = new Date().toISOString();
|
|
115
|
+
await Promise.all(writes.map(({ id, version, chunks, hashes }) => options.manifest.put(namespace, id, {
|
|
116
|
+
id,
|
|
117
|
+
version,
|
|
118
|
+
chunking: chunkingVersion,
|
|
119
|
+
model: options.embeddingModel,
|
|
120
|
+
chunks: Object.fromEntries(chunks.map((chunk, index) => [chunk.id, hashes[index]])),
|
|
121
|
+
updatedAt,
|
|
122
|
+
})));
|
|
123
|
+
await runOptions.operation?.heartbeat({
|
|
124
|
+
pipeline: options.name,
|
|
125
|
+
documents: report.documents,
|
|
126
|
+
});
|
|
127
|
+
runOptions.onProgress?.({ ...report });
|
|
128
|
+
};
|
|
129
|
+
const sources = isSourceList(source) ? source : [source];
|
|
130
|
+
for (const loader of sources) {
|
|
131
|
+
for await (const document of loader) {
|
|
132
|
+
runOptions.signal?.throwIfAborted();
|
|
133
|
+
const id = document.id ?? document.source;
|
|
134
|
+
if (!id)
|
|
135
|
+
throw new TypeError('Durable ingestion needs a document id, or a source to use as one');
|
|
136
|
+
seen.add(id);
|
|
137
|
+
report.documents += 1;
|
|
138
|
+
if (report.documents <= skip) {
|
|
139
|
+
report.resumed += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
batch.push({ document, id });
|
|
143
|
+
if (batch.length >= batchSize)
|
|
144
|
+
await flush();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
runOptions.signal?.throwIfAborted();
|
|
148
|
+
await flush();
|
|
149
|
+
if (runOptions.deleteMissing !== false) {
|
|
150
|
+
const missing = [];
|
|
151
|
+
for (let offset = 0;; offset += 500) {
|
|
152
|
+
const page = await options.manifest.search(namespace, { limit: 500, offset });
|
|
153
|
+
for (const item of page)
|
|
154
|
+
if (!seen.has(item.key))
|
|
155
|
+
missing.push(item.key);
|
|
156
|
+
if (page.length < 500)
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
for (const id of missing) {
|
|
160
|
+
runOptions.signal?.throwIfAborted();
|
|
161
|
+
report.chunksDeleted += await remove([id]);
|
|
162
|
+
report.deleted += 1;
|
|
163
|
+
}
|
|
164
|
+
if (missing.length > 0)
|
|
165
|
+
runOptions.onProgress?.({ ...report });
|
|
166
|
+
}
|
|
167
|
+
return report;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function ingestionExecutor(pipeline, source, options = {}) {
|
|
172
|
+
return (context) => pipeline.run(source(), { ...options, signal: context.signal, operation: context });
|
|
173
|
+
}
|
|
174
|
+
async function hashText(text) {
|
|
175
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
176
|
+
let hex = '';
|
|
177
|
+
for (const byte of new Uint8Array(digest).subarray(0, 16))
|
|
178
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
179
|
+
return hex;
|
|
180
|
+
}
|
|
181
|
+
function stableJson(value) {
|
|
182
|
+
return JSON.stringify(value, (_key, entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
183
|
+
? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a.localeCompare(b)))
|
|
184
|
+
: entry);
|
|
185
|
+
}
|
|
186
|
+
function isSourceList(value) {
|
|
187
|
+
if (!Array.isArray(value))
|
|
188
|
+
return false;
|
|
189
|
+
const first = value[0];
|
|
190
|
+
return first !== undefined && typeof first === 'object' && first !== null && !('text' in first);
|
|
191
|
+
}
|