@anvia/neo4j 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/chunk-PWE3ETSF.js +176 -0
- package/dist/chunk-PWE3ETSF.js.map +1 -0
- package/dist/chunk-QNHOT7P2.js +1486 -0
- package/dist/chunk-QNHOT7P2.js.map +1 -0
- package/dist/chunk-YLW4LCKJ.js +473 -0
- package/dist/chunk-YLW4LCKJ.js.map +1 -0
- package/dist/explore-RA53GFX2.js +8 -0
- package/dist/explore-RA53GFX2.js.map +1 -0
- package/dist/index.d.ts +9 -34
- package/dist/index.js +14 -2207
- package/dist/index.js.map +1 -1
- package/dist/retrieve-QRQHIVRO.js +8 -0
- package/dist/retrieve-QRQHIVRO.js.map +1 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,1459 +1,17 @@
|
|
|
1
|
-
// src/client.ts
|
|
2
|
-
import { auth, driver as createDriver } from "neo4j-driver";
|
|
3
|
-
|
|
4
|
-
// src/graph.ts
|
|
5
1
|
import {
|
|
6
|
-
|
|
7
|
-
} from "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (Object.keys(options.nodes).length === 0) {
|
|
17
|
-
throw new TypeError("A Neo4j graph schema requires at least one node type.");
|
|
18
|
-
}
|
|
19
|
-
for (const [type, definition] of Object.entries(options.nodes)) {
|
|
20
|
-
assertName(type, "node type");
|
|
21
|
-
assertDescription(definition.description, `node ${type}`);
|
|
22
|
-
if (!(definition.properties instanceof z.ZodObject)) {
|
|
23
|
-
throw new TypeError(`Properties for node ${type} must be a Zod object schema.`);
|
|
24
|
-
}
|
|
25
|
-
assertExactPropertySchema(definition.properties, `node ${type}`);
|
|
26
|
-
if (definition.identity.length === 0 || new Set(definition.identity).size !== definition.identity.length) {
|
|
27
|
-
throw new TypeError(`Node ${type} must declare unique identity properties.`);
|
|
28
|
-
}
|
|
29
|
-
const shape = definition.properties.shape;
|
|
30
|
-
for (const property of definition.identity) {
|
|
31
|
-
assertPropertyName(property);
|
|
32
|
-
if (!(property in shape)) {
|
|
33
|
-
throw new TypeError(`Identity property ${property} is not declared by node ${type}.`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
for (const property of Object.keys(shape)) assertPropertyName(property);
|
|
37
|
-
}
|
|
38
|
-
for (const [type, definition] of Object.entries(options.relationships)) {
|
|
39
|
-
assertName(type, "relationship type");
|
|
40
|
-
assertDescription(definition.description, `relationship ${type}`);
|
|
41
|
-
if (!(definition.from in options.nodes) || !(definition.to in options.nodes)) {
|
|
42
|
-
throw new TypeError(`Relationship ${type} references an unknown endpoint type.`);
|
|
43
|
-
}
|
|
44
|
-
if (!(definition.properties instanceof z.ZodObject)) {
|
|
45
|
-
throw new TypeError(`Properties for relationship ${type} must be a Zod object schema.`);
|
|
46
|
-
}
|
|
47
|
-
assertExactPropertySchema(definition.properties, `relationship ${type}`);
|
|
48
|
-
const identity = definition.identity ?? [];
|
|
49
|
-
if (new Set(identity).size !== identity.length) {
|
|
50
|
-
throw new TypeError(`Relationship ${type} contains duplicate identity properties.`);
|
|
51
|
-
}
|
|
52
|
-
for (const property of Object.keys(definition.properties.shape)) assertPropertyName(property);
|
|
53
|
-
for (const property of identity) {
|
|
54
|
-
if (!(property in definition.properties.shape)) {
|
|
55
|
-
throw new TypeError(
|
|
56
|
-
`Identity property ${property} is not declared by relationship ${type}.`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const nodes = Object.fromEntries(
|
|
62
|
-
Object.entries(options.nodes).map(([type, definition]) => [
|
|
63
|
-
type,
|
|
64
|
-
Object.freeze({ ...definition, identity: Object.freeze([...definition.identity]) })
|
|
65
|
-
])
|
|
66
|
-
);
|
|
67
|
-
const relationships = Object.fromEntries(
|
|
68
|
-
Object.entries(options.relationships).map(([type, definition]) => {
|
|
69
|
-
const copy = { ...definition };
|
|
70
|
-
if (definition.identity !== void 0) {
|
|
71
|
-
Object.assign(copy, { identity: Object.freeze([...definition.identity]) });
|
|
72
|
-
}
|
|
73
|
-
return [type, Object.freeze(copy)];
|
|
74
|
-
})
|
|
75
|
-
);
|
|
76
|
-
return Object.freeze({
|
|
77
|
-
nodes: Object.freeze(nodes),
|
|
78
|
-
relationships: Object.freeze(relationships),
|
|
79
|
-
kind: "neo4j-graph-schema"
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function parseNeo4jProperties(value, label) {
|
|
83
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
84
|
-
throw new TypeError(`${label} must be an object of Neo4j property values.`);
|
|
85
|
-
}
|
|
86
|
-
const output = {};
|
|
87
|
-
for (const [key, item] of Object.entries(value)) {
|
|
88
|
-
assertPropertyName(key);
|
|
89
|
-
output[key] = parseNeo4jPropertyValue(item, `${label}.${key}`);
|
|
90
|
-
}
|
|
91
|
-
return output;
|
|
92
|
-
}
|
|
93
|
-
function parseNeo4jPropertyValue(value, label) {
|
|
94
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
95
|
-
if (typeof value === "number") {
|
|
96
|
-
if (!Number.isFinite(value)) throw new TypeError(`${label} must be finite.`);
|
|
97
|
-
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
98
|
-
throw new TypeError(`${label} must be a safe integer.`);
|
|
99
|
-
}
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
if (Array.isArray(value)) {
|
|
103
|
-
if (value.length === 0) return [];
|
|
104
|
-
const parsed = value.map((item, index) => parsePrimitive(item, `${label}[${index}]`));
|
|
105
|
-
const type = typeof parsed[0];
|
|
106
|
-
if (!parsed.every((item) => typeof item === type)) {
|
|
107
|
-
throw new TypeError(`${label} must be a homogeneous array.`);
|
|
108
|
-
}
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
throw new TypeError(`${label} is not a supported Neo4j property value.`);
|
|
112
|
-
}
|
|
113
|
-
function parsePrimitive(value, label) {
|
|
114
|
-
const parsed = parseNeo4jPropertyValue(value, label);
|
|
115
|
-
if (typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean") {
|
|
116
|
-
return parsed;
|
|
117
|
-
}
|
|
118
|
-
throw new TypeError(`${label} must be a primitive property value.`);
|
|
119
|
-
}
|
|
120
|
-
function assertName(value, label) {
|
|
121
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
122
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function assertPropertyName(value) {
|
|
126
|
-
assertName(value, "Neo4j property name");
|
|
127
|
-
if (value.startsWith(reservedPrefix)) {
|
|
128
|
-
throw new TypeError(`Neo4j property names beginning with ${reservedPrefix} are reserved.`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function assertDescription(value, label) {
|
|
132
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
133
|
-
throw new TypeError(`${label} requires a non-empty description.`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function assertExactPropertySchema(schema, label) {
|
|
137
|
-
const definition = schema._zod.def;
|
|
138
|
-
if (!(definition.catchall instanceof z.ZodNever)) {
|
|
139
|
-
throw new TypeError(`${label} properties must use a strict Zod object schema.`);
|
|
140
|
-
}
|
|
141
|
-
for (const [property, propertySchema] of Object.entries(schema.shape)) {
|
|
142
|
-
assertNonEffectfulSchema(propertySchema, `${label} property ${property}`, /* @__PURE__ */ new Set());
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function assertNonEffectfulSchema(value, label, visited) {
|
|
146
|
-
if (!(value instanceof z.ZodType)) {
|
|
147
|
-
throw new TypeError(`${label} must be a Zod schema.`);
|
|
148
|
-
}
|
|
149
|
-
const schema = value;
|
|
150
|
-
if (visited.has(schema)) return;
|
|
151
|
-
visited.add(schema);
|
|
152
|
-
const definition = schema._zod.def;
|
|
153
|
-
const type = definition.type;
|
|
154
|
-
if (definition.coerce === true || type === "default" || type === "prefault" || type === "catch" || type === "transform" || type === "pipe") {
|
|
155
|
-
throw new TypeError(
|
|
156
|
-
`${label} must not coerce, default, transform, preprocess, or catch values.`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
const checks = definition.checks;
|
|
160
|
-
if (Array.isArray(checks) && checks.some((check) => {
|
|
161
|
-
if (typeof check !== "object" || check === null) return false;
|
|
162
|
-
const internals = check._zod;
|
|
163
|
-
return internals?.def?.check === "overwrite";
|
|
164
|
-
})) {
|
|
165
|
-
throw new TypeError(`${label} must not normalize or overwrite values.`);
|
|
166
|
-
}
|
|
167
|
-
for (const child of schemaChildren(definition)) {
|
|
168
|
-
assertNonEffectfulSchema(child, label, visited);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function schemaChildren(definition) {
|
|
172
|
-
const children = [];
|
|
173
|
-
for (const key of ["innerType", "element", "left", "right", "rest", "keyType", "valueType"]) {
|
|
174
|
-
const value = definition[key];
|
|
175
|
-
if (value instanceof z.ZodType) children.push(value);
|
|
176
|
-
}
|
|
177
|
-
for (const key of ["options", "items"]) {
|
|
178
|
-
const value = definition[key];
|
|
179
|
-
if (Array.isArray(value)) {
|
|
180
|
-
for (const item of value) if (item instanceof z.ZodType) children.push(item);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return children;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// src/helpers.ts
|
|
187
|
-
function quoteIdentifier(value) {
|
|
188
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
189
|
-
throw new TypeError("Neo4j identifiers must be non-empty strings.");
|
|
190
|
-
}
|
|
191
|
-
if (value.includes("\0")) throw new TypeError("Neo4j identifiers must not contain NUL.");
|
|
192
|
-
return `\`${value.replaceAll("`", "``")}\``;
|
|
193
|
-
}
|
|
194
|
-
function throwIfAborted(signal) {
|
|
195
|
-
if (signal?.aborted) {
|
|
196
|
-
throw abortError();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
function abortError() {
|
|
200
|
-
const error = new Error("The operation was aborted.");
|
|
201
|
-
error.name = "AbortError";
|
|
202
|
-
return error;
|
|
203
|
-
}
|
|
204
|
-
function positiveInteger(value, label, maximum) {
|
|
205
|
-
if (!Number.isSafeInteger(value) || value < 1 || maximum !== void 0 && value > maximum) {
|
|
206
|
-
throw new RangeError(
|
|
207
|
-
`${label} must be a positive safe integer${maximum === void 0 ? "" : ` no greater than ${maximum}`}.`
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
return value;
|
|
211
|
-
}
|
|
212
|
-
function finiteScore(value, label) {
|
|
213
|
-
if (value !== void 0 && !Number.isFinite(value))
|
|
214
|
-
throw new RangeError(`${label} must be finite.`);
|
|
215
|
-
}
|
|
216
|
-
function strictProperties(value, label) {
|
|
217
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
218
|
-
throw new TypeError(`${label} must be an object.`);
|
|
219
|
-
}
|
|
220
|
-
const normalized = {};
|
|
221
|
-
for (const [key, item] of Object.entries(value))
|
|
222
|
-
normalized[key] = normalizeDriverValue(item, `${label}.${key}`);
|
|
223
|
-
return parseNeo4jProperties(normalized, label);
|
|
224
|
-
}
|
|
225
|
-
function normalizeDriverValue(value, label) {
|
|
226
|
-
if (isInt(value)) {
|
|
227
|
-
if (!value.inSafeRange()) throw new TypeError(`${label} contains an unsafe Neo4j integer.`);
|
|
228
|
-
return value.toNumber();
|
|
229
|
-
}
|
|
230
|
-
if (Array.isArray(value))
|
|
231
|
-
return value.map((item, index) => normalizeDriverValue(item, `${label}[${index}]`));
|
|
232
|
-
return value;
|
|
233
|
-
}
|
|
234
|
-
function stableObject(value) {
|
|
235
|
-
return JSON.stringify(
|
|
236
|
-
Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)))
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// src/graph.ts
|
|
241
|
-
var Neo4jKnowledgeGraphBase = class {
|
|
242
|
-
constructor(owner, schema) {
|
|
243
|
-
this.owner = owner;
|
|
244
|
-
this.schema = schema;
|
|
245
|
-
if (schema.kind !== "neo4j-graph-schema") {
|
|
246
|
-
throw new TypeError("Neo4j knowledge graphs require defineNeo4jGraphSchema().");
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
owner;
|
|
250
|
-
schema;
|
|
251
|
-
async validate(options = {}) {
|
|
252
|
-
throwIfAborted(options.abortSignal);
|
|
253
|
-
await this.assertSupportedVersion(options.abortSignal);
|
|
254
|
-
const expected = expectedIndexes(this.seeds);
|
|
255
|
-
const names = [...expected.keys()];
|
|
256
|
-
const result = await this.query(
|
|
257
|
-
"SHOW INDEXES YIELD name, state, type, entityType, labelsOrTypes, properties, options WHERE name IN $names RETURN name, state, type, entityType, labelsOrTypes, properties, options",
|
|
258
|
-
{ names },
|
|
259
|
-
options.abortSignal
|
|
260
|
-
);
|
|
261
|
-
const actual = new Map(
|
|
262
|
-
result.records.map((record) => [
|
|
263
|
-
requiredString(record.get("name"), "Neo4j index name"),
|
|
264
|
-
record
|
|
265
|
-
])
|
|
266
|
-
);
|
|
267
|
-
for (const [name, definition] of expected) {
|
|
268
|
-
const record = actual.get(name);
|
|
269
|
-
if (record === void 0) throw new Error(`Neo4j index ${name} does not exist.`);
|
|
270
|
-
validateIndexRecord(record, definition);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
async query(text, parameters, abortSignal, access = "read") {
|
|
274
|
-
return this.runTransaction(
|
|
275
|
-
access,
|
|
276
|
-
abortSignal,
|
|
277
|
-
async (transaction) => await transaction.run(text, parameters)
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
seed(name) {
|
|
281
|
-
const seed = this.seeds[name];
|
|
282
|
-
if (seed === void 0) throw new TypeError(`Unknown Neo4j graph seed: ${name}`);
|
|
283
|
-
return seed;
|
|
284
|
-
}
|
|
285
|
-
async transaction(abortSignal, run) {
|
|
286
|
-
return this.runTransaction("write", abortSignal, run);
|
|
287
|
-
}
|
|
288
|
-
async runTransaction(access, abortSignal, run) {
|
|
289
|
-
throwIfAborted(abortSignal);
|
|
290
|
-
const sessionOptions = {
|
|
291
|
-
defaultAccessMode: access === "read" ? "READ" : "WRITE"
|
|
292
|
-
};
|
|
293
|
-
if (this.owner.database !== void 0) {
|
|
294
|
-
Object.assign(sessionOptions, { database: this.owner.database });
|
|
295
|
-
}
|
|
296
|
-
const session = this.owner.nativeDriver().session(sessionOptions);
|
|
297
|
-
const transaction = session.beginTransaction();
|
|
298
|
-
let rejectAbort;
|
|
299
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
300
|
-
rejectAbort = reject;
|
|
301
|
-
});
|
|
302
|
-
const onAbort = () => {
|
|
303
|
-
rejectAbort?.(abortError());
|
|
304
|
-
};
|
|
305
|
-
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
306
|
-
let committing = false;
|
|
307
|
-
try {
|
|
308
|
-
const value = await Promise.race([run(transaction), aborted]);
|
|
309
|
-
throwIfAborted(abortSignal);
|
|
310
|
-
committing = true;
|
|
311
|
-
await transaction.commit();
|
|
312
|
-
return value;
|
|
313
|
-
} catch (error) {
|
|
314
|
-
if (transaction.isOpen()) await transaction.rollback();
|
|
315
|
-
if (!committing && abortSignal?.aborted) throw abortError();
|
|
316
|
-
throw error;
|
|
317
|
-
} finally {
|
|
318
|
-
abortSignal?.removeEventListener("abort", onAbort);
|
|
319
|
-
if (transaction.isOpen()) await transaction.rollback();
|
|
320
|
-
await session.close();
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
async assertSupportedVersion(abortSignal) {
|
|
324
|
-
const result = await this.query(
|
|
325
|
-
"CALL dbms.components() YIELD versions RETURN versions[0] AS version LIMIT 1",
|
|
326
|
-
{},
|
|
327
|
-
abortSignal
|
|
328
|
-
);
|
|
329
|
-
const version = result.records[0]?.get("version");
|
|
330
|
-
if (typeof version !== "string") {
|
|
331
|
-
throw new TypeError("Neo4j returned a non-string server version.");
|
|
332
|
-
}
|
|
333
|
-
if (!supportedVersion(version)) {
|
|
334
|
-
throw new Error(`@anvia/neo4j requires Neo4j 2026.01 or newer; received ${version}.`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
var Neo4jKnowledgeGraph = class extends Neo4jKnowledgeGraphBase {
|
|
339
|
-
evidenceCapability = "none";
|
|
340
|
-
seeds;
|
|
341
|
-
constructor(owner, options) {
|
|
342
|
-
super(owner, options.schema);
|
|
343
|
-
if (Object.keys(options.seeds).length === 0) {
|
|
344
|
-
throw new TypeError("An existing Neo4j knowledge graph requires at least one seed.");
|
|
345
|
-
}
|
|
346
|
-
const seeds = {};
|
|
347
|
-
for (const [name, seed] of Object.entries(options.seeds)) {
|
|
348
|
-
assertName(name, "Neo4j seed name");
|
|
349
|
-
seeds[name] = validateExistingSeed(options.schema, seed);
|
|
350
|
-
}
|
|
351
|
-
this.seeds = Object.freeze(seeds);
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
var ManagedNeo4jKnowledgeGraph = class extends Neo4jKnowledgeGraphBase {
|
|
355
|
-
evidenceCapability = "chunks";
|
|
356
|
-
seeds;
|
|
357
|
-
resources;
|
|
358
|
-
name;
|
|
359
|
-
constructor(owner, options) {
|
|
360
|
-
super(owner, options.schema);
|
|
361
|
-
assertName(options.name, "Managed Neo4j graph name");
|
|
362
|
-
this.name = options.name;
|
|
363
|
-
this.resources = validateManagedResources(options.resources);
|
|
364
|
-
let chunks = {
|
|
365
|
-
labels: Object.freeze([this.resources.labels.chunk]),
|
|
366
|
-
vectorIndex: Object.freeze({
|
|
367
|
-
...this.resources.indexes.chunks.vector,
|
|
368
|
-
property: "__anvia_embedding"
|
|
369
|
-
}),
|
|
370
|
-
entryRelationshipType: "ANVIA_MENTIONS"
|
|
371
|
-
};
|
|
372
|
-
if (this.resources.indexes.chunks.fulltext !== void 0) {
|
|
373
|
-
chunks = {
|
|
374
|
-
...chunks,
|
|
375
|
-
fulltextIndex: Object.freeze({
|
|
376
|
-
...this.resources.indexes.chunks.fulltext,
|
|
377
|
-
properties: Object.freeze(["__anvia_text"])
|
|
378
|
-
})
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
let entities = {
|
|
382
|
-
labels: Object.freeze([this.resources.labels.entity]),
|
|
383
|
-
vectorIndex: Object.freeze({
|
|
384
|
-
...this.resources.indexes.entities.vector,
|
|
385
|
-
property: "__anvia_embedding"
|
|
386
|
-
})
|
|
387
|
-
};
|
|
388
|
-
if (this.resources.indexes.entities.fulltext !== void 0) {
|
|
389
|
-
entities = {
|
|
390
|
-
...entities,
|
|
391
|
-
fulltextIndex: Object.freeze({
|
|
392
|
-
...this.resources.indexes.entities.fulltext,
|
|
393
|
-
properties: Object.freeze([
|
|
394
|
-
...this.resources.indexes.entities.fulltext.properties ?? []
|
|
395
|
-
])
|
|
396
|
-
})
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
this.seeds = Object.freeze({
|
|
400
|
-
chunks: Object.freeze(chunks),
|
|
401
|
-
entities: Object.freeze(entities)
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
async ensure(options) {
|
|
405
|
-
positiveInteger(options.indexTimeoutMs, "Neo4j index timeout");
|
|
406
|
-
throwIfAborted(options.abortSignal);
|
|
407
|
-
const { labels, indexes } = this.resources;
|
|
408
|
-
const statements = [
|
|
409
|
-
uniquenessConstraint(constraintName(this.name, "document"), labels.document, ["__anvia_id"]),
|
|
410
|
-
uniquenessConstraint(constraintName(this.name, "chunk"), labels.chunk, ["__anvia_id"]),
|
|
411
|
-
uniquenessConstraint(constraintName(this.name, "entity"), labels.entity, ["__anvia_key"]),
|
|
412
|
-
...Object.entries(this.schema.nodes).map(
|
|
413
|
-
([type, definition]) => uniquenessConstraint(
|
|
414
|
-
constraintName(this.name, `entity_${type}`),
|
|
415
|
-
type,
|
|
416
|
-
definition.identity
|
|
417
|
-
)
|
|
418
|
-
),
|
|
419
|
-
vectorIndex(indexes.chunks.vector, labels.chunk),
|
|
420
|
-
vectorIndex(indexes.entities.vector, labels.entity)
|
|
421
|
-
];
|
|
422
|
-
if (indexes.chunks.fulltext !== void 0) {
|
|
423
|
-
statements.push(fulltextIndex(indexes.chunks.fulltext.name, labels.chunk, ["__anvia_text"]));
|
|
424
|
-
}
|
|
425
|
-
if (indexes.entities.fulltext !== void 0) {
|
|
426
|
-
statements.push(
|
|
427
|
-
fulltextIndex(
|
|
428
|
-
indexes.entities.fulltext.name,
|
|
429
|
-
labels.entity,
|
|
430
|
-
indexes.entities.fulltext.properties ?? []
|
|
431
|
-
)
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
for (const statement of statements)
|
|
435
|
-
await this.query(statement, {}, options.abortSignal, "write");
|
|
436
|
-
await this.query(
|
|
437
|
-
"CALL db.awaitIndexes($timeoutSeconds)",
|
|
438
|
-
{ timeoutSeconds: Math.ceil(options.indexTimeoutMs / 1e3) },
|
|
439
|
-
options.abortSignal
|
|
440
|
-
);
|
|
441
|
-
await this.validate({ abortSignal: options.abortSignal });
|
|
442
|
-
}
|
|
443
|
-
async validate(options = {}) {
|
|
444
|
-
await super.validate(options);
|
|
445
|
-
const expected = this.expectedConstraints();
|
|
446
|
-
const names = expected.map((constraint) => constraint.name);
|
|
447
|
-
const result = await this.query(
|
|
448
|
-
"SHOW CONSTRAINTS YIELD name, type, entityType, labelsOrTypes, properties WHERE name IN $names RETURN name, type, entityType, labelsOrTypes, properties",
|
|
449
|
-
{ names },
|
|
450
|
-
options.abortSignal
|
|
451
|
-
);
|
|
452
|
-
const actual = new Map(
|
|
453
|
-
result.records.map((record) => [
|
|
454
|
-
requiredString(record.get("name"), "Neo4j constraint name"),
|
|
455
|
-
record
|
|
456
|
-
])
|
|
457
|
-
);
|
|
458
|
-
for (const constraint of expected) {
|
|
459
|
-
const record = actual.get(constraint.name);
|
|
460
|
-
if (record === void 0) {
|
|
461
|
-
throw new Error(`Neo4j constraint ${constraint.name} does not exist.`);
|
|
462
|
-
}
|
|
463
|
-
validateConstraintRecord(record, constraint);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
async replaceDocuments(options) {
|
|
467
|
-
validateReplacement(options, this.resources.indexes, this.schema);
|
|
468
|
-
if (options.documents.length === 0) return emptyWriteResult();
|
|
469
|
-
const documentIds = options.documents.map((document) => document.id);
|
|
470
|
-
const chunksById = new Map(options.chunks.map((chunk) => [chunk.id, chunk.document]));
|
|
471
|
-
const sourceDocuments = (sourceChunkIds) => [
|
|
472
|
-
...new Set(
|
|
473
|
-
sourceChunkIds.map((chunkId) => {
|
|
474
|
-
const chunk = chunksById.get(chunkId);
|
|
475
|
-
if (chunk === void 0)
|
|
476
|
-
throw new TypeError(`Graph fact references unknown chunk ${chunkId}.`);
|
|
477
|
-
return chunk.documentId;
|
|
478
|
-
})
|
|
479
|
-
)
|
|
480
|
-
];
|
|
481
|
-
return await this.transaction(options.abortSignal, async (transaction) => {
|
|
482
|
-
const targets = replacementSnapshotTargets(options);
|
|
483
|
-
const before = await snapshotGraphState(
|
|
484
|
-
transaction,
|
|
485
|
-
this.name,
|
|
486
|
-
this.resources.labels,
|
|
487
|
-
targets
|
|
488
|
-
);
|
|
489
|
-
await removeDocuments(
|
|
490
|
-
transaction,
|
|
491
|
-
this.name,
|
|
492
|
-
this.resources.labels,
|
|
493
|
-
documentIds,
|
|
494
|
-
options.orphanEntities
|
|
495
|
-
);
|
|
496
|
-
await writeDocuments(transaction, this.resources.labels, options.documents);
|
|
497
|
-
await writeChunks(transaction, this.resources.labels, options.chunks);
|
|
498
|
-
await writeEntities(
|
|
499
|
-
transaction,
|
|
500
|
-
this.name,
|
|
501
|
-
this.resources.labels.entity,
|
|
502
|
-
options.entities,
|
|
503
|
-
options.conflict,
|
|
504
|
-
sourceDocuments
|
|
505
|
-
);
|
|
506
|
-
await writeMentions(transaction, this.resources.labels, options.mentions);
|
|
507
|
-
await writeRelationships(
|
|
508
|
-
transaction,
|
|
509
|
-
this.name,
|
|
510
|
-
this.resources.labels.entity,
|
|
511
|
-
options.relationships,
|
|
512
|
-
options.conflict,
|
|
513
|
-
sourceDocuments
|
|
514
|
-
);
|
|
515
|
-
const after = await snapshotGraphState(
|
|
516
|
-
transaction,
|
|
517
|
-
this.name,
|
|
518
|
-
this.resources.labels,
|
|
519
|
-
expandSnapshotTargets(targets, before)
|
|
520
|
-
);
|
|
521
|
-
return diffGraphSnapshots(before, after);
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
async deleteDocuments(options) {
|
|
525
|
-
validateOrphanPolicy(options.orphanEntities);
|
|
526
|
-
const ids = uniqueIds(options.documentIds, "document");
|
|
527
|
-
if (ids.length === 0) return emptyWriteResult();
|
|
528
|
-
return await this.transaction(options.abortSignal, async (transaction) => {
|
|
529
|
-
const targets = emptySnapshotTargets(ids);
|
|
530
|
-
const before = await snapshotGraphState(
|
|
531
|
-
transaction,
|
|
532
|
-
this.name,
|
|
533
|
-
this.resources.labels,
|
|
534
|
-
targets
|
|
535
|
-
);
|
|
536
|
-
await removeDocuments(
|
|
537
|
-
transaction,
|
|
538
|
-
this.name,
|
|
539
|
-
this.resources.labels,
|
|
540
|
-
ids,
|
|
541
|
-
options.orphanEntities
|
|
542
|
-
);
|
|
543
|
-
const after = await snapshotGraphState(
|
|
544
|
-
transaction,
|
|
545
|
-
this.name,
|
|
546
|
-
this.resources.labels,
|
|
547
|
-
expandSnapshotTargets(targets, before)
|
|
548
|
-
);
|
|
549
|
-
return diffGraphSnapshots(before, after);
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
expectedConstraints() {
|
|
553
|
-
return [
|
|
554
|
-
{
|
|
555
|
-
name: constraintName(this.name, "document"),
|
|
556
|
-
label: this.resources.labels.document,
|
|
557
|
-
properties: ["__anvia_id"]
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
name: constraintName(this.name, "chunk"),
|
|
561
|
-
label: this.resources.labels.chunk,
|
|
562
|
-
properties: ["__anvia_id"]
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
name: constraintName(this.name, "entity"),
|
|
566
|
-
label: this.resources.labels.entity,
|
|
567
|
-
properties: ["__anvia_key"]
|
|
568
|
-
},
|
|
569
|
-
...Object.entries(this.schema.nodes).map(([type, definition]) => ({
|
|
570
|
-
name: constraintName(this.name, `entity_${type}`),
|
|
571
|
-
label: type,
|
|
572
|
-
properties: definition.identity
|
|
573
|
-
}))
|
|
574
|
-
];
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
function validateExistingSeed(schema, seed) {
|
|
578
|
-
if (seed.nodeTypes.length !== 1) {
|
|
579
|
-
throw new TypeError(
|
|
580
|
-
"Each existing Neo4j seed requires exactly one indexed node type; register additional indexes as separate seeds."
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
for (const type of seed.nodeTypes) {
|
|
584
|
-
if (!(type in schema.nodes))
|
|
585
|
-
throw new TypeError(`Neo4j seed references unknown node type ${type}.`);
|
|
586
|
-
}
|
|
587
|
-
validateVectorIndex(seed.vectorIndex);
|
|
588
|
-
assertPropertyName(seed.vectorIndex.property);
|
|
589
|
-
if (seed.fulltextIndex !== void 0) {
|
|
590
|
-
validateFulltextIndex(seed.fulltextIndex, "Existing Neo4j full-text index");
|
|
591
|
-
}
|
|
592
|
-
let registration = {
|
|
593
|
-
labels: Object.freeze([...seed.nodeTypes]),
|
|
594
|
-
vectorIndex: Object.freeze({ ...seed.vectorIndex })
|
|
595
|
-
};
|
|
596
|
-
if (seed.fulltextIndex !== void 0) {
|
|
597
|
-
registration = {
|
|
598
|
-
...registration,
|
|
599
|
-
fulltextIndex: Object.freeze({
|
|
600
|
-
...seed.fulltextIndex,
|
|
601
|
-
properties: Object.freeze([...seed.fulltextIndex.properties])
|
|
602
|
-
})
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
return Object.freeze(registration);
|
|
606
|
-
}
|
|
607
|
-
function validateManagedResources(resources) {
|
|
608
|
-
const labels = Object.values(resources.labels);
|
|
609
|
-
for (const label of labels) assertName(label, "Neo4j managed label");
|
|
610
|
-
if (new Set(labels).size !== labels.length)
|
|
611
|
-
throw new TypeError("Managed Neo4j graph labels must be distinct.");
|
|
612
|
-
const indexes = [resources.indexes.chunks.vector, resources.indexes.entities.vector];
|
|
613
|
-
for (const index of indexes) validateVectorIndex(index);
|
|
614
|
-
const names = [
|
|
615
|
-
...indexes.map((index) => index.name),
|
|
616
|
-
resources.indexes.chunks.fulltext?.name,
|
|
617
|
-
resources.indexes.entities.fulltext?.name
|
|
618
|
-
].filter((name) => name !== void 0);
|
|
619
|
-
if (new Set(names).size !== names.length)
|
|
620
|
-
throw new TypeError("Managed Neo4j index names must be unique.");
|
|
621
|
-
if (resources.indexes.entities.fulltext !== void 0) {
|
|
622
|
-
const properties2 = resources.indexes.entities.fulltext.properties ?? [];
|
|
623
|
-
if (properties2.length === 0)
|
|
624
|
-
throw new TypeError("Entity full-text indexes require explicit properties.");
|
|
625
|
-
}
|
|
626
|
-
if (resources.indexes.chunks.fulltext !== void 0) {
|
|
627
|
-
validateFulltextIndex(
|
|
628
|
-
{ ...resources.indexes.chunks.fulltext, properties: ["__anvia_text"] },
|
|
629
|
-
"Chunk full-text index",
|
|
630
|
-
true
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
if (resources.indexes.entities.fulltext !== void 0) {
|
|
634
|
-
validateFulltextIndex(
|
|
635
|
-
{
|
|
636
|
-
...resources.indexes.entities.fulltext,
|
|
637
|
-
properties: resources.indexes.entities.fulltext.properties ?? []
|
|
638
|
-
},
|
|
639
|
-
"Entity full-text index"
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
const copyFulltext = (fulltext) => {
|
|
643
|
-
let copy = { ...fulltext };
|
|
644
|
-
if (fulltext.properties !== void 0) {
|
|
645
|
-
copy = { ...copy, properties: Object.freeze([...fulltext.properties]) };
|
|
646
|
-
}
|
|
647
|
-
return Object.freeze(copy);
|
|
648
|
-
};
|
|
649
|
-
let chunks = Object.freeze({
|
|
650
|
-
vector: Object.freeze({ ...resources.indexes.chunks.vector })
|
|
651
|
-
});
|
|
652
|
-
if (resources.indexes.chunks.fulltext !== void 0) {
|
|
653
|
-
chunks = Object.freeze({
|
|
654
|
-
...chunks,
|
|
655
|
-
fulltext: copyFulltext(resources.indexes.chunks.fulltext)
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
let entities = Object.freeze({
|
|
659
|
-
vector: Object.freeze({ ...resources.indexes.entities.vector })
|
|
660
|
-
});
|
|
661
|
-
if (resources.indexes.entities.fulltext !== void 0) {
|
|
662
|
-
entities = Object.freeze({
|
|
663
|
-
...entities,
|
|
664
|
-
fulltext: copyFulltext(resources.indexes.entities.fulltext)
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return Object.freeze({
|
|
668
|
-
labels: Object.freeze({ ...resources.labels }),
|
|
669
|
-
indexes: Object.freeze({
|
|
670
|
-
chunks,
|
|
671
|
-
entities
|
|
672
|
-
})
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
function validateVectorIndex(index) {
|
|
676
|
-
assertName(index.name, "Neo4j vector index name");
|
|
677
|
-
positiveInteger(index.dimensions, "Neo4j vector dimensions", 4096);
|
|
678
|
-
if (index.similarity !== "cosine" && index.similarity !== "euclidean") {
|
|
679
|
-
throw new TypeError("Neo4j vector similarity must be cosine or euclidean.");
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
function validateFulltextIndex(index, label, allowReservedProperties = false) {
|
|
683
|
-
assertName(index.name, `${label} name`);
|
|
684
|
-
if (index.properties.length === 0 || new Set(index.properties).size !== index.properties.length) {
|
|
685
|
-
throw new TypeError(`${label} requires unique properties.`);
|
|
686
|
-
}
|
|
687
|
-
for (const property of index.properties) {
|
|
688
|
-
if (allowReservedProperties) assertName(property, `${label} property`);
|
|
689
|
-
else assertPropertyName(property);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function expectedIndexes(seeds) {
|
|
693
|
-
const expected = /* @__PURE__ */ new Map();
|
|
694
|
-
const register = (index) => {
|
|
695
|
-
const previous = expected.get(index.name);
|
|
696
|
-
if (previous !== void 0 && stableObject(previous) !== stableObject(index)) {
|
|
697
|
-
throw new TypeError(`Neo4j index ${index.name} has conflicting registrations.`);
|
|
698
|
-
}
|
|
699
|
-
expected.set(index.name, index);
|
|
700
|
-
};
|
|
701
|
-
for (const seed of Object.values(seeds)) {
|
|
702
|
-
register({
|
|
703
|
-
kind: "vector",
|
|
704
|
-
name: seed.vectorIndex.name,
|
|
705
|
-
labels: seed.labels,
|
|
706
|
-
properties: [seed.vectorIndex.property],
|
|
707
|
-
dimensions: seed.vectorIndex.dimensions,
|
|
708
|
-
similarity: seed.vectorIndex.similarity
|
|
709
|
-
});
|
|
710
|
-
if (seed.fulltextIndex !== void 0) {
|
|
711
|
-
register({
|
|
712
|
-
kind: "fulltext",
|
|
713
|
-
name: seed.fulltextIndex.name,
|
|
714
|
-
labels: seed.labels,
|
|
715
|
-
properties: seed.fulltextIndex.properties
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return expected;
|
|
720
|
-
}
|
|
721
|
-
function validateIndexRecord(record, expected) {
|
|
722
|
-
const state = requiredString(record.get("state"), `Neo4j index ${expected.name} state`);
|
|
723
|
-
if (state !== "ONLINE") throw new Error(`Neo4j index ${expected.name} is ${state}, not ONLINE.`);
|
|
724
|
-
const type = requiredString(record.get("type"), `Neo4j index ${expected.name} type`);
|
|
725
|
-
const entityType = requiredString(
|
|
726
|
-
record.get("entityType"),
|
|
727
|
-
`Neo4j index ${expected.name} entity type`
|
|
728
|
-
);
|
|
729
|
-
const labels = requiredStringArray(
|
|
730
|
-
record.get("labelsOrTypes"),
|
|
731
|
-
`Neo4j index ${expected.name} labels`
|
|
732
|
-
);
|
|
733
|
-
const properties2 = requiredStringArray(
|
|
734
|
-
record.get("properties"),
|
|
735
|
-
`Neo4j index ${expected.name} properties`
|
|
736
|
-
);
|
|
737
|
-
if (entityType !== "NODE" || type !== expected.kind.toUpperCase() || !sameStrings(labels, expected.labels) || !sameStrings(properties2, expected.properties)) {
|
|
738
|
-
throw new Error(`Neo4j index ${expected.name} does not match its registration.`);
|
|
739
|
-
}
|
|
740
|
-
if (expected.kind === "vector") {
|
|
741
|
-
const options = requiredObject(record.get("options"), `Neo4j index ${expected.name} options`);
|
|
742
|
-
const config = requiredObject(
|
|
743
|
-
options.indexConfig,
|
|
744
|
-
`Neo4j index ${expected.name} vector configuration`
|
|
745
|
-
);
|
|
746
|
-
const dimensions = requiredSafeInteger(
|
|
747
|
-
config["vector.dimensions"],
|
|
748
|
-
`Neo4j index ${expected.name} dimensions`
|
|
749
|
-
);
|
|
750
|
-
const similarity = requiredString(
|
|
751
|
-
config["vector.similarity_function"],
|
|
752
|
-
`Neo4j index ${expected.name} similarity`
|
|
753
|
-
).toLowerCase();
|
|
754
|
-
if (dimensions !== expected.dimensions || similarity !== expected.similarity) {
|
|
755
|
-
throw new Error(`Neo4j index ${expected.name} has incompatible vector configuration.`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
function validateConstraintRecord(record, expected) {
|
|
760
|
-
const type = requiredString(record.get("type"), `Neo4j constraint ${expected.name} type`);
|
|
761
|
-
const entityType = requiredString(
|
|
762
|
-
record.get("entityType"),
|
|
763
|
-
`Neo4j constraint ${expected.name} entity type`
|
|
764
|
-
);
|
|
765
|
-
const labels = requiredStringArray(
|
|
766
|
-
record.get("labelsOrTypes"),
|
|
767
|
-
`Neo4j constraint ${expected.name} labels`
|
|
768
|
-
);
|
|
769
|
-
const properties2 = requiredStringArray(
|
|
770
|
-
record.get("properties"),
|
|
771
|
-
`Neo4j constraint ${expected.name} properties`
|
|
772
|
-
);
|
|
773
|
-
if (entityType !== "NODE" || type !== "UNIQUENESS" || !sameStrings(labels, [expected.label]) || !sameStrings(properties2, expected.properties)) {
|
|
774
|
-
throw new Error(`Neo4j constraint ${expected.name} does not match its registration.`);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
function requiredObject(value, label) {
|
|
778
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
779
|
-
throw new TypeError(`${label} must be an object.`);
|
|
780
|
-
}
|
|
781
|
-
return value;
|
|
782
|
-
}
|
|
783
|
-
function requiredStringArray(value, label) {
|
|
784
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
785
|
-
throw new TypeError(`${label} must be a string array.`);
|
|
786
|
-
}
|
|
787
|
-
return [...value];
|
|
788
|
-
}
|
|
789
|
-
function requiredSafeInteger(value, label) {
|
|
790
|
-
if (isInt2(value)) {
|
|
791
|
-
if (!value.inSafeRange()) throw new TypeError(`${label} must be a safe integer.`);
|
|
792
|
-
return value.toNumber();
|
|
793
|
-
}
|
|
794
|
-
if (!Number.isSafeInteger(value)) throw new TypeError(`${label} must be a safe integer.`);
|
|
795
|
-
return value;
|
|
796
|
-
}
|
|
797
|
-
function sameStrings(left, right) {
|
|
798
|
-
if (left.length !== right.length) return false;
|
|
799
|
-
const sortedLeft = [...left].sort();
|
|
800
|
-
const sortedRight = [...right].sort();
|
|
801
|
-
return sortedLeft.every((value, index) => value === sortedRight[index]);
|
|
802
|
-
}
|
|
803
|
-
function supportedVersion(value) {
|
|
804
|
-
const match = /^(\d{4})\.(\d+)/.exec(value);
|
|
805
|
-
if (match === null) return false;
|
|
806
|
-
const year = Number(match[1]);
|
|
807
|
-
const month = Number(match[2]);
|
|
808
|
-
return year > 2026 || year === 2026 && month >= 1;
|
|
809
|
-
}
|
|
810
|
-
function constraintName(graph, resource) {
|
|
811
|
-
return `anvia_${graph}_${resource}_identity`;
|
|
812
|
-
}
|
|
813
|
-
function uniquenessConstraint(name, label, properties2) {
|
|
814
|
-
const expression = properties2.length === 1 ? `n.${quoteIdentifier(properties2[0])}` : `(${properties2.map((property) => `n.${quoteIdentifier(property)}`).join(", ")})`;
|
|
815
|
-
return `CREATE CONSTRAINT ${quoteIdentifier(name)} IF NOT EXISTS FOR (n:${quoteIdentifier(label)}) REQUIRE ${expression} IS UNIQUE`;
|
|
816
|
-
}
|
|
817
|
-
function vectorIndex(index, label) {
|
|
818
|
-
return `CREATE VECTOR INDEX ${quoteIdentifier(index.name)} IF NOT EXISTS FOR (n:${quoteIdentifier(label)}) ON (n.${quoteIdentifier("__anvia_embedding")}) OPTIONS {indexConfig: {\`vector.dimensions\`: ${index.dimensions}, \`vector.similarity_function\`: '${index.similarity}'}}`;
|
|
819
|
-
}
|
|
820
|
-
function fulltextIndex(name, label, properties2) {
|
|
821
|
-
if (properties2.length === 0) throw new TypeError(`Full-text index ${name} requires properties.`);
|
|
822
|
-
return `CREATE FULLTEXT INDEX ${quoteIdentifier(name)} IF NOT EXISTS FOR (n:${quoteIdentifier(label)}) ON EACH [${properties2.map((property) => `n.${quoteIdentifier(property)}`).join(", ")}]`;
|
|
823
|
-
}
|
|
824
|
-
function validateReplacement(options, indexes, schema) {
|
|
825
|
-
if (!Array.isArray(options.documents) || !Array.isArray(options.chunks) || !Array.isArray(options.entities) || !Array.isArray(options.relationships) || !Array.isArray(options.mentions)) {
|
|
826
|
-
throw new TypeError("Neo4j replacement collections must be arrays.");
|
|
827
|
-
}
|
|
828
|
-
if (options.conflict !== "error" && options.conflict !== "overwrite" && options.conflict !== "keep-existing") {
|
|
829
|
-
throw new TypeError("Neo4j conflict policy must be error, overwrite, or keep-existing.");
|
|
830
|
-
}
|
|
831
|
-
validateOrphanPolicy(options.orphanEntities);
|
|
832
|
-
const documentIds = uniqueIds(
|
|
833
|
-
options.documents.map((document, index) => {
|
|
834
|
-
requiredObject(document, `Neo4j document at index ${index}`);
|
|
835
|
-
return document.id;
|
|
836
|
-
}),
|
|
837
|
-
"document"
|
|
838
|
-
);
|
|
839
|
-
const documentSet = new Set(documentIds);
|
|
840
|
-
const chunkIds = uniqueIds(
|
|
841
|
-
options.chunks.map((chunk, index) => {
|
|
842
|
-
requiredObject(chunk, `Neo4j chunk at index ${index}`);
|
|
843
|
-
requiredObject(chunk.document, `Neo4j chunk document at index ${index}`);
|
|
844
|
-
return chunk.id;
|
|
845
|
-
}),
|
|
846
|
-
"chunk"
|
|
847
|
-
);
|
|
848
|
-
const chunkSet = new Set(chunkIds);
|
|
849
|
-
const entityIds = uniqueIds(
|
|
850
|
-
options.entities.map((entity, index) => {
|
|
851
|
-
requiredObject(entity, `Neo4j entity at index ${index}`);
|
|
852
|
-
requiredObject(entity.document, `Neo4j entity document at index ${index}`);
|
|
853
|
-
return entity.id;
|
|
854
|
-
}),
|
|
855
|
-
"entity"
|
|
856
|
-
);
|
|
857
|
-
const entitySet = new Set(entityIds);
|
|
858
|
-
for (const document of options.documents)
|
|
859
|
-
parseNeo4jProperties(document.properties ?? {}, `document ${document.id}`);
|
|
860
|
-
const chunkPositions = /* @__PURE__ */ new Set();
|
|
861
|
-
for (const chunk of options.chunks) {
|
|
862
|
-
if (!documentSet.has(chunk.document.documentId))
|
|
863
|
-
throw new TypeError(`Chunk ${chunk.id} references an unknown document.`);
|
|
864
|
-
if (chunk.document.id !== chunk.id)
|
|
865
|
-
throw new TypeError(`Embedded chunk ${chunk.id} has a mismatched document id.`);
|
|
866
|
-
if (!Number.isSafeInteger(chunk.document.index) || chunk.document.index < 0) {
|
|
867
|
-
throw new TypeError(`Chunk ${chunk.id} index must be a non-negative safe integer.`);
|
|
868
|
-
}
|
|
869
|
-
if (typeof chunk.document.text !== "string") {
|
|
870
|
-
throw new TypeError(`Chunk ${chunk.id} text must be a string.`);
|
|
871
|
-
}
|
|
872
|
-
const position = `${chunk.document.documentId}\0${chunk.document.index}`;
|
|
873
|
-
if (chunkPositions.has(position)) {
|
|
874
|
-
throw new TypeError(
|
|
875
|
-
`Document ${chunk.document.documentId} contains duplicate chunk index ${chunk.document.index}.`
|
|
876
|
-
);
|
|
877
|
-
}
|
|
878
|
-
chunkPositions.add(position);
|
|
879
|
-
singleEmbedding(chunk.embeddings, indexes.chunks.vector.dimensions, `chunk ${chunk.id}`);
|
|
880
|
-
parseNeo4jProperties(chunk.document.metadata ?? {}, `chunk ${chunk.id} metadata`);
|
|
881
|
-
}
|
|
882
|
-
const entityTypes = /* @__PURE__ */ new Map();
|
|
883
|
-
const entitySourceChunks = /* @__PURE__ */ new Map();
|
|
884
|
-
for (const entity of options.entities) {
|
|
885
|
-
if (entity.document.key !== entity.id)
|
|
886
|
-
throw new TypeError(`Embedded entity ${entity.id} has a mismatched key.`);
|
|
887
|
-
singleEmbedding(entity.embeddings, indexes.entities.vector.dimensions, `entity ${entity.id}`);
|
|
888
|
-
const type = entity.document.type;
|
|
889
|
-
if (typeof type !== "string") throw new TypeError(`Entity ${entity.id} type must be a string.`);
|
|
890
|
-
const definition = schema.nodes[type];
|
|
891
|
-
if (definition === void 0)
|
|
892
|
-
throw new TypeError(`Entity ${entity.id} has unknown type ${type}.`);
|
|
893
|
-
const properties2 = parseExactSchemaProperties(
|
|
894
|
-
definition.properties,
|
|
895
|
-
entity.document.properties,
|
|
896
|
-
`entity ${entity.id} properties`
|
|
897
|
-
);
|
|
898
|
-
const identity = parseIdentity(entity.document.identity, definition.identity, properties2, type);
|
|
899
|
-
const expectedKey = `${type}:${stableObject(identity)}`;
|
|
900
|
-
if (entity.id !== expectedKey) {
|
|
901
|
-
throw new TypeError(`Entity ${entity.id} does not match its canonical identity key.`);
|
|
902
|
-
}
|
|
903
|
-
const sourceChunkIds = validateSourceChunkIds(
|
|
904
|
-
entity.document.sourceChunkIds,
|
|
905
|
-
chunkSet,
|
|
906
|
-
`entity ${entity.id}`
|
|
907
|
-
);
|
|
908
|
-
entityTypes.set(entity.id, type);
|
|
909
|
-
entitySourceChunks.set(entity.id, new Set(sourceChunkIds));
|
|
910
|
-
}
|
|
911
|
-
uniqueIds(
|
|
912
|
-
options.relationships.map((relationship, index) => {
|
|
913
|
-
requiredObject(relationship, `Neo4j relationship at index ${index}`);
|
|
914
|
-
return relationship.key;
|
|
915
|
-
}),
|
|
916
|
-
"relationship"
|
|
917
|
-
);
|
|
918
|
-
for (const relationship of options.relationships) {
|
|
919
|
-
const definition = schema.relationships[relationship.type];
|
|
920
|
-
if (definition === void 0) {
|
|
921
|
-
throw new TypeError(
|
|
922
|
-
`Relationship ${relationship.key} has unknown type ${relationship.type}.`
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
if (!entitySet.has(relationship.from) || !entitySet.has(relationship.to)) {
|
|
926
|
-
throw new TypeError(`Relationship ${relationship.key} references an unknown entity.`);
|
|
927
|
-
}
|
|
928
|
-
if (entityTypes.get(relationship.from) !== definition.from || entityTypes.get(relationship.to) !== definition.to) {
|
|
929
|
-
throw new TypeError(`Relationship ${relationship.key} has incompatible endpoint types.`);
|
|
930
|
-
}
|
|
931
|
-
const properties2 = parseExactSchemaProperties(
|
|
932
|
-
definition.properties,
|
|
933
|
-
relationship.properties,
|
|
934
|
-
`relationship ${relationship.key} properties`
|
|
935
|
-
);
|
|
936
|
-
const identity = identityFromProperties(
|
|
937
|
-
definition.identity ?? [],
|
|
938
|
-
properties2,
|
|
939
|
-
relationship.type
|
|
940
|
-
);
|
|
941
|
-
const expectedKey = `${relationship.type}:${relationship.from}->${relationship.to}:${stableObject(identity)}`;
|
|
942
|
-
if (relationship.key !== expectedKey) {
|
|
943
|
-
throw new TypeError(`Relationship ${relationship.key} does not match its canonical key.`);
|
|
944
|
-
}
|
|
945
|
-
const sourceChunkIds = validateSourceChunkIds(
|
|
946
|
-
relationship.sourceChunkIds,
|
|
947
|
-
chunkSet,
|
|
948
|
-
`relationship ${relationship.key}`
|
|
949
|
-
);
|
|
950
|
-
const fromSources = entitySourceChunks.get(relationship.from);
|
|
951
|
-
const toSources = entitySourceChunks.get(relationship.to);
|
|
952
|
-
if (fromSources === void 0 || toSources === void 0 || sourceChunkIds.some((chunkId) => !fromSources.has(chunkId) || !toSources.has(chunkId))) {
|
|
953
|
-
throw new TypeError(
|
|
954
|
-
`Relationship ${relationship.key} source chunks must support both endpoint entities.`
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
const mentionKeys = /* @__PURE__ */ new Set();
|
|
959
|
-
const mentionChunksByEntity = /* @__PURE__ */ new Map();
|
|
960
|
-
for (const [index, mention] of options.mentions.entries()) {
|
|
961
|
-
requiredObject(mention, `Neo4j mention at index ${index}`);
|
|
962
|
-
if (!chunkSet.has(mention.chunkId) || !entitySet.has(mention.entityKey)) {
|
|
963
|
-
throw new TypeError(`Mention at index ${index} references an unknown chunk or entity.`);
|
|
964
|
-
}
|
|
965
|
-
const key = `${mention.chunkId}\0${mention.entityKey}`;
|
|
966
|
-
if (mentionKeys.has(key)) throw new TypeError(`Duplicate Neo4j mention at index ${index}.`);
|
|
967
|
-
mentionKeys.add(key);
|
|
968
|
-
const chunks = mentionChunksByEntity.get(mention.entityKey);
|
|
969
|
-
if (chunks === void 0)
|
|
970
|
-
mentionChunksByEntity.set(mention.entityKey, /* @__PURE__ */ new Set([mention.chunkId]));
|
|
971
|
-
else chunks.add(mention.chunkId);
|
|
972
|
-
}
|
|
973
|
-
for (const entityId of entityIds) {
|
|
974
|
-
const sources = [...entitySourceChunks.get(entityId) ?? []];
|
|
975
|
-
const mentions = [...mentionChunksByEntity.get(entityId) ?? []];
|
|
976
|
-
if (!sameStrings(sources, mentions)) {
|
|
977
|
-
throw new TypeError(`Entity ${entityId} source chunks must exactly match its mentions.`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
function parseExactSchemaProperties(schema, value, label) {
|
|
982
|
-
const input = parseNeo4jProperties(value, label);
|
|
983
|
-
const result = schema.safeParse(value);
|
|
984
|
-
if (!result.success) throw new TypeError(`${label} does not match the graph schema.`);
|
|
985
|
-
const output = parseNeo4jProperties(result.data, `${label} output`);
|
|
986
|
-
if (stableObject(input) !== stableObject(output)) {
|
|
987
|
-
throw new TypeError(`${label} must not be coerced, defaulted, transformed, or stripped.`);
|
|
988
|
-
}
|
|
989
|
-
return input;
|
|
990
|
-
}
|
|
991
|
-
function parseIdentity(value, keys, properties2, type) {
|
|
992
|
-
const identity = identityFromProperties(
|
|
993
|
-
keys,
|
|
994
|
-
parseNeo4jProperties(value, `${type} identity`),
|
|
995
|
-
type
|
|
996
|
-
);
|
|
997
|
-
if (!sameStrings(Object.keys(identity), keys)) {
|
|
998
|
-
throw new TypeError(`Entity ${type} identity must contain exactly its declared properties.`);
|
|
999
|
-
}
|
|
1000
|
-
for (const key of keys) {
|
|
1001
|
-
if (identity[key] !== properties2[key]) {
|
|
1002
|
-
throw new TypeError(`Entity ${type} identity property ${key} does not match its properties.`);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return identity;
|
|
1006
|
-
}
|
|
1007
|
-
function identityFromProperties(keys, properties2, type) {
|
|
1008
|
-
const identity = {};
|
|
1009
|
-
for (const key of keys) {
|
|
1010
|
-
const value = properties2[key];
|
|
1011
|
-
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
1012
|
-
throw new TypeError(`Identity property ${key} for ${type} must be a primitive value.`);
|
|
1013
|
-
}
|
|
1014
|
-
identity[key] = value;
|
|
1015
|
-
}
|
|
1016
|
-
return identity;
|
|
1017
|
-
}
|
|
1018
|
-
function validateSourceChunkIds(value, chunkIds, label) {
|
|
1019
|
-
const ids = uniqueIds(value, `${label} source chunk`);
|
|
1020
|
-
if (ids.length === 0) throw new TypeError(`${label} requires source chunk provenance.`);
|
|
1021
|
-
for (const id of ids) {
|
|
1022
|
-
if (!chunkIds.has(id)) throw new TypeError(`${label} references unknown source chunk ${id}.`);
|
|
1023
|
-
}
|
|
1024
|
-
return ids;
|
|
1025
|
-
}
|
|
1026
|
-
function validateOrphanPolicy(value) {
|
|
1027
|
-
if (value !== "delete" && value !== "keep") {
|
|
1028
|
-
throw new TypeError("Neo4j orphan entity policy must be delete or keep.");
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
function singleEmbedding(embeddings, dimensions, label) {
|
|
1032
|
-
if (!Array.isArray(embeddings)) throw new TypeError(`${label} embeddings must be an array.`);
|
|
1033
|
-
if (embeddings.length !== 1) throw new TypeError(`${label} requires exactly one embedding.`);
|
|
1034
|
-
const vector = embeddings[0]?.vector;
|
|
1035
|
-
if (vector === void 0 || vector.length !== dimensions || !vector.every(Number.isFinite)) {
|
|
1036
|
-
throw new TypeError(`${label} has an invalid ${dimensions}-dimension embedding.`);
|
|
1037
|
-
}
|
|
1038
|
-
return vector;
|
|
1039
|
-
}
|
|
1040
|
-
function uniqueIds(ids, label) {
|
|
1041
|
-
if (!Array.isArray(ids)) throw new TypeError(`Neo4j ${label} ids must be an array.`);
|
|
1042
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1043
|
-
for (const id of ids) {
|
|
1044
|
-
if (typeof id !== "string" || id.length === 0)
|
|
1045
|
-
throw new TypeError(`Neo4j ${label} ids must be non-empty strings.`);
|
|
1046
|
-
if (seen.has(id)) throw new TypeError(`Duplicate Neo4j ${label} id: ${id}`);
|
|
1047
|
-
seen.add(id);
|
|
1048
|
-
}
|
|
1049
|
-
return [...seen];
|
|
1050
|
-
}
|
|
1051
|
-
function replacementSnapshotTargets(options) {
|
|
1052
|
-
return {
|
|
1053
|
-
documentIds: options.documents.map((document) => document.id),
|
|
1054
|
-
chunkIds: options.chunks.map((chunk) => chunk.id),
|
|
1055
|
-
entityKeys: options.entities.map((entity) => entity.id),
|
|
1056
|
-
relationshipKeys: options.relationships.map((relationship) => relationship.key),
|
|
1057
|
-
mentions: options.mentions
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
function emptySnapshotTargets(documentIds) {
|
|
1061
|
-
return {
|
|
1062
|
-
documentIds,
|
|
1063
|
-
chunkIds: [],
|
|
1064
|
-
entityKeys: [],
|
|
1065
|
-
relationshipKeys: [],
|
|
1066
|
-
mentions: []
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
function expandSnapshotTargets(targets, snapshot) {
|
|
1070
|
-
const mentions = new Map(
|
|
1071
|
-
targets.mentions.map((mention) => [mentionKey(mention.chunkId, mention.entityKey), mention])
|
|
1072
|
-
);
|
|
1073
|
-
for (const key of snapshot.mentions.keys()) {
|
|
1074
|
-
const separator = key.indexOf("\0");
|
|
1075
|
-
if (separator < 0) throw new TypeError("Stored Neo4j mention has an invalid identity.");
|
|
1076
|
-
mentions.set(key, { chunkId: key.slice(0, separator), entityKey: key.slice(separator + 1) });
|
|
1077
|
-
}
|
|
1078
|
-
return {
|
|
1079
|
-
documentIds: targets.documentIds,
|
|
1080
|
-
chunkIds: [.../* @__PURE__ */ new Set([...targets.chunkIds, ...snapshot.chunks.keys()])],
|
|
1081
|
-
entityKeys: [.../* @__PURE__ */ new Set([...targets.entityKeys, ...snapshot.entities.keys()])],
|
|
1082
|
-
relationshipKeys: [.../* @__PURE__ */ new Set([...targets.relationshipKeys, ...snapshot.relationships.keys()])],
|
|
1083
|
-
mentions: [...mentions.values()]
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
async function snapshotGraphState(transaction, graphName, labels, targets) {
|
|
1087
|
-
const documentsResult = await transaction.run(
|
|
1088
|
-
`MATCH (d:${quoteIdentifier(labels.document)})
|
|
1089
|
-
WHERE d.${quoteIdentifier("__anvia_id")} IN $documentIds
|
|
1090
|
-
RETURN d.${quoteIdentifier("__anvia_id")} AS key, properties(d) AS properties`,
|
|
1091
|
-
{ documentIds: targets.documentIds }
|
|
1092
|
-
);
|
|
1093
|
-
const chunksResult = await transaction.run(
|
|
1094
|
-
`MATCH (c:${quoteIdentifier(labels.chunk)})
|
|
1095
|
-
OPTIONAL MATCH (d:${quoteIdentifier(labels.document)})-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c)
|
|
1096
|
-
WITH c, d WHERE c.${quoteIdentifier("__anvia_id")} IN $chunkIds OR d.${quoteIdentifier("__anvia_id")} IN $documentIds
|
|
1097
|
-
RETURN c.${quoteIdentifier("__anvia_id")} AS key, properties(c) AS properties`,
|
|
1098
|
-
{ chunkIds: targets.chunkIds, documentIds: targets.documentIds }
|
|
1099
|
-
);
|
|
1100
|
-
const entitiesResult = await transaction.run(
|
|
1101
|
-
`MATCH (e:${quoteIdentifier(labels.entity)})
|
|
1102
|
-
WHERE e.${quoteIdentifier("__anvia_graph")} = $graph AND (
|
|
1103
|
-
e.${quoteIdentifier("__anvia_key")} IN $entityKeys OR
|
|
1104
|
-
any(id IN coalesce(e.${quoteIdentifier("__anvia_source_document_ids")}, []) WHERE id IN $documentIds)
|
|
1105
|
-
)
|
|
1106
|
-
RETURN e.${quoteIdentifier("__anvia_key")} AS key, labels(e) AS labels, properties(e) AS properties`,
|
|
1107
|
-
{ graph: graphName, entityKeys: targets.entityKeys, documentIds: targets.documentIds }
|
|
1108
|
-
);
|
|
1109
|
-
const relationshipsResult = await transaction.run(
|
|
1110
|
-
`MATCH ()-[r]->()
|
|
1111
|
-
WHERE r.${quoteIdentifier("__anvia_graph")} = $graph AND (
|
|
1112
|
-
r.${quoteIdentifier("__anvia_key")} IN $relationshipKeys OR
|
|
1113
|
-
any(id IN coalesce(r.${quoteIdentifier("__anvia_source_document_ids")}, []) WHERE id IN $documentIds)
|
|
1114
|
-
)
|
|
1115
|
-
RETURN r.${quoteIdentifier("__anvia_key")} AS key,
|
|
1116
|
-
type(r) AS type,
|
|
1117
|
-
startNode(r).${quoteIdentifier("__anvia_key")} AS source,
|
|
1118
|
-
endNode(r).${quoteIdentifier("__anvia_key")} AS target,
|
|
1119
|
-
properties(r) AS properties`,
|
|
1120
|
-
{
|
|
1121
|
-
graph: graphName,
|
|
1122
|
-
relationshipKeys: targets.relationshipKeys,
|
|
1123
|
-
documentIds: targets.documentIds
|
|
1124
|
-
}
|
|
1125
|
-
);
|
|
1126
|
-
const mentionsResult = await transaction.run(
|
|
1127
|
-
`MATCH (c:${quoteIdentifier(labels.chunk)})-[:${quoteIdentifier("ANVIA_MENTIONS")}]->(e:${quoteIdentifier(labels.entity)})
|
|
1128
|
-
OPTIONAL MATCH (d:${quoteIdentifier(labels.document)})-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c)
|
|
1129
|
-
WITH c, e, d WHERE d.${quoteIdentifier("__anvia_id")} IN $documentIds OR
|
|
1130
|
-
any(item IN $mentions WHERE item.chunkId = c.${quoteIdentifier("__anvia_id")} AND item.entityKey = e.${quoteIdentifier("__anvia_key")})
|
|
1131
|
-
RETURN c.${quoteIdentifier("__anvia_id")} AS chunkId, e.${quoteIdentifier("__anvia_key")} AS entityKey`,
|
|
1132
|
-
{ documentIds: targets.documentIds, mentions: targets.mentions }
|
|
1133
|
-
);
|
|
1134
|
-
return {
|
|
1135
|
-
documents: snapshotProperties(documentsResult.records, "document"),
|
|
1136
|
-
chunks: snapshotProperties(chunksResult.records, "chunk"),
|
|
1137
|
-
entities: snapshotProperties(entitiesResult.records, "entity", true),
|
|
1138
|
-
relationships: snapshotRelationships(relationshipsResult.records),
|
|
1139
|
-
mentions: snapshotMentions(mentionsResult.records)
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
function snapshotProperties(records, resource, includeLabels = false) {
|
|
1143
|
-
const output = /* @__PURE__ */ new Map();
|
|
1144
|
-
for (const record of records) {
|
|
1145
|
-
const key = requiredString(record.get("key"), `Neo4j ${resource} snapshot key`);
|
|
1146
|
-
if (output.has(key))
|
|
1147
|
-
throw new TypeError(`Neo4j ${resource} snapshot contains duplicate ${key}.`);
|
|
1148
|
-
const properties2 = normalizedSnapshotProperties(
|
|
1149
|
-
record.get("properties"),
|
|
1150
|
-
`Neo4j ${resource} snapshot`
|
|
1151
|
-
);
|
|
1152
|
-
const labels = includeLabels ? requiredStringArray(record.get("labels"), `Neo4j ${resource} snapshot labels`).sort() : [];
|
|
1153
|
-
output.set(key, stableObject({ labels: labels.join("\0"), properties: properties2 }));
|
|
1154
|
-
}
|
|
1155
|
-
return output;
|
|
1156
|
-
}
|
|
1157
|
-
function snapshotRelationships(records) {
|
|
1158
|
-
const output = /* @__PURE__ */ new Map();
|
|
1159
|
-
for (const record of records) {
|
|
1160
|
-
const key = requiredString(record.get("key"), "Neo4j relationship snapshot key");
|
|
1161
|
-
if (output.has(key))
|
|
1162
|
-
throw new TypeError(`Neo4j relationship snapshot contains duplicate ${key}.`);
|
|
1163
|
-
output.set(
|
|
1164
|
-
key,
|
|
1165
|
-
stableObject({
|
|
1166
|
-
source: requiredString(record.get("source"), "Neo4j relationship snapshot source"),
|
|
1167
|
-
target: requiredString(record.get("target"), "Neo4j relationship snapshot target"),
|
|
1168
|
-
type: requiredString(record.get("type"), "Neo4j relationship snapshot type"),
|
|
1169
|
-
properties: normalizedSnapshotProperties(
|
|
1170
|
-
record.get("properties"),
|
|
1171
|
-
"Neo4j relationship snapshot"
|
|
1172
|
-
)
|
|
1173
|
-
})
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
return output;
|
|
1177
|
-
}
|
|
1178
|
-
function snapshotMentions(records) {
|
|
1179
|
-
const output = /* @__PURE__ */ new Map();
|
|
1180
|
-
for (const record of records) {
|
|
1181
|
-
const key = mentionKey(
|
|
1182
|
-
requiredString(record.get("chunkId"), "Neo4j mention chunk id"),
|
|
1183
|
-
requiredString(record.get("entityKey"), "Neo4j mention entity key")
|
|
1184
|
-
);
|
|
1185
|
-
if (output.has(key)) throw new TypeError(`Neo4j mention snapshot contains duplicate ${key}.`);
|
|
1186
|
-
output.set(key, key);
|
|
1187
|
-
}
|
|
1188
|
-
return output;
|
|
1189
|
-
}
|
|
1190
|
-
function normalizedSnapshotProperties(value, label) {
|
|
1191
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1192
|
-
throw new TypeError(`${label} properties must be an object.`);
|
|
1193
|
-
}
|
|
1194
|
-
const normalized = {};
|
|
1195
|
-
for (const [key, item] of Object.entries(value)) {
|
|
1196
|
-
normalized[key] = parseNeo4jPropertyValue(
|
|
1197
|
-
normalizeSnapshotDriverValue(item, `${label}.${key}`),
|
|
1198
|
-
`${label}.${key}`
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
for (const property of ["__anvia_source_document_ids", "__anvia_source_chunk_ids"]) {
|
|
1202
|
-
const items = normalized[property];
|
|
1203
|
-
if (items !== void 0) {
|
|
1204
|
-
if (!Array.isArray(items) || !items.every((item) => typeof item === "string")) {
|
|
1205
|
-
throw new TypeError(`${label}.${property} must be a string array.`);
|
|
1206
|
-
}
|
|
1207
|
-
normalized[property] = [...new Set(items)].sort();
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
return stableObject(normalized);
|
|
1211
|
-
}
|
|
1212
|
-
function normalizeSnapshotDriverValue(value, label) {
|
|
1213
|
-
if (isInt2(value)) {
|
|
1214
|
-
if (!value.inSafeRange()) throw new TypeError(`${label} contains an unsafe Neo4j integer.`);
|
|
1215
|
-
return value.toNumber();
|
|
1216
|
-
}
|
|
1217
|
-
if (Array.isArray(value)) {
|
|
1218
|
-
return value.map((item, index) => normalizeSnapshotDriverValue(item, `${label}[${index}]`));
|
|
1219
|
-
}
|
|
1220
|
-
return value;
|
|
1221
|
-
}
|
|
1222
|
-
function mentionKey(chunkId, entityKey2) {
|
|
1223
|
-
return `${chunkId}\0${entityKey2}`;
|
|
1224
|
-
}
|
|
1225
|
-
function diffGraphSnapshots(before, after) {
|
|
1226
|
-
return {
|
|
1227
|
-
documents: diffSnapshotMaps(before.documents, after.documents),
|
|
1228
|
-
chunks: diffSnapshotMaps(before.chunks, after.chunks),
|
|
1229
|
-
entities: diffSnapshotMaps(before.entities, after.entities),
|
|
1230
|
-
relationships: diffSnapshotMaps(before.relationships, after.relationships),
|
|
1231
|
-
mentions: diffSnapshotMaps(before.mentions, after.mentions)
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
function diffSnapshotMaps(before, after) {
|
|
1235
|
-
let created = 0;
|
|
1236
|
-
let updated = 0;
|
|
1237
|
-
let deleted = 0;
|
|
1238
|
-
let unchanged = 0;
|
|
1239
|
-
for (const key of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
|
|
1240
|
-
const previous = before.get(key);
|
|
1241
|
-
const next = after.get(key);
|
|
1242
|
-
if (previous === void 0) created += 1;
|
|
1243
|
-
else if (next === void 0) deleted += 1;
|
|
1244
|
-
else if (previous === next) unchanged += 1;
|
|
1245
|
-
else updated += 1;
|
|
1246
|
-
}
|
|
1247
|
-
return { created, updated, deleted, unchanged };
|
|
1248
|
-
}
|
|
1249
|
-
function emptyWriteResult() {
|
|
1250
|
-
const empty = () => ({ created: 0, updated: 0, deleted: 0, unchanged: 0 });
|
|
1251
|
-
return {
|
|
1252
|
-
documents: empty(),
|
|
1253
|
-
chunks: empty(),
|
|
1254
|
-
entities: empty(),
|
|
1255
|
-
relationships: empty(),
|
|
1256
|
-
mentions: empty()
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
async function removeDocuments(transaction, graphName, labels, documentIds, orphanEntities) {
|
|
1260
|
-
const chunks = await transaction.run(
|
|
1261
|
-
`MATCH (d:${quoteIdentifier(labels.document)})-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c:${quoteIdentifier(labels.chunk)}) WHERE d.${quoteIdentifier("__anvia_id")} IN $documentIds RETURN collect(c.${quoteIdentifier("__anvia_id")}) AS chunkIds`,
|
|
1262
|
-
{ documentIds }
|
|
1263
|
-
);
|
|
1264
|
-
const chunkIdsValue = chunks.records[0]?.get("chunkIds");
|
|
1265
|
-
const chunkIds = requiredStringArray(chunkIdsValue, "Neo4j replacement chunk ids");
|
|
1266
|
-
await transaction.run(
|
|
1267
|
-
`MATCH ()-[r]->() WHERE r.${quoteIdentifier("__anvia_graph")} = $graph AND any(id IN r.${quoteIdentifier("__anvia_source_document_ids")} WHERE id IN $documentIds)
|
|
1268
|
-
SET r.${quoteIdentifier("__anvia_source_document_ids")} = [id IN r.${quoteIdentifier("__anvia_source_document_ids")} WHERE NOT id IN $documentIds]
|
|
1269
|
-
SET r.${quoteIdentifier("__anvia_source_chunk_ids")} = [id IN coalesce(r.${quoteIdentifier("__anvia_source_chunk_ids")}, []) WHERE NOT id IN $chunkIds]
|
|
1270
|
-
WITH r WHERE size(r.${quoteIdentifier("__anvia_source_document_ids")}) = 0 DELETE r`,
|
|
1271
|
-
{ graph: graphName, documentIds, chunkIds }
|
|
1272
|
-
);
|
|
1273
|
-
await transaction.run(
|
|
1274
|
-
`MATCH (d:${quoteIdentifier(labels.document)}) WHERE d.${quoteIdentifier("__anvia_id")} IN $documentIds
|
|
1275
|
-
OPTIONAL MATCH (d)-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c:${quoteIdentifier(labels.chunk)})
|
|
1276
|
-
DETACH DELETE c, d`,
|
|
1277
|
-
{ documentIds }
|
|
1278
|
-
);
|
|
1279
|
-
await transaction.run(
|
|
1280
|
-
`MATCH (e:${quoteIdentifier(labels.entity)})
|
|
1281
|
-
WHERE e.${quoteIdentifier("__anvia_graph")} = $graph AND
|
|
1282
|
-
any(id IN coalesce(e.${quoteIdentifier("__anvia_source_document_ids")}, []) WHERE id IN $documentIds)
|
|
1283
|
-
SET e.${quoteIdentifier("__anvia_source_document_ids")} = [id IN e.${quoteIdentifier("__anvia_source_document_ids")} WHERE NOT id IN $documentIds],
|
|
1284
|
-
e.${quoteIdentifier("__anvia_source_chunk_ids")} = [id IN coalesce(e.${quoteIdentifier("__anvia_source_chunk_ids")}, []) WHERE NOT id IN $chunkIds]
|
|
1285
|
-
WITH e WHERE $deleteOrphans AND size(e.${quoteIdentifier("__anvia_source_document_ids")}) = 0
|
|
1286
|
-
DETACH DELETE e`,
|
|
1287
|
-
{
|
|
1288
|
-
graph: graphName,
|
|
1289
|
-
documentIds,
|
|
1290
|
-
chunkIds,
|
|
1291
|
-
deleteOrphans: orphanEntities === "delete"
|
|
1292
|
-
}
|
|
1293
|
-
);
|
|
1294
|
-
}
|
|
1295
|
-
async function writeDocuments(transaction, labels, documents) {
|
|
1296
|
-
await transaction.run(
|
|
1297
|
-
`UNWIND $rows AS row MERGE (d:${quoteIdentifier(labels.document)} {${quoteIdentifier("__anvia_id")}: row.id}) SET d += row.properties`,
|
|
1298
|
-
{
|
|
1299
|
-
rows: documents.map((document) => ({
|
|
1300
|
-
id: document.id,
|
|
1301
|
-
properties: document.properties ?? {}
|
|
1302
|
-
}))
|
|
1303
|
-
}
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
async function writeChunks(transaction, labels, chunks) {
|
|
1307
|
-
const rows = chunks.map((chunk) => ({
|
|
1308
|
-
id: chunk.id,
|
|
1309
|
-
documentId: chunk.document.documentId,
|
|
1310
|
-
index: chunk.document.index,
|
|
1311
|
-
text: chunk.document.text,
|
|
1312
|
-
metadata: chunk.document.metadata ?? {},
|
|
1313
|
-
embedding: chunk.embeddings[0]?.vector
|
|
1314
|
-
}));
|
|
1315
|
-
await transaction.run(
|
|
1316
|
-
`UNWIND $rows AS row
|
|
1317
|
-
MATCH (d:${quoteIdentifier(labels.document)} {${quoteIdentifier("__anvia_id")}: row.documentId})
|
|
1318
|
-
CREATE (c:${quoteIdentifier(labels.chunk)} {${quoteIdentifier("__anvia_id")}: row.id, ${quoteIdentifier("__anvia_document_id")}: row.documentId, ${quoteIdentifier("__anvia_index")}: row.index, ${quoteIdentifier("__anvia_text")}: row.text, ${quoteIdentifier("__anvia_embedding")}: row.embedding})
|
|
1319
|
-
SET c += row.metadata
|
|
1320
|
-
CREATE (d)-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c)`,
|
|
1321
|
-
{ rows }
|
|
1322
|
-
);
|
|
1323
|
-
const pairs = [...rows].sort(
|
|
1324
|
-
(left, right) => left.documentId.localeCompare(right.documentId) || left.index - right.index
|
|
1325
|
-
).flatMap((row, index, all) => {
|
|
1326
|
-
const next = all[index + 1];
|
|
1327
|
-
return next !== void 0 && next.documentId === row.documentId ? [{ from: row.id, to: next.id }] : [];
|
|
1328
|
-
});
|
|
1329
|
-
if (pairs.length > 0) {
|
|
1330
|
-
await transaction.run(
|
|
1331
|
-
`UNWIND $rows AS row MATCH (a:${quoteIdentifier(labels.chunk)} {${quoteIdentifier("__anvia_id")}: row.from}), (b:${quoteIdentifier(labels.chunk)} {${quoteIdentifier("__anvia_id")}: row.to}) CREATE (a)-[:${quoteIdentifier("ANVIA_NEXT_CHUNK")}]->(b)`,
|
|
1332
|
-
{ rows: pairs }
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
async function writeEntities(transaction, graphName, entityLabel, entities, conflict, sourceDocuments) {
|
|
1337
|
-
const grouped = groupBy(entities, (entity) => entity.document.type);
|
|
1338
|
-
for (const [type, values] of grouped) {
|
|
1339
|
-
const rows = values.map((entity) => ({
|
|
1340
|
-
key: entity.id,
|
|
1341
|
-
properties: entity.document.properties,
|
|
1342
|
-
embedding: entity.embeddings[0]?.vector,
|
|
1343
|
-
sourceDocumentIds: sourceDocuments(entity.document.sourceChunkIds),
|
|
1344
|
-
sourceChunkIds: entity.document.sourceChunkIds
|
|
1345
|
-
}));
|
|
1346
|
-
if (conflict === "error") await assertEntityConflicts(transaction, entityLabel, rows);
|
|
1347
|
-
const propertySet = conflict === "keep-existing" ? `ON CREATE SET e += row.properties
|
|
1348
|
-
WITH e, row,
|
|
1349
|
-
coalesce(e.${quoteIdentifier("__anvia_source_document_ids")}, []) AS existingDocumentIds,
|
|
1350
|
-
coalesce(e.${quoteIdentifier("__anvia_source_chunk_ids")}, []) AS existingChunkIds` : `WITH e, row,
|
|
1351
|
-
coalesce(e.${quoteIdentifier("__anvia_source_document_ids")}, []) AS existingDocumentIds,
|
|
1352
|
-
coalesce(e.${quoteIdentifier("__anvia_source_chunk_ids")}, []) AS existingChunkIds
|
|
1353
|
-
${conflict === "overwrite" ? "SET e = row.properties" : "SET e += row.properties"}`;
|
|
1354
|
-
await transaction.run(
|
|
1355
|
-
`UNWIND $rows AS row
|
|
1356
|
-
MERGE (e:${quoteIdentifier(entityLabel)}:${quoteIdentifier(type)} {${quoteIdentifier("__anvia_key")}: row.key})
|
|
1357
|
-
${propertySet}
|
|
1358
|
-
SET e.${quoteIdentifier("__anvia_key")} = row.key,
|
|
1359
|
-
e.${quoteIdentifier("__anvia_graph")} = $graph,
|
|
1360
|
-
e.${quoteIdentifier("__anvia_embedding")} = ${conflict === "keep-existing" ? `coalesce(e.${quoteIdentifier("__anvia_embedding")}, row.embedding)` : "row.embedding"},
|
|
1361
|
-
e.${quoteIdentifier("__anvia_source_document_ids")} = reduce(all = existingDocumentIds, id IN row.sourceDocumentIds | CASE WHEN id IN all THEN all ELSE all + id END),
|
|
1362
|
-
e.${quoteIdentifier("__anvia_source_chunk_ids")} = reduce(all = existingChunkIds, id IN row.sourceChunkIds | CASE WHEN id IN all THEN all ELSE all + id END)`,
|
|
1363
|
-
{ rows, graph: graphName }
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
async function assertEntityConflicts(transaction, entityLabel, rows) {
|
|
1368
|
-
const result = await transaction.run(
|
|
1369
|
-
`UNWIND $keys AS key MATCH (e:${quoteIdentifier(entityLabel)} {${quoteIdentifier("__anvia_key")}: key}) RETURN key, properties(e) AS properties`,
|
|
1370
|
-
{ keys: rows.map((row) => row.key) }
|
|
1371
|
-
);
|
|
1372
|
-
const incoming = new Map(rows.map((row) => [row.key, row.properties]));
|
|
1373
|
-
for (const record of result.records) {
|
|
1374
|
-
const key = requiredString(record.get("key"), "Neo4j entity key");
|
|
1375
|
-
const existing = applicationProperties(record.get("properties"));
|
|
1376
|
-
if (stableObject(existing) !== stableObject(incoming.get(key) ?? {})) {
|
|
1377
|
-
throw new Error(`Neo4j entity ${key} conflicts with existing properties.`);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
async function writeMentions(transaction, labels, mentions) {
|
|
1382
|
-
if (mentions.length === 0) return;
|
|
1383
|
-
await transaction.run(
|
|
1384
|
-
`UNWIND $rows AS row
|
|
1385
|
-
MATCH (c:${quoteIdentifier(labels.chunk)} {${quoteIdentifier("__anvia_id")}: row.chunkId}), (e:${quoteIdentifier(labels.entity)} {${quoteIdentifier("__anvia_key")}: row.entityKey})
|
|
1386
|
-
MERGE (c)-[:${quoteIdentifier("ANVIA_MENTIONS")}]->(e)`,
|
|
1387
|
-
{ rows: mentions }
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
async function writeRelationships(transaction, graphName, entityLabel, relationships, conflict, sourceDocuments) {
|
|
1391
|
-
const grouped = groupBy(relationships, (relationship) => relationship.type);
|
|
1392
|
-
for (const [type, values] of grouped) {
|
|
1393
|
-
const rows = values.map((relationship) => ({
|
|
1394
|
-
key: relationship.key,
|
|
1395
|
-
from: relationship.from,
|
|
1396
|
-
to: relationship.to,
|
|
1397
|
-
properties: relationship.properties,
|
|
1398
|
-
sourceDocumentIds: sourceDocuments(relationship.sourceChunkIds),
|
|
1399
|
-
sourceChunkIds: relationship.sourceChunkIds
|
|
1400
|
-
}));
|
|
1401
|
-
if (conflict === "error") {
|
|
1402
|
-
const existing = await transaction.run(
|
|
1403
|
-
`UNWIND $keys AS key MATCH ()-[r:${quoteIdentifier(type)} {${quoteIdentifier("__anvia_key")}: key}]->() RETURN key, properties(r) AS properties`,
|
|
1404
|
-
{ keys: rows.map((row) => row.key) }
|
|
1405
|
-
);
|
|
1406
|
-
const incoming = new Map(rows.map((row) => [row.key, row.properties]));
|
|
1407
|
-
for (const record of existing.records) {
|
|
1408
|
-
const key = requiredString(record.get("key"), "Neo4j relationship key");
|
|
1409
|
-
if (stableObject(applicationProperties(record.get("properties"))) !== stableObject(incoming.get(key) ?? {})) {
|
|
1410
|
-
throw new Error(`Neo4j relationship ${key} conflicts with existing properties.`);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
const propertySet = conflict === "keep-existing" ? `ON CREATE SET r += row.properties
|
|
1415
|
-
WITH r, row,
|
|
1416
|
-
coalesce(r.${quoteIdentifier("__anvia_source_document_ids")}, []) AS existingDocumentIds,
|
|
1417
|
-
coalesce(r.${quoteIdentifier("__anvia_source_chunk_ids")}, []) AS existingChunkIds` : `WITH r, row,
|
|
1418
|
-
coalesce(r.${quoteIdentifier("__anvia_source_document_ids")}, []) AS existingDocumentIds,
|
|
1419
|
-
coalesce(r.${quoteIdentifier("__anvia_source_chunk_ids")}, []) AS existingChunkIds
|
|
1420
|
-
${conflict === "overwrite" ? "SET r = row.properties" : "SET r += row.properties"}`;
|
|
1421
|
-
await transaction.run(
|
|
1422
|
-
`UNWIND $rows AS row
|
|
1423
|
-
MATCH (a:${quoteIdentifier(entityLabel)} {${quoteIdentifier("__anvia_key")}: row.from}), (b:${quoteIdentifier(entityLabel)} {${quoteIdentifier("__anvia_key")}: row.to})
|
|
1424
|
-
MERGE (a)-[r:${quoteIdentifier(type)} {${quoteIdentifier("__anvia_key")}: row.key}]->(b)
|
|
1425
|
-
${propertySet}
|
|
1426
|
-
SET r.${quoteIdentifier("__anvia_key")} = row.key,
|
|
1427
|
-
r.${quoteIdentifier("__anvia_graph")} = $graph,
|
|
1428
|
-
r.${quoteIdentifier("__anvia_source_document_ids")} = reduce(all = existingDocumentIds, id IN row.sourceDocumentIds | CASE WHEN id IN all THEN all ELSE all + id END),
|
|
1429
|
-
r.${quoteIdentifier("__anvia_source_chunk_ids")} = reduce(all = existingChunkIds, id IN row.sourceChunkIds | CASE WHEN id IN all THEN all ELSE all + id END)`,
|
|
1430
|
-
{ rows, graph: graphName }
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
function requiredString(value, label) {
|
|
1435
|
-
if (typeof value !== "string") throw new TypeError(`${label} must be a string.`);
|
|
1436
|
-
return value;
|
|
1437
|
-
}
|
|
1438
|
-
function applicationProperties(value) {
|
|
1439
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
1440
|
-
return parseNeo4jProperties(
|
|
1441
|
-
Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("__anvia_"))),
|
|
1442
|
-
"stored Neo4j properties"
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
function groupBy(values, keyFor) {
|
|
1446
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1447
|
-
for (const value of values) {
|
|
1448
|
-
const key = keyFor(value);
|
|
1449
|
-
const group = groups.get(key);
|
|
1450
|
-
if (group === void 0) groups.set(key, [value]);
|
|
1451
|
-
else group.push(value);
|
|
1452
|
-
}
|
|
1453
|
-
return groups;
|
|
1454
|
-
}
|
|
2
|
+
retrieveGraphContext
|
|
3
|
+
} from "./chunk-YLW4LCKJ.js";
|
|
4
|
+
import {
|
|
5
|
+
exploreGraph
|
|
6
|
+
} from "./chunk-PWE3ETSF.js";
|
|
7
|
+
import {
|
|
8
|
+
ManagedNeo4jKnowledgeGraph,
|
|
9
|
+
Neo4jKnowledgeGraph,
|
|
10
|
+
defineNeo4jGraphSchema
|
|
11
|
+
} from "./chunk-QNHOT7P2.js";
|
|
1455
12
|
|
|
1456
13
|
// src/client.ts
|
|
14
|
+
import { auth, driver as createDriver } from "neo4j-driver";
|
|
1457
15
|
var Neo4jClient = class {
|
|
1458
16
|
driver;
|
|
1459
17
|
ownsDriver;
|
|
@@ -1504,768 +62,17 @@ var Neo4jClient = class {
|
|
|
1504
62
|
};
|
|
1505
63
|
|
|
1506
64
|
// src/extract.ts
|
|
1507
|
-
import {
|
|
1508
|
-
import { extract } from "@anvia/core/extractor";
|
|
1509
|
-
import { z as z2 } from "zod";
|
|
65
|
+
import { extractGraphFacts as extractSharedGraphFacts, GraphFactConflictError } from "@anvia/graph";
|
|
1510
66
|
async function extractGraphFacts(options) {
|
|
1511
|
-
|
|
1512
|
-
throwIfAborted(options.abortSignal);
|
|
1513
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1514
|
-
for (const chunk of options.chunks) {
|
|
1515
|
-
if (ids.has(chunk.id)) throw new TypeError(`Duplicate graph chunk id: ${chunk.id}`);
|
|
1516
|
-
ids.add(chunk.id);
|
|
1517
|
-
if (typeof chunk.text !== "string")
|
|
1518
|
-
throw new TypeError(`Graph chunk ${chunk.id} text must be a string.`);
|
|
1519
|
-
}
|
|
1520
|
-
const outputSchema = extractionSchema(options.schema);
|
|
1521
|
-
const instructions = graphInstructions(options.schema, options.instructions);
|
|
1522
|
-
const extractions = await mapConcurrent(
|
|
1523
|
-
options.chunks,
|
|
1524
|
-
options.concurrency ?? 1,
|
|
1525
|
-
async (chunk) => {
|
|
1526
|
-
throwIfAborted(options.abortSignal);
|
|
1527
|
-
const result = await extract({
|
|
1528
|
-
model: options.model,
|
|
1529
|
-
text: chunk.text,
|
|
1530
|
-
outputSchema,
|
|
1531
|
-
instructions,
|
|
1532
|
-
retries: options.retries,
|
|
1533
|
-
abortSignal: options.abortSignal
|
|
1534
|
-
});
|
|
1535
|
-
return { chunkId: chunk.id, output: result.output, usage: result.usage };
|
|
1536
|
-
}
|
|
1537
|
-
);
|
|
1538
|
-
return {
|
|
1539
|
-
output: normalizeFacts(options.schema, extractions),
|
|
1540
|
-
usage: extractions.reduce((usage, item) => Usage.add(usage, item.usage), Usage.empty())
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
function extractionSchema(schema) {
|
|
1544
|
-
const entityVariants = Object.entries(schema.nodes).map(
|
|
1545
|
-
([type, definition]) => z2.object({
|
|
1546
|
-
ref: z2.string().min(1),
|
|
1547
|
-
type: z2.literal(type),
|
|
1548
|
-
properties: definition.properties
|
|
1549
|
-
})
|
|
1550
|
-
);
|
|
1551
|
-
const relationshipVariants = Object.entries(schema.relationships).map(
|
|
1552
|
-
([type, definition]) => z2.object({
|
|
1553
|
-
type: z2.literal(type),
|
|
1554
|
-
from: z2.string().min(1),
|
|
1555
|
-
to: z2.string().min(1),
|
|
1556
|
-
properties: definition.properties
|
|
1557
|
-
})
|
|
1558
|
-
);
|
|
1559
|
-
const entity = union(entityVariants, z2.never());
|
|
1560
|
-
const relationship = union(relationshipVariants, z2.never());
|
|
1561
|
-
return z2.object({
|
|
1562
|
-
entities: z2.array(entity),
|
|
1563
|
-
relationships: z2.array(relationship)
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
function union(items, empty) {
|
|
1567
|
-
if (items.length === 0) return empty;
|
|
1568
|
-
if (items.length === 1) return items[0];
|
|
1569
|
-
return z2.union(items);
|
|
1570
|
-
}
|
|
1571
|
-
function graphInstructions(schema, instructions) {
|
|
1572
|
-
const nodes = Object.entries(schema.nodes).map(([type, definition]) => `- ${type}: ${definition.description}`).join("\n");
|
|
1573
|
-
const relationships = Object.entries(schema.relationships).map(
|
|
1574
|
-
([type, definition]) => `- ${type}: ${definition.from} -> ${definition.to}. ${definition.description}`
|
|
1575
|
-
).join("\n");
|
|
1576
|
-
return [
|
|
1577
|
-
"Extract only facts directly supported by the source text.",
|
|
1578
|
-
"Give every extracted entity a unique local ref and use those refs as relationship endpoints.",
|
|
1579
|
-
"Do not invent entity or relationship types outside the declared schema.",
|
|
1580
|
-
`Node types:
|
|
1581
|
-
${nodes}`,
|
|
1582
|
-
relationships.length === 0 ? "No relationship types are allowed." : `Relationship types:
|
|
1583
|
-
${relationships}`,
|
|
1584
|
-
instructions
|
|
1585
|
-
].filter((part) => part !== void 0 && part.length > 0).join("\n\n");
|
|
1586
|
-
}
|
|
1587
|
-
function normalizeFacts(schema, extractions) {
|
|
1588
|
-
const entities = /* @__PURE__ */ new Map();
|
|
1589
|
-
const relationships = /* @__PURE__ */ new Map();
|
|
1590
|
-
const mentions = /* @__PURE__ */ new Map();
|
|
1591
|
-
for (const extraction of extractions) {
|
|
1592
|
-
const refs = /* @__PURE__ */ new Map();
|
|
1593
|
-
for (const raw of extraction.output.entities) {
|
|
1594
|
-
if (refs.has(raw.ref))
|
|
1595
|
-
throw new TypeError(`Duplicate entity ref ${raw.ref} in chunk ${extraction.chunkId}.`);
|
|
1596
|
-
const definition = schema.nodes[raw.type];
|
|
1597
|
-
if (definition === void 0) throw new TypeError(`Unknown graph entity type: ${raw.type}`);
|
|
1598
|
-
const properties2 = parseNeo4jProperties(
|
|
1599
|
-
definition.properties.parse(raw.properties),
|
|
1600
|
-
`entity ${raw.type}`
|
|
1601
|
-
);
|
|
1602
|
-
const identity = identityFromProperties2(definition.identity, properties2, raw.type);
|
|
1603
|
-
const key = entityKey(raw.type, identity);
|
|
1604
|
-
const current = entities.get(key);
|
|
1605
|
-
const next = {
|
|
1606
|
-
key,
|
|
1607
|
-
type: raw.type,
|
|
1608
|
-
identity,
|
|
1609
|
-
properties: properties2,
|
|
1610
|
-
sourceChunkIds: [extraction.chunkId]
|
|
1611
|
-
};
|
|
1612
|
-
if (current === void 0) {
|
|
1613
|
-
entities.set(key, next);
|
|
1614
|
-
refs.set(raw.ref, next);
|
|
1615
|
-
} else {
|
|
1616
|
-
assertSameProperties(
|
|
1617
|
-
current.properties,
|
|
1618
|
-
properties2,
|
|
1619
|
-
key,
|
|
1620
|
-
current.sourceChunkIds,
|
|
1621
|
-
extraction.chunkId
|
|
1622
|
-
);
|
|
1623
|
-
const merged = {
|
|
1624
|
-
...current,
|
|
1625
|
-
sourceChunkIds: [...current.sourceChunkIds, extraction.chunkId]
|
|
1626
|
-
};
|
|
1627
|
-
entities.set(key, merged);
|
|
1628
|
-
refs.set(raw.ref, merged);
|
|
1629
|
-
}
|
|
1630
|
-
mentions.set(`${extraction.chunkId}\0${key}`, {
|
|
1631
|
-
chunkId: extraction.chunkId,
|
|
1632
|
-
entityKey: key
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
for (const raw of extraction.output.relationships) {
|
|
1636
|
-
const definition = schema.relationships[raw.type];
|
|
1637
|
-
if (definition === void 0)
|
|
1638
|
-
throw new TypeError(`Unknown graph relationship type: ${raw.type}`);
|
|
1639
|
-
const from = refs.get(raw.from);
|
|
1640
|
-
const to = refs.get(raw.to);
|
|
1641
|
-
if (from === void 0 || to === void 0) {
|
|
1642
|
-
throw new TypeError(
|
|
1643
|
-
`Relationship ${raw.type} in chunk ${extraction.chunkId} references an unknown entity ref.`
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
if (from.type !== definition.from || to.type !== definition.to) {
|
|
1647
|
-
throw new TypeError(
|
|
1648
|
-
`Relationship ${raw.type} has invalid endpoint types ${from.type} -> ${to.type}.`
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
const properties2 = parseNeo4jProperties(
|
|
1652
|
-
definition.properties.parse(raw.properties),
|
|
1653
|
-
`relationship ${raw.type}`
|
|
1654
|
-
);
|
|
1655
|
-
const identity = identityFromProperties2(definition.identity ?? [], properties2, raw.type);
|
|
1656
|
-
const key = relationshipKey(raw.type, from.key, to.key, identity);
|
|
1657
|
-
const current = relationships.get(key);
|
|
1658
|
-
const next = {
|
|
1659
|
-
key,
|
|
1660
|
-
type: raw.type,
|
|
1661
|
-
from: from.key,
|
|
1662
|
-
to: to.key,
|
|
1663
|
-
properties: properties2,
|
|
1664
|
-
sourceChunkIds: [extraction.chunkId]
|
|
1665
|
-
};
|
|
1666
|
-
if (current === void 0) relationships.set(key, next);
|
|
1667
|
-
else {
|
|
1668
|
-
assertSameProperties(
|
|
1669
|
-
current.properties,
|
|
1670
|
-
properties2,
|
|
1671
|
-
key,
|
|
1672
|
-
current.sourceChunkIds,
|
|
1673
|
-
extraction.chunkId
|
|
1674
|
-
);
|
|
1675
|
-
relationships.set(key, {
|
|
1676
|
-
...current,
|
|
1677
|
-
sourceChunkIds: [...current.sourceChunkIds, extraction.chunkId]
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return {
|
|
1683
|
-
entities: [...entities.values()],
|
|
1684
|
-
relationships: [...relationships.values()],
|
|
1685
|
-
mentions: [...mentions.values()]
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
function identityFromProperties2(keys, properties2, type) {
|
|
1689
|
-
const identity = {};
|
|
1690
|
-
for (const key of keys) {
|
|
1691
|
-
const value = properties2[key];
|
|
1692
|
-
if (value === void 0 || !(typeof value === "string" || typeof value === "number" || typeof value === "boolean")) {
|
|
1693
|
-
throw new TypeError(`Identity property ${key} for ${type} must be a primitive value.`);
|
|
1694
|
-
}
|
|
1695
|
-
identity[key] = value;
|
|
1696
|
-
}
|
|
1697
|
-
return identity;
|
|
1698
|
-
}
|
|
1699
|
-
function entityKey(type, identity) {
|
|
1700
|
-
return `${type}:${stableObject2(identity)}`;
|
|
1701
|
-
}
|
|
1702
|
-
function relationshipKey(type, from, to, identity) {
|
|
1703
|
-
return `${type}:${from}->${to}:${stableObject2(identity)}`;
|
|
1704
|
-
}
|
|
1705
|
-
function stableObject2(value) {
|
|
1706
|
-
return JSON.stringify(
|
|
1707
|
-
Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)))
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
function assertSameProperties(left, right, key, sourceChunkIds, nextChunkId) {
|
|
1711
|
-
if (stableObject2(left) !== stableObject2(right)) {
|
|
1712
|
-
throw new GraphFactConflictError(key, [...sourceChunkIds, nextChunkId]);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
var GraphFactConflictError = class extends Error {
|
|
1716
|
-
constructor(factKey, sourceChunkIds) {
|
|
1717
|
-
super(
|
|
1718
|
-
`Conflicting graph fact ${factKey} was extracted from chunks ${sourceChunkIds.join(", ")}.`
|
|
1719
|
-
);
|
|
1720
|
-
this.factKey = factKey;
|
|
1721
|
-
this.sourceChunkIds = sourceChunkIds;
|
|
1722
|
-
this.name = "GraphFactConflictError";
|
|
1723
|
-
}
|
|
1724
|
-
factKey;
|
|
1725
|
-
sourceChunkIds;
|
|
1726
|
-
};
|
|
1727
|
-
function assertConcurrency(value) {
|
|
1728
|
-
if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) {
|
|
1729
|
-
throw new RangeError("Graph extraction concurrency must be a positive safe integer.");
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
async function mapConcurrent(values, concurrency, run) {
|
|
1733
|
-
const output = Array(values.length);
|
|
1734
|
-
let cursor = 0;
|
|
1735
|
-
await Promise.all(
|
|
1736
|
-
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
1737
|
-
for (; ; ) {
|
|
1738
|
-
const index = cursor;
|
|
1739
|
-
cursor += 1;
|
|
1740
|
-
if (index >= values.length) return;
|
|
1741
|
-
output[index] = await run(values[index]);
|
|
1742
|
-
}
|
|
1743
|
-
})
|
|
1744
|
-
);
|
|
1745
|
-
return output;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
// src/retrieve.ts
|
|
1749
|
-
import { embedText } from "@anvia/core/embeddings";
|
|
1750
|
-
import { int, isInt as isInt3 } from "neo4j-driver";
|
|
1751
|
-
async function retrieveGraphContext(options) {
|
|
1752
|
-
if (!(options.graph instanceof Neo4jKnowledgeGraphBase)) {
|
|
1753
|
-
throw new TypeError("retrieveGraphContext requires a Neo4j knowledge graph registration.");
|
|
1754
|
-
}
|
|
1755
|
-
if (typeof options.query !== "string" || options.query.trim().length === 0) {
|
|
1756
|
-
throw new TypeError("Graph retrieval query must be a non-empty string.");
|
|
1757
|
-
}
|
|
1758
|
-
validateSearch(options.search);
|
|
1759
|
-
validateTraversal(options.graph.schema, options.traversal);
|
|
1760
|
-
validateEvidence(options.graph.evidenceCapability, options.evidence);
|
|
1761
|
-
throwIfAborted(options.abortSignal);
|
|
1762
|
-
const graph = options.graph;
|
|
1763
|
-
const selectedSeeds = options.search.seeds.map((name) => ({ name, seed: graph.seed(name) }));
|
|
1764
|
-
const expectedDimensions = selectedSeeds[0]?.seed.vectorIndex.dimensions;
|
|
1765
|
-
if (expectedDimensions === void 0 || selectedSeeds.some((item) => item.seed.vectorIndex.dimensions !== expectedDimensions)) {
|
|
1766
|
-
throw new TypeError("Selected Neo4j graph seeds must use the same vector dimensions.");
|
|
1767
|
-
}
|
|
1768
|
-
if (options.model.dimensions !== void 0 && options.model.dimensions !== expectedDimensions) {
|
|
1769
|
-
throw new TypeError(
|
|
1770
|
-
`Embedding model dimensions ${options.model.dimensions} do not match Neo4j graph dimensions ${expectedDimensions}.`
|
|
1771
|
-
);
|
|
1772
|
-
}
|
|
1773
|
-
if (options.search.type === "hybrid") {
|
|
1774
|
-
for (const { name, seed } of selectedSeeds) {
|
|
1775
|
-
if (seed.fulltextIndex === void 0) {
|
|
1776
|
-
throw new TypeError(`Neo4j graph seed ${name} does not declare a full-text index.`);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
const { embedding } = await embedText({
|
|
1781
|
-
model: options.model,
|
|
1782
|
-
text: options.query,
|
|
1783
|
-
retries: options.retries,
|
|
1784
|
-
abortSignal: options.abortSignal
|
|
1785
|
-
});
|
|
1786
|
-
if (embedding.vector.length !== expectedDimensions) {
|
|
1787
|
-
throw new TypeError(
|
|
1788
|
-
`Query embedding dimensions ${embedding.vector.length} do not match Neo4j graph dimensions ${expectedDimensions}.`
|
|
1789
|
-
);
|
|
1790
|
-
}
|
|
1791
|
-
const resultLists = [];
|
|
1792
|
-
for (const { seed } of selectedSeeds) {
|
|
1793
|
-
const limit = options.search.type === "hybrid" ? options.search.candidatesPerSeed : options.search.topK;
|
|
1794
|
-
resultLists.push(
|
|
1795
|
-
await vectorSearch(
|
|
1796
|
-
graph,
|
|
1797
|
-
seed.labels,
|
|
1798
|
-
seed.vectorIndex.name,
|
|
1799
|
-
embedding.vector,
|
|
1800
|
-
limit,
|
|
1801
|
-
seed.entryRelationshipType,
|
|
1802
|
-
options.abortSignal
|
|
1803
|
-
)
|
|
1804
|
-
);
|
|
1805
|
-
if (options.search.type === "hybrid") {
|
|
1806
|
-
const fulltextIndex2 = seed.fulltextIndex;
|
|
1807
|
-
if (fulltextIndex2 === void 0) {
|
|
1808
|
-
throw new TypeError("Hybrid Neo4j graph retrieval requires a full-text index.");
|
|
1809
|
-
}
|
|
1810
|
-
resultLists.push(
|
|
1811
|
-
await fulltextSearch(
|
|
1812
|
-
graph,
|
|
1813
|
-
fulltextIndex2.name,
|
|
1814
|
-
options.query,
|
|
1815
|
-
limit,
|
|
1816
|
-
seed.entryRelationshipType,
|
|
1817
|
-
options.abortSignal
|
|
1818
|
-
)
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
const hits = options.search.type === "hybrid" ? fuseResults(resultLists, options.search.topK, options.search.rrfK) : vectorResults(resultLists[0] ?? [], options.search.topK, options.search.minScore);
|
|
1823
|
-
throwIfAborted(options.abortSignal);
|
|
1824
|
-
const seeds = hits.map((hit) => seedFromHit(hit, options.graph.schema));
|
|
1825
|
-
const traversed = hits.length === 0 ? { nodes: [], relationships: [] } : await traverse(graph, hits, options.traversal, options.abortSignal);
|
|
1826
|
-
const context = { seeds, ...traversed };
|
|
1827
|
-
const evidence = await hydrateEvidence(graph, context, options.evidence, options.abortSignal);
|
|
1828
|
-
return { ...context, evidence };
|
|
1829
|
-
}
|
|
1830
|
-
async function vectorSearch(graph, labels, indexName, vector, limit, entryRelationshipType, abortSignal) {
|
|
1831
|
-
const labelExpression = labels.map(quoteIdentifier).join("|");
|
|
1832
|
-
const result = await graph.query(
|
|
1833
|
-
`CYPHER 25
|
|
1834
|
-
MATCH (seed:${labelExpression})
|
|
1835
|
-
SEARCH seed IN (VECTOR INDEX ${quoteIdentifier(indexName)} FOR $vector LIMIT $limit) SCORE AS score
|
|
1836
|
-
RETURN elementId(seed) AS elementId, labels(seed) AS labels, properties(seed) AS properties, score`,
|
|
1837
|
-
{ vector, limit: int(limit) },
|
|
1838
|
-
abortSignal
|
|
1839
|
-
);
|
|
1840
|
-
return result.records.map((record) => ({ ...parseSearchHit(record), entryRelationshipType }));
|
|
1841
|
-
}
|
|
1842
|
-
async function fulltextSearch(graph, indexName, query, limit, entryRelationshipType, abortSignal) {
|
|
1843
|
-
const result = await graph.query(
|
|
1844
|
-
`CALL db.index.fulltext.queryNodes($indexName, $query, {limit: $limit}) YIELD node AS seed, score
|
|
1845
|
-
RETURN elementId(seed) AS elementId, labels(seed) AS labels, properties(seed) AS properties, score`,
|
|
1846
|
-
{ indexName, query: escapeLucene(query), limit: int(limit) },
|
|
1847
|
-
abortSignal
|
|
1848
|
-
);
|
|
1849
|
-
return result.records.map((record) => ({ ...parseSearchHit(record), entryRelationshipType }));
|
|
1850
|
-
}
|
|
1851
|
-
function parseSearchHit(record) {
|
|
1852
|
-
const elementId = record.get("elementId");
|
|
1853
|
-
const labels = record.get("labels");
|
|
1854
|
-
const properties2 = record.get("properties");
|
|
1855
|
-
const score = record.get("score");
|
|
1856
|
-
if (typeof elementId !== "string" || !Array.isArray(labels) || !labels.every((label) => typeof label === "string") || typeof properties2 !== "object" || properties2 === null || typeof score !== "number" || !Number.isFinite(score)) {
|
|
1857
|
-
throw new TypeError("Neo4j search returned a malformed result.");
|
|
1858
|
-
}
|
|
1859
|
-
return { elementId, labels, properties: properties2, score };
|
|
1860
|
-
}
|
|
1861
|
-
function seedFromHit(hit, schema) {
|
|
1862
|
-
const application = applicationProperties2(hit.properties, "Neo4j search seed");
|
|
1863
|
-
const key = stringProperty(hit.properties, "__anvia_key") ?? stringProperty(hit.properties, "__anvia_id") ?? hit.elementId;
|
|
1864
|
-
const type = hit.labels.find((label) => label in schema.nodes) ?? hit.labels[0] ?? "Node";
|
|
1865
|
-
return {
|
|
1866
|
-
key,
|
|
1867
|
-
type,
|
|
1868
|
-
score: hit.score,
|
|
1869
|
-
properties: application,
|
|
1870
|
-
sourceChunkIds: hit.entryRelationshipType === "ANVIA_MENTIONS" ? [key] : stringArrayProperty(hit.properties, "__anvia_source_chunk_ids")
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
async function hydrateEvidence(graph, context, options, abortSignal) {
|
|
1874
|
-
if (options.type === "none") return [];
|
|
1875
|
-
if (!(graph instanceof ManagedNeo4jKnowledgeGraph)) {
|
|
1876
|
-
throw new TypeError("Chunk evidence requires a managed Neo4j knowledge graph.");
|
|
1877
|
-
}
|
|
1878
|
-
const chunkIds = [];
|
|
1879
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1880
|
-
for (const sourceChunkIds of [
|
|
1881
|
-
...context.seeds.map((seed) => seed.sourceChunkIds),
|
|
1882
|
-
...context.nodes.map((node) => node.sourceChunkIds),
|
|
1883
|
-
...context.relationships.map((relationship) => relationship.sourceChunkIds)
|
|
1884
|
-
]) {
|
|
1885
|
-
for (const chunkId of sourceChunkIds) {
|
|
1886
|
-
if (seen.has(chunkId)) continue;
|
|
1887
|
-
seen.add(chunkId);
|
|
1888
|
-
chunkIds.push(chunkId);
|
|
1889
|
-
if (chunkIds.length === options.maxChunks) break;
|
|
1890
|
-
}
|
|
1891
|
-
if (chunkIds.length === options.maxChunks) break;
|
|
1892
|
-
}
|
|
1893
|
-
if (chunkIds.length === 0) return [];
|
|
1894
|
-
throwIfAborted(abortSignal);
|
|
1895
|
-
const result = await graph.query(
|
|
1896
|
-
`MATCH (d:${quoteIdentifier(graph.resources.labels.document)})-[:${quoteIdentifier("ANVIA_HAS_CHUNK")}]->(c:${quoteIdentifier(graph.resources.labels.chunk)})
|
|
1897
|
-
WHERE c.${quoteIdentifier("__anvia_id")} IN $chunkIds
|
|
1898
|
-
RETURN c.${quoteIdentifier("__anvia_id")} AS chunkId,
|
|
1899
|
-
d.${quoteIdentifier("__anvia_id")} AS documentId,
|
|
1900
|
-
c.${quoteIdentifier("__anvia_index")} AS index,
|
|
1901
|
-
c.${quoteIdentifier("__anvia_text")} AS text,
|
|
1902
|
-
properties(c) AS properties`,
|
|
1903
|
-
{ chunkIds },
|
|
1904
|
-
abortSignal
|
|
1905
|
-
);
|
|
1906
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1907
|
-
for (const record of result.records) {
|
|
1908
|
-
const evidence = evidenceFromRecord(record);
|
|
1909
|
-
if (byId.has(evidence.chunkId)) {
|
|
1910
|
-
throw new TypeError(`Neo4j evidence returned duplicate chunk ${evidence.chunkId}.`);
|
|
1911
|
-
}
|
|
1912
|
-
byId.set(evidence.chunkId, evidence);
|
|
1913
|
-
}
|
|
1914
|
-
return chunkIds.map((chunkId) => {
|
|
1915
|
-
const evidence = byId.get(chunkId);
|
|
1916
|
-
if (evidence === void 0) {
|
|
1917
|
-
throw new Error(`Neo4j graph evidence references missing chunk ${chunkId}.`);
|
|
1918
|
-
}
|
|
1919
|
-
return evidence;
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
function evidenceFromRecord(record) {
|
|
1923
|
-
const chunkId = stringValue(record.get("chunkId"), "Neo4j evidence chunk id");
|
|
1924
|
-
const documentId = stringValue(record.get("documentId"), "Neo4j evidence document id");
|
|
1925
|
-
const index = safeIntegerValue(record.get("index"), "Neo4j evidence chunk index");
|
|
1926
|
-
const text = stringValue(record.get("text"), "Neo4j evidence text");
|
|
1927
|
-
const raw = record.get("properties");
|
|
1928
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1929
|
-
throw new TypeError("Neo4j evidence properties must be an object.");
|
|
1930
|
-
}
|
|
1931
|
-
return {
|
|
1932
|
-
chunkId,
|
|
1933
|
-
documentId,
|
|
1934
|
-
index,
|
|
1935
|
-
text,
|
|
1936
|
-
metadata: applicationProperties2(raw, "Neo4j evidence metadata")
|
|
1937
|
-
};
|
|
1938
|
-
}
|
|
1939
|
-
function safeIntegerValue(value, label) {
|
|
1940
|
-
const number = isInt3(value) ? value.inSafeRange() ? value.toNumber() : void 0 : value;
|
|
1941
|
-
if (typeof number !== "number" || !Number.isSafeInteger(number)) {
|
|
1942
|
-
throw new TypeError(`${label} must be a safe integer.`);
|
|
1943
|
-
}
|
|
1944
|
-
return number;
|
|
1945
|
-
}
|
|
1946
|
-
function vectorResults(results, topK, minScore) {
|
|
1947
|
-
return results.filter((item) => minScore === void 0 || item.score >= minScore).slice(0, topK);
|
|
1948
|
-
}
|
|
1949
|
-
async function traverse(graph, seeds, options, abortSignal) {
|
|
1950
|
-
const traversalSeedIds = new Set(
|
|
1951
|
-
seeds.filter((seed) => seed.entryRelationshipType === void 0).map((seed) => seed.elementId)
|
|
1952
|
-
);
|
|
1953
|
-
const entryGroups = groupBy2(
|
|
1954
|
-
seeds.filter(
|
|
1955
|
-
(seed) => seed.entryRelationshipType !== void 0
|
|
1956
|
-
),
|
|
1957
|
-
(seed) => seed.entryRelationshipType
|
|
1958
|
-
);
|
|
1959
|
-
for (const [relationshipType, group] of entryGroups) {
|
|
1960
|
-
const entries = await graph.query(
|
|
1961
|
-
`MATCH (seed) WHERE elementId(seed) IN $seedIds
|
|
1962
|
-
MATCH (seed)-[:${quoteIdentifier(relationshipType)}]->(entry)
|
|
1963
|
-
RETURN DISTINCT elementId(entry) AS elementId`,
|
|
1964
|
-
{ seedIds: group.map((seed) => seed.elementId) },
|
|
1965
|
-
abortSignal
|
|
1966
|
-
);
|
|
1967
|
-
for (const record of entries.records) {
|
|
1968
|
-
traversalSeedIds.add(stringValue(record.get("elementId"), "Neo4j entry elementId"));
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
if (traversalSeedIds.size === 0) return { nodes: [], relationships: [] };
|
|
1972
|
-
const limitedSeedIds = [...traversalSeedIds].slice(0, options.maxNodes);
|
|
1973
|
-
const pattern = options.direction === "outgoing" ? `(seed)-[*1..${options.maxDepth}]->(node)` : options.direction === "incoming" ? `(seed)<-[*1..${options.maxDepth}]-(node)` : `(seed)-[*1..${options.maxDepth}]-(node)`;
|
|
1974
|
-
const pathLimit = Math.max(options.maxNodes, options.maxRelationships);
|
|
1975
|
-
const seedNodes = await graph.query(
|
|
1976
|
-
`MATCH (node) WHERE elementId(node) IN $seedIds
|
|
1977
|
-
RETURN {elementId: elementId(node), labels: labels(node), properties: properties(node)} AS node`,
|
|
1978
|
-
{ seedIds: limitedSeedIds },
|
|
1979
|
-
abortSignal
|
|
1980
|
-
);
|
|
1981
|
-
const result = await graph.query(
|
|
1982
|
-
`MATCH (seed) WHERE elementId(seed) IN $seedIds
|
|
1983
|
-
MATCH path = ${pattern}
|
|
1984
|
-
WHERE all(rel IN relationships(path) WHERE type(rel) IN $relationshipTypes)
|
|
1985
|
-
RETURN [item IN nodes(path) | {elementId: elementId(item), labels: labels(item), properties: properties(item)}] AS nodes,
|
|
1986
|
-
[item IN relationships(path) | {elementId: elementId(item), type: type(item), properties: properties(item), from: elementId(startNode(item)), to: elementId(endNode(item))}] AS relationships
|
|
1987
|
-
LIMIT $pathLimit`,
|
|
1988
|
-
{
|
|
1989
|
-
seedIds: limitedSeedIds,
|
|
1990
|
-
relationshipTypes: options.relationships,
|
|
1991
|
-
pathLimit: int(pathLimit)
|
|
1992
|
-
},
|
|
1993
|
-
abortSignal
|
|
1994
|
-
);
|
|
1995
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
1996
|
-
const nodeKeysByElementId = /* @__PURE__ */ new Map();
|
|
1997
|
-
const relationships = /* @__PURE__ */ new Map();
|
|
1998
|
-
const seedNodesByElementId = /* @__PURE__ */ new Map();
|
|
1999
|
-
for (const record of seedNodes.records) {
|
|
2000
|
-
const value = record.get("node");
|
|
2001
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2002
|
-
throw new TypeError("Neo4j traversal returned a malformed seed node.");
|
|
2003
|
-
}
|
|
2004
|
-
const map = value;
|
|
2005
|
-
seedNodesByElementId.set(stringValue(map.elementId, "Neo4j seed node elementId"), map);
|
|
2006
|
-
}
|
|
2007
|
-
const addNode = (value) => {
|
|
2008
|
-
const parsed = contextNode(value, graph.schema);
|
|
2009
|
-
const existing = nodes.get(parsed.node.key);
|
|
2010
|
-
if (existing === void 0 && nodes.size >= options.maxNodes) return;
|
|
2011
|
-
if (existing === void 0) nodes.set(parsed.node.key, parsed.node);
|
|
2012
|
-
nodeKeysByElementId.set(parsed.elementId, parsed.node.key);
|
|
2013
|
-
};
|
|
2014
|
-
for (const elementId of limitedSeedIds) {
|
|
2015
|
-
const value = seedNodesByElementId.get(elementId);
|
|
2016
|
-
if (value !== void 0) addNode(value);
|
|
2017
|
-
}
|
|
2018
|
-
for (const record of result.records) {
|
|
2019
|
-
for (const value of listOfMaps(record.get("nodes"), "Neo4j traversal nodes")) {
|
|
2020
|
-
addNode(value);
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
for (const record of result.records) {
|
|
2024
|
-
for (const value of listOfMaps(record.get("relationships"), "Neo4j traversal relationships")) {
|
|
2025
|
-
const relationship = contextRelationship(value, nodeKeysByElementId);
|
|
2026
|
-
if (relationship !== void 0 && !relationships.has(relationship.key) && relationships.size < options.maxRelationships) {
|
|
2027
|
-
relationships.set(relationship.key, relationship);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
return { nodes: [...nodes.values()], relationships: [...relationships.values()] };
|
|
2032
|
-
}
|
|
2033
|
-
function contextNode(value, schema) {
|
|
2034
|
-
const labels = value.labels;
|
|
2035
|
-
const raw = value.properties;
|
|
2036
|
-
if (!Array.isArray(labels) || !labels.every((label) => typeof label === "string")) {
|
|
2037
|
-
throw new TypeError("Neo4j traversal returned malformed node labels.");
|
|
2038
|
-
}
|
|
2039
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2040
|
-
throw new TypeError("Neo4j traversal returned malformed node properties.");
|
|
2041
|
-
}
|
|
2042
|
-
const properties2 = applicationProperties2(raw, "Neo4j graph node");
|
|
2043
|
-
const type = labels.find((label) => label in schema.nodes) ?? labels[0] ?? "Node";
|
|
2044
|
-
const definition = schema.nodes[type];
|
|
2045
|
-
const identity = {};
|
|
2046
|
-
if (definition !== void 0) {
|
|
2047
|
-
for (const property of definition.identity) {
|
|
2048
|
-
const item = properties2[property];
|
|
2049
|
-
if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
|
|
2050
|
-
identity[property] = item;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
const elementId = stringValue(value.elementId, "Neo4j node elementId");
|
|
2055
|
-
return {
|
|
2056
|
-
elementId,
|
|
2057
|
-
node: {
|
|
2058
|
-
key: stringProperty(raw, "__anvia_key") ?? elementId,
|
|
2059
|
-
type,
|
|
2060
|
-
identity,
|
|
2061
|
-
properties: properties2,
|
|
2062
|
-
sourceChunkIds: stringArrayProperty(
|
|
2063
|
-
raw,
|
|
2064
|
-
"__anvia_source_chunk_ids"
|
|
2065
|
-
)
|
|
2066
|
-
}
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
function contextRelationship(value, nodeKeysByElementId) {
|
|
2070
|
-
const raw = value.properties;
|
|
2071
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
2072
|
-
throw new TypeError("Neo4j traversal returned malformed relationship properties.");
|
|
2073
|
-
}
|
|
2074
|
-
const elementId = stringValue(value.elementId, "Neo4j relationship elementId");
|
|
2075
|
-
const fromElementId = stringValue(value.from, "Neo4j relationship source");
|
|
2076
|
-
const toElementId = stringValue(value.to, "Neo4j relationship target");
|
|
2077
|
-
const from = nodeKeysByElementId.get(fromElementId);
|
|
2078
|
-
const to = nodeKeysByElementId.get(toElementId);
|
|
2079
|
-
if (from === void 0 || to === void 0) return void 0;
|
|
2080
|
-
return {
|
|
2081
|
-
key: stringProperty(raw, "__anvia_key") ?? elementId,
|
|
2082
|
-
type: stringValue(value.type, "Neo4j relationship type"),
|
|
2083
|
-
from,
|
|
2084
|
-
to,
|
|
2085
|
-
properties: applicationProperties2(raw, "Neo4j graph relationship"),
|
|
2086
|
-
sourceChunkIds: stringArrayProperty(raw, "__anvia_source_chunk_ids")
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
function fuseResults(lists, topK, rrfK) {
|
|
2090
|
-
const fused = /* @__PURE__ */ new Map();
|
|
2091
|
-
for (const list of lists) {
|
|
2092
|
-
list.forEach((item, index) => {
|
|
2093
|
-
const score = 1 / (rrfK + index + 1);
|
|
2094
|
-
const current = fused.get(item.elementId);
|
|
2095
|
-
fused.set(item.elementId, { ...item, score: (current?.score ?? 0) + score });
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
return [...fused.values()].sort((left, right) => right.score - left.score).slice(0, topK);
|
|
2099
|
-
}
|
|
2100
|
-
function validateSearch(search) {
|
|
2101
|
-
positiveInteger(search.topK, "Neo4j graph search topK");
|
|
2102
|
-
if (search.seeds.length === 0 || new Set(search.seeds).size !== search.seeds.length) {
|
|
2103
|
-
throw new TypeError("Neo4j graph search requires unique seed names.");
|
|
2104
|
-
}
|
|
2105
|
-
if (search.type === "vector") {
|
|
2106
|
-
if (search.seeds.length !== 1)
|
|
2107
|
-
throw new TypeError(
|
|
2108
|
-
"Vector graph search accepts exactly one seed; use hybrid search to fuse multiple seeds."
|
|
2109
|
-
);
|
|
2110
|
-
finiteScore(search.minScore, "Neo4j graph search minScore");
|
|
2111
|
-
} else {
|
|
2112
|
-
positiveInteger(search.candidatesPerSeed, "Neo4j hybrid candidatesPerSeed");
|
|
2113
|
-
positiveInteger(search.rrfK, "Neo4j hybrid rrfK");
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
function validateEvidence(capability, evidence) {
|
|
2117
|
-
if (typeof evidence !== "object" || evidence === null) {
|
|
2118
|
-
throw new TypeError("Neo4j graph retrieval requires an explicit evidence mode.");
|
|
2119
|
-
}
|
|
2120
|
-
if (evidence.type === "none") return;
|
|
2121
|
-
if (evidence.type !== "chunks") {
|
|
2122
|
-
throw new TypeError("Unknown Neo4j graph evidence mode.");
|
|
2123
|
-
}
|
|
2124
|
-
positiveInteger(evidence.maxChunks, "Neo4j graph evidence maxChunks");
|
|
2125
|
-
if (capability !== "chunks") {
|
|
2126
|
-
throw new TypeError("Chunk evidence requires a managed Neo4j knowledge graph.");
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
function validateTraversal(schema, traversal) {
|
|
2130
|
-
if (traversal.direction !== "outgoing" && traversal.direction !== "incoming" && traversal.direction !== "both") {
|
|
2131
|
-
throw new TypeError("Neo4j traversal direction must be outgoing, incoming, or both.");
|
|
2132
|
-
}
|
|
2133
|
-
positiveInteger(traversal.maxDepth, "Neo4j traversal maxDepth", 8);
|
|
2134
|
-
positiveInteger(traversal.maxNodes, "Neo4j traversal maxNodes");
|
|
2135
|
-
positiveInteger(traversal.maxRelationships, "Neo4j traversal maxRelationships");
|
|
2136
|
-
if (traversal.relationships.length === 0 || new Set(traversal.relationships).size !== traversal.relationships.length) {
|
|
2137
|
-
throw new TypeError("Neo4j traversal requires unique relationship types.");
|
|
2138
|
-
}
|
|
2139
|
-
for (const type of traversal.relationships) {
|
|
2140
|
-
if (!(type in schema.relationships))
|
|
2141
|
-
throw new TypeError(`Neo4j traversal references unknown relationship type ${type}.`);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
function applicationProperties2(value, label) {
|
|
2145
|
-
return strictProperties(
|
|
2146
|
-
Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("__anvia_"))),
|
|
2147
|
-
label
|
|
2148
|
-
);
|
|
2149
|
-
}
|
|
2150
|
-
function stringProperty(value, property) {
|
|
2151
|
-
const item = value[property];
|
|
2152
|
-
if (item === void 0) return void 0;
|
|
2153
|
-
return stringValue(item, `Neo4j property ${property}`);
|
|
2154
|
-
}
|
|
2155
|
-
function stringArrayProperty(value, property) {
|
|
2156
|
-
const item = value[property];
|
|
2157
|
-
if (item === void 0) return [];
|
|
2158
|
-
if (!Array.isArray(item) || !item.every((entry) => typeof entry === "string")) {
|
|
2159
|
-
throw new TypeError(`Neo4j property ${property} must be a string array.`);
|
|
2160
|
-
}
|
|
2161
|
-
return [...item];
|
|
2162
|
-
}
|
|
2163
|
-
function stringValue(value, label) {
|
|
2164
|
-
if (typeof value !== "string") throw new TypeError(`${label} must be a string.`);
|
|
2165
|
-
return value;
|
|
2166
|
-
}
|
|
2167
|
-
function listOfMaps(value, label) {
|
|
2168
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "object" && item !== null && !Array.isArray(item))) {
|
|
2169
|
-
throw new TypeError(`${label} must be an array of objects.`);
|
|
2170
|
-
}
|
|
2171
|
-
return value;
|
|
2172
|
-
}
|
|
2173
|
-
function escapeLucene(value) {
|
|
2174
|
-
const specialCharacters = new Set('+-&|!(){}[]^"~*?:\\/');
|
|
2175
|
-
let escaped = "";
|
|
2176
|
-
for (const character of value) {
|
|
2177
|
-
escaped += specialCharacters.has(character) ? `\\${character}` : character;
|
|
2178
|
-
}
|
|
2179
|
-
return escaped;
|
|
2180
|
-
}
|
|
2181
|
-
function groupBy2(values, keyFor) {
|
|
2182
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2183
|
-
for (const value of values) {
|
|
2184
|
-
const key = keyFor(value);
|
|
2185
|
-
const group = groups.get(key);
|
|
2186
|
-
if (group === void 0) groups.set(key, [value]);
|
|
2187
|
-
else group.push(value);
|
|
2188
|
-
}
|
|
2189
|
-
return groups;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
// src/tool.ts
|
|
2193
|
-
import { createTool } from "@anvia/core/tool";
|
|
2194
|
-
import { z as z3 } from "zod";
|
|
2195
|
-
var primitive = z3.union([z3.string(), z3.number().finite(), z3.boolean()]);
|
|
2196
|
-
var propertyValue = z3.union([
|
|
2197
|
-
primitive,
|
|
2198
|
-
z3.array(z3.string()),
|
|
2199
|
-
z3.array(z3.number().finite()),
|
|
2200
|
-
z3.array(z3.boolean())
|
|
2201
|
-
]);
|
|
2202
|
-
var properties = z3.record(z3.string(), propertyValue);
|
|
2203
|
-
var contextSchema = z3.object({
|
|
2204
|
-
seeds: z3.array(
|
|
2205
|
-
z3.object({
|
|
2206
|
-
key: z3.string(),
|
|
2207
|
-
type: z3.string(),
|
|
2208
|
-
score: z3.number().finite(),
|
|
2209
|
-
properties,
|
|
2210
|
-
sourceChunkIds: z3.array(z3.string())
|
|
2211
|
-
})
|
|
2212
|
-
),
|
|
2213
|
-
nodes: z3.array(
|
|
2214
|
-
z3.object({
|
|
2215
|
-
key: z3.string(),
|
|
2216
|
-
type: z3.string(),
|
|
2217
|
-
identity: z3.record(z3.string(), primitive),
|
|
2218
|
-
properties,
|
|
2219
|
-
sourceChunkIds: z3.array(z3.string())
|
|
2220
|
-
})
|
|
2221
|
-
),
|
|
2222
|
-
relationships: z3.array(
|
|
2223
|
-
z3.object({
|
|
2224
|
-
key: z3.string(),
|
|
2225
|
-
type: z3.string(),
|
|
2226
|
-
from: z3.string(),
|
|
2227
|
-
to: z3.string(),
|
|
2228
|
-
properties,
|
|
2229
|
-
sourceChunkIds: z3.array(z3.string())
|
|
2230
|
-
})
|
|
2231
|
-
),
|
|
2232
|
-
evidence: z3.array(
|
|
2233
|
-
z3.object({
|
|
2234
|
-
chunkId: z3.string(),
|
|
2235
|
-
documentId: z3.string(),
|
|
2236
|
-
index: z3.number().int(),
|
|
2237
|
-
text: z3.string(),
|
|
2238
|
-
metadata: properties
|
|
2239
|
-
})
|
|
2240
|
-
)
|
|
2241
|
-
});
|
|
2242
|
-
function createNeo4jGraphSearchTool(options) {
|
|
2243
|
-
return createTool({
|
|
2244
|
-
name: options.name,
|
|
2245
|
-
description: options.description,
|
|
2246
|
-
inputSchema: z3.object({
|
|
2247
|
-
query: z3.string().min(1).describe("The question or concept to search for in the graph.")
|
|
2248
|
-
}),
|
|
2249
|
-
outputSchema: contextSchema,
|
|
2250
|
-
execute: async ({ query }, context) => await retrieveGraphContext({
|
|
2251
|
-
graph: options.graph,
|
|
2252
|
-
model: options.model,
|
|
2253
|
-
query,
|
|
2254
|
-
search: options.search,
|
|
2255
|
-
traversal: options.traversal,
|
|
2256
|
-
evidence: options.evidence,
|
|
2257
|
-
retries: options.retries,
|
|
2258
|
-
abortSignal: context.abortSignal
|
|
2259
|
-
})
|
|
2260
|
-
});
|
|
67
|
+
return extractSharedGraphFacts(options);
|
|
2261
68
|
}
|
|
2262
69
|
export {
|
|
2263
70
|
GraphFactConflictError,
|
|
2264
71
|
ManagedNeo4jKnowledgeGraph,
|
|
2265
72
|
Neo4jClient,
|
|
2266
73
|
Neo4jKnowledgeGraph,
|
|
2267
|
-
createNeo4jGraphSearchTool,
|
|
2268
74
|
defineNeo4jGraphSchema,
|
|
75
|
+
exploreGraph,
|
|
2269
76
|
extractGraphFacts,
|
|
2270
77
|
retrieveGraphContext
|
|
2271
78
|
};
|