@m6d/cortex-server 1.1.0 → 1.1.2
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 +38 -38
- package/dist/src/factory.d.ts +13 -1
- package/dist/src/ws/index.d.ts +1 -1
- package/package.json +54 -54
- package/src/adapters/database.ts +21 -28
- package/src/adapters/minio.ts +69 -69
- package/src/adapters/mssql.ts +171 -195
- package/src/adapters/storage.ts +4 -4
- package/src/ai/fetch.ts +31 -31
- package/src/ai/helpers.ts +18 -22
- package/src/ai/index.ts +101 -113
- package/src/ai/interceptors/resolve-captured-files.ts +42 -49
- package/src/ai/prompt.ts +80 -83
- package/src/ai/tools/call-endpoint.tool.ts +75 -82
- package/src/ai/tools/capture-files.tool.ts +15 -17
- package/src/ai/tools/execute-code.tool.ts +73 -80
- package/src/ai/tools/query-graph.tool.ts +17 -17
- package/src/auth/middleware.ts +51 -51
- package/src/cli/extract-endpoints.ts +436 -474
- package/src/config.ts +124 -134
- package/src/db/migrate.ts +13 -13
- package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
- package/src/db/schema.ts +46 -58
- package/src/factory.ts +136 -139
- package/src/graph/generate-cypher.ts +97 -97
- package/src/graph/helpers.ts +37 -37
- package/src/graph/index.ts +20 -20
- package/src/graph/neo4j.ts +82 -89
- package/src/graph/resolver.ts +201 -211
- package/src/graph/seed.ts +101 -114
- package/src/graph/types.ts +88 -88
- package/src/graph/validate.ts +55 -57
- package/src/index.ts +5 -5
- package/src/routes/chat.ts +23 -23
- package/src/routes/files.ts +75 -80
- package/src/routes/threads.ts +52 -54
- package/src/routes/ws.ts +22 -22
- package/src/types.ts +30 -30
- package/src/ws/connections.ts +11 -11
- package/src/ws/events.ts +2 -2
- package/src/ws/index.ts +1 -5
- package/src/ws/notify.ts +4 -4
package/src/graph/resolver.ts
CHANGED
|
@@ -16,14 +16,14 @@ import type { ConceptDef } from "./types.ts";
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
18
|
export type RerankerConfig = {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
url: string;
|
|
20
|
+
apiKey: string;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type ResolverConfig = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
neo4j: Neo4jClient;
|
|
25
|
+
embeddingModel: EmbeddingModel;
|
|
26
|
+
reranker?: RerankerConfig;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -31,39 +31,39 @@ export type ResolverConfig = {
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
type EndpointDependency = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
depPath: string;
|
|
35
|
+
depMethod: string;
|
|
36
|
+
paramName: string;
|
|
37
|
+
fromField: string;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
type EndpointRelation = "read" | "write";
|
|
41
41
|
|
|
42
42
|
export type ResolvedEndpoint = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
53
|
};
|
|
54
54
|
|
|
55
55
|
export type ResolvedService = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
concept: string;
|
|
57
|
+
serviceName: string;
|
|
58
|
+
builtInId: string;
|
|
59
|
+
description: string;
|
|
60
|
+
rules: string[];
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export type ResolvedContext = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
readEndpoints: ResolvedEndpoint[];
|
|
65
|
+
writeEndpoints: ResolvedEndpoint[];
|
|
66
|
+
services: ResolvedService[];
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
@@ -71,24 +71,24 @@ export type ResolvedContext = {
|
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
|
|
73
73
|
type EndpointRow = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
84
|
};
|
|
85
85
|
|
|
86
86
|
type ServiceRow = {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
concept: string;
|
|
88
|
+
serviceName: string;
|
|
89
|
+
builtInId: string;
|
|
90
|
+
description: string | null;
|
|
91
|
+
rules: (string | null)[];
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
@@ -96,50 +96,45 @@ type ServiceRow = {
|
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
97
|
|
|
98
98
|
export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
113
|
YIELD node, score
|
|
114
114
|
WHERE score > 0.3
|
|
115
115
|
RETURN node.name AS name, node.description AS description, score
|
|
116
116
|
ORDER BY score DESC`,
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
{ embedding },
|
|
118
|
+
);
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
> & { score: number })[];
|
|
120
|
+
let concepts = JSON.parse(conceptsRaw) as (Pick<ConceptDef, "name" | "description"> & {
|
|
121
|
+
score: number;
|
|
122
|
+
})[];
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
if (!Array.isArray(concepts) || concepts.length === 0) {
|
|
125
|
+
return empty;
|
|
126
|
+
}
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
concepts = concepts.slice(0, 3);
|
|
137
|
-
}
|
|
128
|
+
if (config.reranker) {
|
|
129
|
+
concepts = await computeRelevanceScores(prompt, concepts, config.reranker);
|
|
130
|
+
} else {
|
|
131
|
+
concepts = concepts.slice(0, 3);
|
|
132
|
+
}
|
|
138
133
|
|
|
139
|
-
|
|
134
|
+
const conceptNames = concepts.map((c) => c.name);
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
const endpointsRaw = await config.neo4j.query(
|
|
137
|
+
`UNWIND $names AS conceptName
|
|
143
138
|
MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
|
|
144
139
|
WITH conceptName, collect(DISTINCT ancestor) AS family
|
|
145
140
|
UNWIND family AS related
|
|
@@ -166,34 +161,32 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
166
161
|
dependencies,
|
|
167
162
|
collect(DISTINCT rEndpoint.description) +
|
|
168
163
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const servicesRaw = await config.neo4j.query(
|
|
196
|
-
`UNWIND $names AS conceptName
|
|
164
|
+
{ names: conceptNames },
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const endpointRows = asArray<EndpointRow>(JSON.parse(endpointsRaw));
|
|
168
|
+
const allEndpoints = dedupeEndpoints(
|
|
169
|
+
endpointRows.map(function (row) {
|
|
170
|
+
return {
|
|
171
|
+
concept: row.concept,
|
|
172
|
+
relation:
|
|
173
|
+
row.relationType === "MUTATED_VIA" ? ("write" as const) : ("read" as const),
|
|
174
|
+
name: row.name,
|
|
175
|
+
path: row.path,
|
|
176
|
+
method: row.method,
|
|
177
|
+
params: row.params ?? "[]",
|
|
178
|
+
body: row.body ?? "[]",
|
|
179
|
+
response: row.response ?? "[]",
|
|
180
|
+
dependencies: row.dependencies.filter(
|
|
181
|
+
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
182
|
+
),
|
|
183
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
184
|
+
};
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const servicesRaw = await config.neo4j.query(
|
|
189
|
+
`UNWIND $names AS conceptName
|
|
197
190
|
MATCH (c:Concept {name: conceptName})-[:SPECIALIZES*0..3]->(ancestor:Concept)
|
|
198
191
|
WITH conceptName, collect(DISTINCT ancestor) AS family
|
|
199
192
|
UNWIND family AS related
|
|
@@ -207,27 +200,27 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
207
200
|
svc.description AS description,
|
|
208
201
|
collect(DISTINCT rService.description) +
|
|
209
202
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
203
|
+
{ names: conceptNames },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const services = dedupeServices(
|
|
207
|
+
asArray<ServiceRow>(JSON.parse(servicesRaw)).map(function (row) {
|
|
208
|
+
return {
|
|
209
|
+
concept: row.concept,
|
|
210
|
+
serviceName: row.serviceName,
|
|
211
|
+
builtInId: row.builtInId,
|
|
212
|
+
description: row.description ?? "",
|
|
213
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
214
|
+
};
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Service-to-Servicing bridge: if any services were resolved, pull in
|
|
219
|
+
// the generic Servicing endpoints
|
|
220
|
+
let servicingEndpoints: ResolvedEndpoint[] = [];
|
|
221
|
+
if (services.length > 0) {
|
|
222
|
+
const servicingRaw = await config.neo4j.query(
|
|
223
|
+
`MATCH (c:Concept {name: 'ServiceRequest'})-[rel:QUERIED_VIA|MUTATED_VIA]->(e:Endpoint)
|
|
231
224
|
OPTIONAL MATCH (e)-[d:DEPENDS_ON]->(dep:Endpoint)
|
|
232
225
|
WITH c, rel, e,
|
|
233
226
|
collect(DISTINCT {
|
|
@@ -249,46 +242,43 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
249
242
|
dependencies,
|
|
250
243
|
collect(DISTINCT rEndpoint.description) +
|
|
251
244
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
245
|
+
{},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
servicingEndpoints = dedupeEndpoints(
|
|
249
|
+
asArray<EndpointRow>(JSON.parse(servicingRaw)).map(function (row) {
|
|
250
|
+
return {
|
|
251
|
+
concept: row.concept,
|
|
252
|
+
relation:
|
|
253
|
+
row.relationType === "MUTATED_VIA"
|
|
254
|
+
? ("write" as const)
|
|
255
|
+
: ("read" as const),
|
|
256
|
+
name: row.name,
|
|
257
|
+
path: row.path,
|
|
258
|
+
method: row.method,
|
|
259
|
+
params: row.params ?? "[]",
|
|
260
|
+
body: row.body ?? "[]",
|
|
261
|
+
response: row.response ?? "[]",
|
|
262
|
+
dependencies: row.dependencies.filter(
|
|
263
|
+
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
264
|
+
),
|
|
265
|
+
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
266
|
+
};
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const combinedEndpoints = dedupeEndpoints([...allEndpoints, ...servicingEndpoints]);
|
|
277
272
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
} satisfies ResolvedContext;
|
|
288
|
-
} catch (error) {
|
|
289
|
-
console.error("Graph resolution failed:", error);
|
|
290
|
-
return empty;
|
|
291
|
-
}
|
|
273
|
+
return {
|
|
274
|
+
readEndpoints: combinedEndpoints.filter((ep) => ep.relation === "read"),
|
|
275
|
+
writeEndpoints: combinedEndpoints.filter((ep) => ep.relation === "write"),
|
|
276
|
+
services,
|
|
277
|
+
} satisfies ResolvedContext;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error("Graph resolution failed:", error);
|
|
280
|
+
return empty;
|
|
281
|
+
}
|
|
292
282
|
}
|
|
293
283
|
|
|
294
284
|
// ---------------------------------------------------------------------------
|
|
@@ -296,62 +286,62 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
296
286
|
// ---------------------------------------------------------------------------
|
|
297
287
|
|
|
298
288
|
function asArray<T>(value: unknown) {
|
|
299
|
-
|
|
289
|
+
return Array.isArray(value) ? (value as T[]) : ([] as T[]);
|
|
300
290
|
}
|
|
301
291
|
|
|
302
292
|
function dedupeEndpoints(endpoints: ResolvedEndpoint[]) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
293
|
+
const seen = new Set<string>();
|
|
294
|
+
const deduped: ResolvedEndpoint[] = [];
|
|
295
|
+
|
|
296
|
+
for (const ep of endpoints) {
|
|
297
|
+
const key = `${ep.relation}|${ep.concept}|${ep.method}|${ep.path}`;
|
|
298
|
+
if (seen.has(key)) continue;
|
|
299
|
+
seen.add(key);
|
|
300
|
+
deduped.push(ep);
|
|
301
|
+
}
|
|
312
302
|
|
|
313
|
-
|
|
303
|
+
return deduped;
|
|
314
304
|
}
|
|
315
305
|
|
|
316
306
|
function dedupeServices(services: ResolvedService[]) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
307
|
+
const seen = new Set<string>();
|
|
308
|
+
const deduped: ResolvedService[] = [];
|
|
309
|
+
|
|
310
|
+
for (const svc of services) {
|
|
311
|
+
const key = `${svc.concept}|${svc.builtInId}`;
|
|
312
|
+
if (seen.has(key)) continue;
|
|
313
|
+
seen.add(key);
|
|
314
|
+
deduped.push(svc);
|
|
315
|
+
}
|
|
326
316
|
|
|
327
|
-
|
|
317
|
+
return deduped;
|
|
328
318
|
}
|
|
329
319
|
|
|
330
320
|
async function computeRelevanceScores(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
321
|
+
prompt: string,
|
|
322
|
+
concepts: (Pick<ConceptDef, "name" | "description"> & { score: number })[],
|
|
323
|
+
reranker: RerankerConfig,
|
|
334
324
|
) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
325
|
+
const response = await fetch(reranker.url, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: {
|
|
328
|
+
Authorization: `Bearer ${reranker.apiKey}`,
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
queries: [prompt],
|
|
333
|
+
documents: concepts.map((x) => x.description),
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const data = (await response.json()) as { scores: number[] };
|
|
338
|
+
const scores = data.scores;
|
|
339
|
+
|
|
340
|
+
const scoredConcepts = concepts.map((c, idx) => ({
|
|
341
|
+
...c,
|
|
342
|
+
score: scores[idx]!,
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
scoredConcepts.sort((a, b) => b.score - a.score);
|
|
346
|
+
return scoredConcepts.slice(0, 3);
|
|
357
347
|
}
|