@m6d/cortex-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/src/adapters/database.d.ts +27 -0
- package/dist/src/adapters/minio.d.ts +10 -0
- package/dist/src/adapters/mssql.d.ts +3 -0
- package/dist/src/adapters/storage.d.ts +6 -0
- package/dist/src/ai/fetch.d.ts +2 -0
- package/dist/src/ai/helpers.d.ts +5 -0
- package/dist/src/ai/index.d.ts +4 -0
- package/dist/src/ai/interceptors/resolve-captured-files.d.ts +11 -0
- package/dist/src/ai/prompt.d.ts +4 -0
- package/dist/src/ai/tools/call-endpoint.tool.d.ts +7 -0
- package/dist/src/ai/tools/capture-files.tool.d.ts +6 -0
- package/dist/src/ai/tools/execute-code.tool.d.ts +4 -0
- package/dist/src/ai/tools/query-graph.tool.d.ts +5 -0
- package/dist/src/auth/middleware.d.ts +4 -0
- package/dist/src/cli/extract-endpoints.d.ts +6 -0
- package/dist/src/config.d.ts +145 -0
- package/dist/src/db/migrate.d.ts +1 -0
- package/dist/src/db/schema.d.ts +345 -0
- package/dist/src/factory.d.ts +17 -0
- package/dist/src/graph/generate-cypher.d.ts +22 -0
- package/dist/src/graph/helpers.d.ts +60 -0
- package/dist/src/graph/index.d.ts +11 -0
- package/dist/src/graph/neo4j.d.ts +18 -0
- package/dist/src/graph/resolver.d.ts +51 -0
- package/dist/src/graph/seed.d.ts +19 -0
- package/dist/src/graph/types.d.ts +104 -0
- package/dist/src/graph/validate.d.ts +2 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/routes/chat.d.ts +3 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/index.d.ts +4 -0
- package/dist/src/routes/threads.d.ts +3 -0
- package/dist/src/routes/ws.d.ts +3 -0
- package/dist/src/types.d.ts +56 -0
- package/dist/src/ws/connections.d.ts +4 -0
- package/dist/src/ws/events.d.ts +8 -0
- package/dist/src/ws/index.d.ts +3 -0
- package/dist/src/ws/notify.d.ts +2 -0
- package/index.ts +1 -0
- package/package.json +57 -0
- package/src/adapters/database.ts +33 -0
- package/src/adapters/minio.ts +89 -0
- package/src/adapters/mssql.ts +203 -0
- package/src/adapters/storage.ts +6 -0
- package/src/ai/fetch.ts +39 -0
- package/src/ai/helpers.ts +36 -0
- package/src/ai/index.ts +145 -0
- package/src/ai/interceptors/resolve-captured-files.ts +64 -0
- package/src/ai/prompt.ts +120 -0
- package/src/ai/tools/call-endpoint.tool.ts +96 -0
- package/src/ai/tools/capture-files.tool.ts +22 -0
- package/src/ai/tools/execute-code.tool.ts +108 -0
- package/src/ai/tools/query-graph.tool.ts +35 -0
- package/src/auth/middleware.ts +63 -0
- package/src/cli/extract-endpoints.ts +588 -0
- package/src/config.ts +155 -0
- package/src/db/migrate.ts +21 -0
- package/src/db/migrations/20260309012148_cloudy_maria_hill/migration.sql +36 -0
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +305 -0
- package/src/db/schema.ts +77 -0
- package/src/factory.ts +159 -0
- package/src/graph/generate-cypher.ts +179 -0
- package/src/graph/helpers.ts +68 -0
- package/src/graph/index.ts +47 -0
- package/src/graph/neo4j.ts +117 -0
- package/src/graph/resolver.ts +357 -0
- package/src/graph/seed.ts +172 -0
- package/src/graph/types.ts +152 -0
- package/src/graph/validate.ts +80 -0
- package/src/index.ts +27 -0
- package/src/routes/chat.ts +38 -0
- package/src/routes/files.ts +105 -0
- package/src/routes/index.ts +4 -0
- package/src/routes/threads.ts +69 -0
- package/src/routes/ws.ts +33 -0
- package/src/types.ts +50 -0
- package/src/ws/connections.ts +23 -0
- package/src/ws/events.ts +6 -0
- package/src/ws/index.ts +7 -0
- package/src/ws/notify.ts +9 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-resolves relevant API endpoints from the Neo4j knowledge graph
|
|
3
|
+
* using vector similarity search on concept descriptions.
|
|
4
|
+
*
|
|
5
|
+
* All configuration is passed explicitly — no hardcoded model references
|
|
6
|
+
* or environment variables.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EmbeddingModel } from "ai";
|
|
10
|
+
import { embed } from "ai";
|
|
11
|
+
import type { Neo4jClient } from "./neo4j.ts";
|
|
12
|
+
import type { ConceptDef } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Config
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type RerankerConfig = {
|
|
19
|
+
url: string;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ResolverConfig = {
|
|
24
|
+
neo4j: Neo4jClient;
|
|
25
|
+
embeddingModel: EmbeddingModel;
|
|
26
|
+
reranker?: RerankerConfig;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Result types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
type EndpointDependency = {
|
|
34
|
+
depPath: string;
|
|
35
|
+
depMethod: string;
|
|
36
|
+
paramName: string;
|
|
37
|
+
fromField: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type EndpointRelation = "read" | "write";
|
|
41
|
+
|
|
42
|
+
export type ResolvedEndpoint = {
|
|
43
|
+
concept: string;
|
|
44
|
+
relation: EndpointRelation;
|
|
45
|
+
name: string;
|
|
46
|
+
path: string;
|
|
47
|
+
method: string;
|
|
48
|
+
params: string;
|
|
49
|
+
body: string;
|
|
50
|
+
response: string;
|
|
51
|
+
dependencies: EndpointDependency[];
|
|
52
|
+
rules: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ResolvedService = {
|
|
56
|
+
concept: string;
|
|
57
|
+
serviceName: string;
|
|
58
|
+
builtInId: string;
|
|
59
|
+
description: string;
|
|
60
|
+
rules: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ResolvedContext = {
|
|
64
|
+
readEndpoints: ResolvedEndpoint[];
|
|
65
|
+
writeEndpoints: ResolvedEndpoint[];
|
|
66
|
+
services: ResolvedService[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal row types
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
type EndpointRow = {
|
|
74
|
+
concept: string;
|
|
75
|
+
relationType: "QUERIED_VIA" | "MUTATED_VIA";
|
|
76
|
+
name: string;
|
|
77
|
+
path: string;
|
|
78
|
+
method: string;
|
|
79
|
+
params: string | null;
|
|
80
|
+
body: string | null;
|
|
81
|
+
response: string | null;
|
|
82
|
+
dependencies: (EndpointDependency | { depPath: null })[];
|
|
83
|
+
rules: (string | null)[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type ServiceRow = {
|
|
87
|
+
concept: string;
|
|
88
|
+
serviceName: string;
|
|
89
|
+
builtInId: string;
|
|
90
|
+
description: string | null;
|
|
91
|
+
rules: (string | null)[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Public API
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
99
|
+
const empty: ResolvedContext = {
|
|
100
|
+
readEndpoints: [],
|
|
101
|
+
writeEndpoints: [],
|
|
102
|
+
services: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const { embedding } = await embed({
|
|
107
|
+
model: config.embeddingModel,
|
|
108
|
+
value: prompt,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const conceptsRaw = await config.neo4j.query(
|
|
112
|
+
`CALL db.index.vector.queryNodes('concept_embeddings', 10, $embedding)
|
|
113
|
+
YIELD node, score
|
|
114
|
+
WHERE score > 0.3
|
|
115
|
+
RETURN node.name AS name, node.description AS description, score
|
|
116
|
+
ORDER BY score DESC`,
|
|
117
|
+
{ embedding },
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
let concepts = JSON.parse(conceptsRaw) as (Pick<
|
|
121
|
+
ConceptDef,
|
|
122
|
+
"name" | "description"
|
|
123
|
+
> & { score: number })[];
|
|
124
|
+
|
|
125
|
+
if (!Array.isArray(concepts) || concepts.length === 0) {
|
|
126
|
+
return empty;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (config.reranker) {
|
|
130
|
+
concepts = await computeRelevanceScores(
|
|
131
|
+
prompt,
|
|
132
|
+
concepts,
|
|
133
|
+
config.reranker,
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
concepts = concepts.slice(0, 3);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const conceptNames = concepts.map((c) => c.name);
|
|
140
|
+
|
|
141
|
+
const endpointsRaw = await config.neo4j.query(
|
|
142
|
+
`UNWIND $names AS conceptName
|
|
143
|
+
MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
|
|
144
|
+
WITH conceptName, collect(DISTINCT ancestor) AS family
|
|
145
|
+
UNWIND family AS related
|
|
146
|
+
MATCH (related)-[rel:QUERIED_VIA|MUTATED_VIA]->(e:Endpoint)
|
|
147
|
+
WITH conceptName, related, rel, e
|
|
148
|
+
OPTIONAL MATCH (e)-[d:DEPENDS_ON]->(dep:Endpoint)
|
|
149
|
+
WITH conceptName, related, rel, e,
|
|
150
|
+
collect(DISTINCT {
|
|
151
|
+
depPath: dep.path,
|
|
152
|
+
depMethod: dep.method,
|
|
153
|
+
paramName: d.paramName,
|
|
154
|
+
fromField: d.fromField
|
|
155
|
+
}) AS dependencies
|
|
156
|
+
OPTIONAL MATCH (rEndpoint:Rule)-[:GOVERNS]->(e)
|
|
157
|
+
OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(related)
|
|
158
|
+
RETURN conceptName AS concept,
|
|
159
|
+
type(rel) AS relationType,
|
|
160
|
+
e.name AS name,
|
|
161
|
+
e.path AS path,
|
|
162
|
+
e.method AS method,
|
|
163
|
+
e.params AS params,
|
|
164
|
+
e.body AS body,
|
|
165
|
+
e.response AS response,
|
|
166
|
+
dependencies,
|
|
167
|
+
collect(DISTINCT rEndpoint.description) +
|
|
168
|
+
collect(DISTINCT rConcept.description) AS rules`,
|
|
169
|
+
{ names: conceptNames },
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const endpointRows = asArray<EndpointRow>(JSON.parse(endpointsRaw));
|
|
173
|
+
const allEndpoints = dedupeEndpoints(
|
|
174
|
+
endpointRows.map(function (row) {
|
|
175
|
+
return {
|
|
176
|
+
concept: row.concept,
|
|
177
|
+
relation:
|
|
178
|
+
row.relationType === "MUTATED_VIA"
|
|
179
|
+
? ("write" as const)
|
|
180
|
+
: ("read" as const),
|
|
181
|
+
name: row.name,
|
|
182
|
+
path: row.path,
|
|
183
|
+
method: row.method,
|
|
184
|
+
params: row.params ?? "[]",
|
|
185
|
+
body: row.body ?? "[]",
|
|
186
|
+
response: row.response ?? "[]",
|
|
187
|
+
dependencies: row.dependencies.filter(
|
|
188
|
+
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
189
|
+
),
|
|
190
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
191
|
+
};
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const servicesRaw = await config.neo4j.query(
|
|
196
|
+
`UNWIND $names AS conceptName
|
|
197
|
+
MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
|
|
198
|
+
WITH conceptName, collect(DISTINCT ancestor) AS family
|
|
199
|
+
UNWIND family AS related
|
|
200
|
+
MATCH (related)-[:REQUESTED_VIA]->(svc:Service)
|
|
201
|
+
WITH conceptName, related, svc
|
|
202
|
+
OPTIONAL MATCH (rService:Rule)-[:GOVERNS]->(svc)
|
|
203
|
+
OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(related)
|
|
204
|
+
RETURN conceptName AS concept,
|
|
205
|
+
svc.name AS serviceName,
|
|
206
|
+
svc.builtInId AS builtInId,
|
|
207
|
+
svc.description AS description,
|
|
208
|
+
collect(DISTINCT rService.description) +
|
|
209
|
+
collect(DISTINCT rConcept.description) AS rules`,
|
|
210
|
+
{ names: conceptNames },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const services = dedupeServices(
|
|
214
|
+
asArray<ServiceRow>(JSON.parse(servicesRaw)).map(function (row) {
|
|
215
|
+
return {
|
|
216
|
+
concept: row.concept,
|
|
217
|
+
serviceName: row.serviceName,
|
|
218
|
+
builtInId: row.builtInId,
|
|
219
|
+
description: row.description ?? "",
|
|
220
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
221
|
+
};
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Service-to-Servicing bridge: if any services were resolved, pull in
|
|
226
|
+
// the generic Servicing endpoints
|
|
227
|
+
let servicingEndpoints: ResolvedEndpoint[] = [];
|
|
228
|
+
if (services.length > 0) {
|
|
229
|
+
const servicingRaw = await config.neo4j.query(
|
|
230
|
+
`MATCH (c:Concept {name: 'ServiceRequest'})-[rel:QUERIED_VIA|MUTATED_VIA]->(e:Endpoint)
|
|
231
|
+
OPTIONAL MATCH (e)-[d:DEPENDS_ON]->(dep:Endpoint)
|
|
232
|
+
WITH c, rel, e,
|
|
233
|
+
collect(DISTINCT {
|
|
234
|
+
depPath: dep.path,
|
|
235
|
+
depMethod: dep.method,
|
|
236
|
+
paramName: d.paramName,
|
|
237
|
+
fromField: d.fromField
|
|
238
|
+
}) AS dependencies
|
|
239
|
+
OPTIONAL MATCH (rEndpoint:Rule)-[:GOVERNS]->(e)
|
|
240
|
+
OPTIONAL MATCH (rConcept:Rule)-[:GOVERNS]->(c)
|
|
241
|
+
RETURN 'ServiceRequest' AS concept,
|
|
242
|
+
type(rel) AS relationType,
|
|
243
|
+
e.name AS name,
|
|
244
|
+
e.path AS path,
|
|
245
|
+
e.method AS method,
|
|
246
|
+
e.params AS params,
|
|
247
|
+
e.body AS body,
|
|
248
|
+
e.response AS response,
|
|
249
|
+
dependencies,
|
|
250
|
+
collect(DISTINCT rEndpoint.description) +
|
|
251
|
+
collect(DISTINCT rConcept.description) AS rules`,
|
|
252
|
+
{},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
servicingEndpoints = dedupeEndpoints(
|
|
256
|
+
asArray<EndpointRow>(JSON.parse(servicingRaw)).map(function (row) {
|
|
257
|
+
return {
|
|
258
|
+
concept: row.concept,
|
|
259
|
+
relation:
|
|
260
|
+
row.relationType === "MUTATED_VIA"
|
|
261
|
+
? ("write" as const)
|
|
262
|
+
: ("read" as const),
|
|
263
|
+
name: row.name,
|
|
264
|
+
path: row.path,
|
|
265
|
+
method: row.method,
|
|
266
|
+
params: row.params ?? "[]",
|
|
267
|
+
body: row.body ?? "[]",
|
|
268
|
+
response: row.response ?? "[]",
|
|
269
|
+
dependencies: row.dependencies.filter(
|
|
270
|
+
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
271
|
+
),
|
|
272
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const combinedEndpoints = dedupeEndpoints([
|
|
279
|
+
...allEndpoints,
|
|
280
|
+
...servicingEndpoints,
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
readEndpoints: combinedEndpoints.filter((ep) => ep.relation === "read"),
|
|
285
|
+
writeEndpoints: combinedEndpoints.filter((ep) => ep.relation === "write"),
|
|
286
|
+
services,
|
|
287
|
+
} satisfies ResolvedContext;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("Graph resolution failed:", error);
|
|
290
|
+
return empty;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Internal helpers
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
function asArray<T>(value: unknown) {
|
|
299
|
+
return Array.isArray(value) ? (value as T[]) : ([] as T[]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function dedupeEndpoints(endpoints: ResolvedEndpoint[]) {
|
|
303
|
+
const seen = new Set<string>();
|
|
304
|
+
const deduped: ResolvedEndpoint[] = [];
|
|
305
|
+
|
|
306
|
+
for (const ep of endpoints) {
|
|
307
|
+
const key = `${ep.relation}|${ep.concept}|${ep.method}|${ep.path}`;
|
|
308
|
+
if (seen.has(key)) continue;
|
|
309
|
+
seen.add(key);
|
|
310
|
+
deduped.push(ep);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return deduped;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function dedupeServices(services: ResolvedService[]) {
|
|
317
|
+
const seen = new Set<string>();
|
|
318
|
+
const deduped: ResolvedService[] = [];
|
|
319
|
+
|
|
320
|
+
for (const svc of services) {
|
|
321
|
+
const key = `${svc.concept}|${svc.builtInId}`;
|
|
322
|
+
if (seen.has(key)) continue;
|
|
323
|
+
seen.add(key);
|
|
324
|
+
deduped.push(svc);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return deduped;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function computeRelevanceScores(
|
|
331
|
+
prompt: string,
|
|
332
|
+
concepts: (Pick<ConceptDef, "name" | "description"> & { score: number })[],
|
|
333
|
+
reranker: RerankerConfig,
|
|
334
|
+
) {
|
|
335
|
+
const response = await fetch(reranker.url, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
Authorization: `Bearer ${reranker.apiKey}`,
|
|
339
|
+
"Content-Type": "application/json",
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
queries: [prompt],
|
|
343
|
+
documents: concepts.map((x) => x.description),
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const data = (await response.json()) as { scores: number[] };
|
|
348
|
+
const scores = data.scores;
|
|
349
|
+
|
|
350
|
+
const scoredConcepts = concepts.map((c, idx) => ({
|
|
351
|
+
...c,
|
|
352
|
+
score: scores[idx]!,
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
scoredConcepts.sort((a, b) => b.score - a.score);
|
|
356
|
+
return scoredConcepts.slice(0, 3);
|
|
357
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed orchestrator for the knowledge graph.
|
|
3
|
+
*
|
|
4
|
+
* Validates domains, generates Cypher, executes against Neo4j,
|
|
5
|
+
* and generates concept embeddings. All configuration is passed
|
|
6
|
+
* explicitly — no environment variables or Convex dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EmbeddingModel } from "ai";
|
|
10
|
+
import { embed } from "ai";
|
|
11
|
+
import type { Neo4jClient, Neo4jConfig } from "./neo4j.ts";
|
|
12
|
+
import { createNeo4jClient } from "./neo4j.ts";
|
|
13
|
+
import { generateCypher, type GeneratedCypher } from "./generate-cypher.ts";
|
|
14
|
+
import type { DomainDef } from "./types.ts";
|
|
15
|
+
import { validateDomain } from "./validate.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Config
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type EmbeddingConfig = {
|
|
22
|
+
model: EmbeddingModel;
|
|
23
|
+
dimension: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SeedGraphConfig = {
|
|
27
|
+
neo4j: Neo4jConfig;
|
|
28
|
+
embedding: EmbeddingConfig;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Internal helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
async function runStatements(
|
|
36
|
+
client: Neo4jClient,
|
|
37
|
+
label: string,
|
|
38
|
+
statements: string[],
|
|
39
|
+
) {
|
|
40
|
+
console.log(`\n--- ${label} (${statements.length} statements) ---`);
|
|
41
|
+
|
|
42
|
+
let success = 0;
|
|
43
|
+
let failed = 0;
|
|
44
|
+
|
|
45
|
+
for (const stmt of statements) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await client.query(stmt);
|
|
48
|
+
const parsed = JSON.parse(result);
|
|
49
|
+
if (parsed.error) {
|
|
50
|
+
console.error(` ✗ Failed: ${parsed.message}`);
|
|
51
|
+
console.error(` Statement: ${stmt.substring(0, 120)}...`);
|
|
52
|
+
failed++;
|
|
53
|
+
} else {
|
|
54
|
+
success++;
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(` ✗ Exception: ${e}`);
|
|
58
|
+
console.error(` Statement: ${stmt.substring(0, 120)}...`);
|
|
59
|
+
failed++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(` ✓ ${success} succeeded, ${failed} failed`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function generateEmbeddings(
|
|
67
|
+
client: Neo4jClient,
|
|
68
|
+
embeddingConfig: EmbeddingConfig,
|
|
69
|
+
) {
|
|
70
|
+
console.log("Seeding concept embeddings...");
|
|
71
|
+
|
|
72
|
+
// 1. Create the vector index (idempotent)
|
|
73
|
+
await client.query(
|
|
74
|
+
`CREATE VECTOR INDEX concept_embeddings IF NOT EXISTS
|
|
75
|
+
FOR (c:Concept) ON (c.embedding)
|
|
76
|
+
OPTIONS {indexConfig: {
|
|
77
|
+
\`vector.dimensions\`: ${embeddingConfig.dimension},
|
|
78
|
+
\`vector.similarity_function\`: 'cosine'
|
|
79
|
+
}}`,
|
|
80
|
+
);
|
|
81
|
+
console.log("Vector index created (or already exists)");
|
|
82
|
+
|
|
83
|
+
// 2. Fetch all concepts
|
|
84
|
+
const conceptsRaw = await client.query(
|
|
85
|
+
"MATCH (c:Concept) RETURN c.name AS name, c.description AS description",
|
|
86
|
+
);
|
|
87
|
+
const concepts: { name: string; description: string }[] =
|
|
88
|
+
JSON.parse(conceptsRaw);
|
|
89
|
+
|
|
90
|
+
if (!Array.isArray(concepts) || concepts.length === 0) {
|
|
91
|
+
console.log("No concepts found in Neo4j");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`Found ${concepts.length} concepts, generating embeddings...`);
|
|
96
|
+
|
|
97
|
+
// 3. Generate and store embeddings for each concept
|
|
98
|
+
const embeddingResults = await Promise.all(
|
|
99
|
+
concepts.map(async (concept) => ({
|
|
100
|
+
concept,
|
|
101
|
+
embedding: (
|
|
102
|
+
await embed({
|
|
103
|
+
model: embeddingConfig.model,
|
|
104
|
+
value: concept.description,
|
|
105
|
+
})
|
|
106
|
+
).embedding,
|
|
107
|
+
})),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// 4. Store the embeddings in the graph
|
|
111
|
+
await Promise.all(
|
|
112
|
+
embeddingResults.map(
|
|
113
|
+
async (result) =>
|
|
114
|
+
await client.query(
|
|
115
|
+
`MATCH (c:Concept {name: $name})
|
|
116
|
+
SET c.embedding = $embedding`,
|
|
117
|
+
{
|
|
118
|
+
name: result.concept.name,
|
|
119
|
+
embedding: result.embedding,
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
console.log("Embedding seeding complete!");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Public API
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export async function seedGraph(
|
|
133
|
+
config: SeedGraphConfig,
|
|
134
|
+
domains: Record<string, DomainDef>,
|
|
135
|
+
) {
|
|
136
|
+
// Validate all domains
|
|
137
|
+
const validationErrors = Object.entries(domains).flatMap(([name, domain]) =>
|
|
138
|
+
validateDomain(domain).map((err) => `[${name}] ${err}`),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (validationErrors.length) {
|
|
142
|
+
validationErrors.forEach((err) => console.error(err));
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Validation failed with ${validationErrors.length} error(s)`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const client = createNeo4jClient(config.neo4j, config.embedding.model);
|
|
149
|
+
const targets = Object.entries(domains);
|
|
150
|
+
|
|
151
|
+
console.log(`Seeding ${targets.length} domain(s)...`);
|
|
152
|
+
|
|
153
|
+
// Generate Cypher for all target domains
|
|
154
|
+
const allCypher: [string, GeneratedCypher][] = targets.map(([name, data]) => [
|
|
155
|
+
name,
|
|
156
|
+
generateCypher(data),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// Phase 1: Create all nodes across every domain first.
|
|
160
|
+
// This ensures cross-domain references resolve correctly.
|
|
161
|
+
const allNodes = allCypher.flatMap(([, c]) => c.nodes);
|
|
162
|
+
await runStatements(client, "Phase 1: Nodes", allNodes);
|
|
163
|
+
|
|
164
|
+
// Phase 2: Create all edges now that every node exists.
|
|
165
|
+
const allEdges = allCypher.flatMap(([, c]) => c.edges);
|
|
166
|
+
await runStatements(client, "Phase 2: Edges", allEdges);
|
|
167
|
+
|
|
168
|
+
console.log("\nGraph seeding complete!");
|
|
169
|
+
|
|
170
|
+
// Phase 3: Generate embeddings for concepts
|
|
171
|
+
await generateEmbeddings(client, config.embedding);
|
|
172
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative types for knowledge graph seed data.
|
|
3
|
+
*
|
|
4
|
+
* This is the v2 schema:
|
|
5
|
+
* - Endpoints use params/body/response EndpointProperty arrays.
|
|
6
|
+
* - Source/default mappings are intentionally removed.
|
|
7
|
+
* - Services are concept associations (no hardcoded endpoint steps).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Endpoint property model (v2)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export type EndpointScalarType =
|
|
15
|
+
| "uuid"
|
|
16
|
+
| "number"
|
|
17
|
+
| "date"
|
|
18
|
+
| "datetime"
|
|
19
|
+
| "string"
|
|
20
|
+
| "boolean"
|
|
21
|
+
| "any"
|
|
22
|
+
| "object";
|
|
23
|
+
|
|
24
|
+
export type EndpointProperty = {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly required: boolean;
|
|
27
|
+
readonly type: EndpointScalarType | string;
|
|
28
|
+
readonly isArray?: boolean;
|
|
29
|
+
readonly properties?: readonly EndpointProperty[];
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ResponseKind = "object" | "array" | "paginated" | "file" | "none";
|
|
34
|
+
|
|
35
|
+
export type AutoGenerated = {
|
|
36
|
+
readonly params: readonly EndpointProperty[];
|
|
37
|
+
readonly body: readonly EndpointProperty[];
|
|
38
|
+
readonly response: readonly EndpointProperty[];
|
|
39
|
+
readonly successStatus: number;
|
|
40
|
+
readonly errorStatuses: readonly number[];
|
|
41
|
+
readonly responseKind?: ResponseKind;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Concept descriptor
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export type ConceptDef = {
|
|
49
|
+
readonly __brand: "concept";
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
aliases?: string[];
|
|
53
|
+
parentConcept?: ConceptDef;
|
|
54
|
+
governedBy?: RuleDef[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Endpoint descriptor (v2)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export type EndpointDef = {
|
|
62
|
+
readonly __brand: "endpoint";
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
path: string;
|
|
66
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
67
|
+
propertiesDescriptions: Record<string, string>;
|
|
68
|
+
|
|
69
|
+
params: EndpointProperty[];
|
|
70
|
+
body: EndpointProperty[];
|
|
71
|
+
response: EndpointProperty[];
|
|
72
|
+
successStatus: number;
|
|
73
|
+
errorStatuses: number[];
|
|
74
|
+
responseKind: ResponseKind;
|
|
75
|
+
|
|
76
|
+
queries?: ConceptDef[];
|
|
77
|
+
mutates?: ConceptDef[];
|
|
78
|
+
|
|
79
|
+
returns?: { concept: ConceptDef; field?: string }[];
|
|
80
|
+
dependsOn?: {
|
|
81
|
+
endpoint: EndpointDef;
|
|
82
|
+
paramName: string;
|
|
83
|
+
fromField: string;
|
|
84
|
+
}[];
|
|
85
|
+
|
|
86
|
+
governedBy?: RuleDef[];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// EndpointInput
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export type EndpointInput = {
|
|
94
|
+
name: string;
|
|
95
|
+
path: string;
|
|
96
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
97
|
+
description?: string;
|
|
98
|
+
autoGenerated?: AutoGenerated;
|
|
99
|
+
|
|
100
|
+
propertiesDescriptions?: Record<string, string>;
|
|
101
|
+
|
|
102
|
+
queries?: ConceptDef[];
|
|
103
|
+
mutates?: ConceptDef[];
|
|
104
|
+
paramDescriptions?: Partial<Record<string, string>>;
|
|
105
|
+
responseDescriptions?: Partial<Record<string, string>>;
|
|
106
|
+
|
|
107
|
+
returns?: { concept: ConceptDef; field?: string }[];
|
|
108
|
+
dependsOn?: {
|
|
109
|
+
endpoint: EndpointDef;
|
|
110
|
+
paramName: string;
|
|
111
|
+
fromField: string;
|
|
112
|
+
}[];
|
|
113
|
+
|
|
114
|
+
governedBy?: RuleDef[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Service descriptor (v2)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export type ServiceDef = {
|
|
122
|
+
readonly __brand: "service";
|
|
123
|
+
name: string;
|
|
124
|
+
description: string;
|
|
125
|
+
builtInId: string;
|
|
126
|
+
belongsTo: ConceptDef;
|
|
127
|
+
governedBy?: RuleDef[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Rule descriptor
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
export type RuleDef = {
|
|
135
|
+
readonly __brand: "rule";
|
|
136
|
+
name: string;
|
|
137
|
+
description: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Domain descriptor
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
export type DomainDef = {
|
|
145
|
+
readonly __brand: "domain";
|
|
146
|
+
name: string;
|
|
147
|
+
description: string;
|
|
148
|
+
concepts?: ConceptDef[];
|
|
149
|
+
endpoints?: EndpointDef[];
|
|
150
|
+
services?: ServiceDef[];
|
|
151
|
+
rules?: RuleDef[];
|
|
152
|
+
};
|