@m6d/cortex-server 1.3.0 → 1.5.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/dist/src/adapters/database.d.ts +3 -0
- package/dist/src/ai/active-streams.d.ts +14 -0
- package/dist/src/ai/context/builder.d.ts +24 -0
- package/dist/src/ai/context/compressor.d.ts +7 -0
- package/dist/src/ai/context/index.d.ts +15 -0
- package/dist/src/ai/context/summarizer.d.ts +5 -0
- package/dist/src/ai/context/token-estimator.d.ts +20 -0
- package/dist/src/ai/context/types.d.ts +20 -0
- package/dist/src/ai/index.d.ts +1 -1
- package/dist/src/ai/prompt.d.ts +6 -1
- package/dist/src/config.d.ts +4 -0
- package/dist/src/db/schema.d.ts +19 -1
- package/dist/src/graph/expand-domains.d.ts +2 -0
- package/dist/src/graph/helpers.d.ts +5 -0
- package/dist/src/graph/resolver.d.ts +2 -0
- package/dist/src/graph/types.d.ts +6 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/routes/ws.d.ts +5 -1
- package/dist/src/types.d.ts +32 -14
- package/dist/src/ws/connections.d.ts +3 -3
- package/dist/src/ws/events.d.ts +28 -3
- package/dist/src/ws/index.d.ts +1 -1
- package/dist/src/ws/notify.d.ts +1 -1
- package/package.json +1 -1
- package/src/adapters/database.ts +3 -0
- package/src/adapters/mssql.ts +26 -6
- package/src/ai/active-streams.ts +123 -0
- package/src/ai/context/builder.ts +94 -0
- package/src/ai/context/compressor.ts +47 -0
- package/src/ai/context/index.ts +75 -0
- package/src/ai/context/summarizer.ts +50 -0
- package/src/ai/context/token-estimator.ts +60 -0
- package/src/ai/context/types.ts +28 -0
- package/src/ai/index.ts +124 -29
- package/src/ai/prompt.ts +27 -18
- package/src/ai/tools/query-graph.tool.ts +1 -1
- package/src/cli/extract-endpoints.ts +18 -18
- package/src/config.ts +4 -0
- package/src/db/migrations/20260315000000_add_context_meta/migration.sql +1 -0
- package/src/db/schema.ts +6 -1
- package/src/factory.ts +11 -1
- package/src/graph/expand-domains.ts +276 -0
- package/src/graph/generate-cypher.ts +18 -5
- package/src/graph/helpers.ts +1 -0
- package/src/graph/resolver.ts +10 -0
- package/src/graph/seed.ts +5 -2
- package/src/graph/types.ts +6 -0
- package/src/index.ts +2 -0
- package/src/routes/chat.ts +47 -2
- package/src/routes/threads.ts +46 -9
- package/src/routes/ws.ts +37 -23
- package/src/types.ts +37 -13
- package/src/ws/connections.ts +15 -9
- package/src/ws/events.ts +31 -3
- package/src/ws/index.ts +9 -1
- package/src/ws/notify.ts +2 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { ConceptDef, DomainDef, EndpointDef, RuleDef, ServiceDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type DomainCollections = {
|
|
4
|
+
concepts: ConceptDef[];
|
|
5
|
+
endpoints: EndpointDef[];
|
|
6
|
+
services: ServiceDef[];
|
|
7
|
+
rules: RuleDef[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type OwnershipMaps = {
|
|
11
|
+
concepts: WeakMap<ConceptDef, string>;
|
|
12
|
+
endpoints: WeakMap<EndpointDef, string>;
|
|
13
|
+
services: WeakMap<ServiceDef, string>;
|
|
14
|
+
rules: WeakMap<RuleDef, string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function expandDomains(domains: Record<string, DomainDef>) {
|
|
18
|
+
const ownership = createOwnershipMaps(domains);
|
|
19
|
+
const expanded: Record<string, DomainDef> = {};
|
|
20
|
+
|
|
21
|
+
for (const [domainKey, domain] of Object.entries(domains)) {
|
|
22
|
+
const collections = cloneCollections(domain);
|
|
23
|
+
const pendingConcepts = [...collections.concepts];
|
|
24
|
+
const pendingEndpoints = [...collections.endpoints];
|
|
25
|
+
const pendingServices = [...collections.services];
|
|
26
|
+
|
|
27
|
+
while (
|
|
28
|
+
pendingConcepts.length > 0 ||
|
|
29
|
+
pendingEndpoints.length > 0 ||
|
|
30
|
+
pendingServices.length > 0
|
|
31
|
+
) {
|
|
32
|
+
const concept = pendingConcepts.pop();
|
|
33
|
+
if (concept) {
|
|
34
|
+
includeConcept({
|
|
35
|
+
domainKey,
|
|
36
|
+
concept,
|
|
37
|
+
ownership,
|
|
38
|
+
collections,
|
|
39
|
+
pendingConcepts,
|
|
40
|
+
pendingEndpoints,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const endpoint = pendingEndpoints.pop();
|
|
45
|
+
if (endpoint) {
|
|
46
|
+
includeEndpoint({
|
|
47
|
+
domainKey,
|
|
48
|
+
endpoint,
|
|
49
|
+
ownership,
|
|
50
|
+
collections,
|
|
51
|
+
pendingConcepts,
|
|
52
|
+
pendingEndpoints,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const service = pendingServices.pop();
|
|
57
|
+
if (service) {
|
|
58
|
+
includeService({
|
|
59
|
+
domainKey,
|
|
60
|
+
service,
|
|
61
|
+
ownership,
|
|
62
|
+
collections,
|
|
63
|
+
pendingConcepts,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expanded[domainKey] = {
|
|
69
|
+
...domain,
|
|
70
|
+
concepts: collections.concepts,
|
|
71
|
+
endpoints: collections.endpoints,
|
|
72
|
+
services: collections.services,
|
|
73
|
+
rules: collections.rules,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return expanded;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createOwnershipMaps(domains: Record<string, DomainDef>) {
|
|
81
|
+
const ownership: OwnershipMaps = {
|
|
82
|
+
concepts: new WeakMap<ConceptDef, string>(),
|
|
83
|
+
endpoints: new WeakMap<EndpointDef, string>(),
|
|
84
|
+
services: new WeakMap<ServiceDef, string>(),
|
|
85
|
+
rules: new WeakMap<RuleDef, string>(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const [domainKey, domain] of Object.entries(domains)) {
|
|
89
|
+
for (const concept of domain.concepts ?? []) {
|
|
90
|
+
assignOwner(ownership.concepts, concept, domainKey);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const endpoint of domain.endpoints ?? []) {
|
|
94
|
+
assignOwner(ownership.endpoints, endpoint, domainKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const service of domain.services ?? []) {
|
|
98
|
+
assignOwner(ownership.services, service, domainKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const rule of domain.rules ?? []) {
|
|
102
|
+
assignOwner(ownership.rules, rule, domainKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ownership;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assignOwner<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
|
|
110
|
+
if (!owners.has(value)) {
|
|
111
|
+
owners.set(value, domainKey);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cloneCollections(domain: DomainDef) {
|
|
116
|
+
return {
|
|
117
|
+
concepts: [...(domain.concepts ?? [])],
|
|
118
|
+
endpoints: [...(domain.endpoints ?? [])],
|
|
119
|
+
services: [...(domain.services ?? [])],
|
|
120
|
+
rules: [...(domain.rules ?? [])],
|
|
121
|
+
} satisfies DomainCollections;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function includeConcept(options: {
|
|
125
|
+
domainKey: string;
|
|
126
|
+
concept: ConceptDef;
|
|
127
|
+
ownership: OwnershipMaps;
|
|
128
|
+
collections: DomainCollections;
|
|
129
|
+
pendingConcepts: ConceptDef[];
|
|
130
|
+
pendingEndpoints: EndpointDef[];
|
|
131
|
+
}) {
|
|
132
|
+
const { domainKey, concept, ownership, collections, pendingConcepts, pendingEndpoints } =
|
|
133
|
+
options;
|
|
134
|
+
|
|
135
|
+
includeRule(domainKey, concept.governedBy ?? [], ownership, collections);
|
|
136
|
+
|
|
137
|
+
if (shouldAttach(ownership.concepts, concept, domainKey)) {
|
|
138
|
+
pushUnique(collections.concepts, concept);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
concept.parentConcept &&
|
|
143
|
+
shouldAttach(ownership.concepts, concept.parentConcept, domainKey)
|
|
144
|
+
) {
|
|
145
|
+
if (pushUnique(collections.concepts, concept.parentConcept)) {
|
|
146
|
+
pendingConcepts.push(concept.parentConcept);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const endpoint of collections.endpoints) {
|
|
151
|
+
if (referencesConcept(endpoint, concept)) {
|
|
152
|
+
includeEndpoint({
|
|
153
|
+
domainKey,
|
|
154
|
+
endpoint,
|
|
155
|
+
ownership,
|
|
156
|
+
collections,
|
|
157
|
+
pendingConcepts,
|
|
158
|
+
pendingEndpoints,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function includeEndpoint(options: {
|
|
165
|
+
domainKey: string;
|
|
166
|
+
endpoint: EndpointDef;
|
|
167
|
+
ownership: OwnershipMaps;
|
|
168
|
+
collections: DomainCollections;
|
|
169
|
+
pendingConcepts: ConceptDef[];
|
|
170
|
+
pendingEndpoints: EndpointDef[];
|
|
171
|
+
}) {
|
|
172
|
+
const { domainKey, endpoint, ownership, collections, pendingConcepts, pendingEndpoints } =
|
|
173
|
+
options;
|
|
174
|
+
|
|
175
|
+
if (shouldAttach(ownership.endpoints, endpoint, domainKey)) {
|
|
176
|
+
pushUnique(collections.endpoints, endpoint);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
includeRule(domainKey, endpoint.governedBy ?? [], ownership, collections);
|
|
180
|
+
|
|
181
|
+
for (const concept of endpoint.queries ?? []) {
|
|
182
|
+
if (
|
|
183
|
+
shouldAttach(ownership.concepts, concept, domainKey) &&
|
|
184
|
+
pushUnique(collections.concepts, concept)
|
|
185
|
+
) {
|
|
186
|
+
pendingConcepts.push(concept);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const concept of endpoint.mutates ?? []) {
|
|
191
|
+
if (
|
|
192
|
+
shouldAttach(ownership.concepts, concept, domainKey) &&
|
|
193
|
+
pushUnique(collections.concepts, concept)
|
|
194
|
+
) {
|
|
195
|
+
pendingConcepts.push(concept);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const returned of endpoint.returns ?? []) {
|
|
200
|
+
if (
|
|
201
|
+
shouldAttach(ownership.concepts, returned.concept, domainKey) &&
|
|
202
|
+
pushUnique(collections.concepts, returned.concept)
|
|
203
|
+
) {
|
|
204
|
+
pendingConcepts.push(returned.concept);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const dependency of endpoint.dependsOn ?? []) {
|
|
209
|
+
if (
|
|
210
|
+
shouldAttach(ownership.endpoints, dependency.endpoint, domainKey) &&
|
|
211
|
+
pushUnique(collections.endpoints, dependency.endpoint)
|
|
212
|
+
) {
|
|
213
|
+
pendingEndpoints.push(dependency.endpoint);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function includeService(options: {
|
|
219
|
+
domainKey: string;
|
|
220
|
+
service: ServiceDef;
|
|
221
|
+
ownership: OwnershipMaps;
|
|
222
|
+
collections: DomainCollections;
|
|
223
|
+
pendingConcepts: ConceptDef[];
|
|
224
|
+
}) {
|
|
225
|
+
const { domainKey, service, ownership, collections, pendingConcepts } = options;
|
|
226
|
+
|
|
227
|
+
if (shouldAttach(ownership.services, service, domainKey)) {
|
|
228
|
+
pushUnique(collections.services, service);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
includeRule(domainKey, service.governedBy ?? [], ownership, collections);
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
shouldAttach(ownership.concepts, service.belongsTo, domainKey) &&
|
|
235
|
+
pushUnique(collections.concepts, service.belongsTo)
|
|
236
|
+
) {
|
|
237
|
+
pendingConcepts.push(service.belongsTo);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function includeRule(
|
|
242
|
+
domainKey: string,
|
|
243
|
+
rules: readonly RuleDef[],
|
|
244
|
+
ownership: OwnershipMaps,
|
|
245
|
+
collections: DomainCollections,
|
|
246
|
+
) {
|
|
247
|
+
for (const rule of rules) {
|
|
248
|
+
if (shouldAttach(ownership.rules, rule, domainKey)) {
|
|
249
|
+
pushUnique(collections.rules, rule);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function shouldAttach<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
|
|
255
|
+
const owner = owners.get(value);
|
|
256
|
+
return owner == null || owner === domainKey;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pushUnique<T extends object>(values: T[], value: T) {
|
|
260
|
+
if (values.includes(value)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
values.push(value);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function referencesConcept(endpoint: EndpointDef, concept: ConceptDef) {
|
|
269
|
+
return (
|
|
270
|
+
endpoint.queries?.includes(concept) ||
|
|
271
|
+
endpoint.mutates?.includes(concept) ||
|
|
272
|
+
endpoint.returns?.some(function (returned) {
|
|
273
|
+
return returned.concept === concept;
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -31,9 +31,12 @@ export function generateCypher(data: DomainDef) {
|
|
|
31
31
|
const edges: string[] = [];
|
|
32
32
|
|
|
33
33
|
// -- Domain node --
|
|
34
|
+
const domainMeta = data.metadata
|
|
35
|
+
? `,\n d.metadata = '${jsonStr(data.metadata)}'`
|
|
36
|
+
: "";
|
|
34
37
|
nodes.push(
|
|
35
38
|
`MERGE (d:Domain {name: '${esc(data.name)}'})
|
|
36
|
-
SET d.description = '${esc(data.description)}'`,
|
|
39
|
+
SET d.description = '${esc(data.description)}'${domainMeta}`,
|
|
37
40
|
);
|
|
38
41
|
|
|
39
42
|
// -- Concept nodes --
|
|
@@ -42,10 +45,11 @@ export function generateCypher(data: DomainDef) {
|
|
|
42
45
|
c.aliases && c.aliases.length > 0
|
|
43
46
|
? `, c.aliases = [${c.aliases.map((a) => `'${esc(a)}'`).join(", ")}]`
|
|
44
47
|
: "";
|
|
48
|
+
const conceptMeta = c.metadata ? `, c.metadata = '${jsonStr(c.metadata)}'` : "";
|
|
45
49
|
|
|
46
50
|
nodes.push(
|
|
47
51
|
`MERGE (c:Concept {name: '${esc(c.name)}'})
|
|
48
|
-
SET c.description = '${esc(c.description)}'${aliasClause}`,
|
|
52
|
+
SET c.description = '${esc(c.description)}'${aliasClause}${conceptMeta}`,
|
|
49
53
|
);
|
|
50
54
|
|
|
51
55
|
edges.push(
|
|
@@ -73,6 +77,9 @@ export function generateCypher(data: DomainDef) {
|
|
|
73
77
|
|
|
74
78
|
// -- Endpoint nodes --
|
|
75
79
|
for (const ep of data.endpoints ?? []) {
|
|
80
|
+
const endpointMeta = ep.metadata
|
|
81
|
+
? `,\n e.metadata = '${jsonStr(ep.metadata)}'`
|
|
82
|
+
: "";
|
|
76
83
|
nodes.push(
|
|
77
84
|
`MERGE (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
78
85
|
SET e.name = '${esc(ep.name)}',
|
|
@@ -82,7 +89,7 @@ export function generateCypher(data: DomainDef) {
|
|
|
82
89
|
e.response = '${jsonStr(ep.response)}',
|
|
83
90
|
e.propertiesDescriptions = '${jsonStr(ep.propertiesDescriptions)}',
|
|
84
91
|
e.successStatus = ${ep.successStatus},
|
|
85
|
-
e.errorStatuses = [${ep.errorStatuses.join(", ")}]`,
|
|
92
|
+
e.errorStatuses = [${ep.errorStatuses.join(", ")}]${endpointMeta}`,
|
|
86
93
|
);
|
|
87
94
|
|
|
88
95
|
for (const concept of ep.queries ?? []) {
|
|
@@ -131,10 +138,13 @@ export function generateCypher(data: DomainDef) {
|
|
|
131
138
|
|
|
132
139
|
// -- Service nodes --
|
|
133
140
|
for (const svc of data.services ?? []) {
|
|
141
|
+
const serviceMeta = svc.metadata
|
|
142
|
+
? `,\n s.metadata = '${jsonStr(svc.metadata)}'`
|
|
143
|
+
: "";
|
|
134
144
|
nodes.push(
|
|
135
145
|
`MERGE (s:Service {builtInId: '${esc(svc.builtInId)}'})
|
|
136
146
|
SET s.name = '${esc(svc.name)}',
|
|
137
|
-
s.description = '${esc(svc.description)}'`,
|
|
147
|
+
s.description = '${esc(svc.description)}'${serviceMeta}`,
|
|
138
148
|
);
|
|
139
149
|
|
|
140
150
|
edges.push(
|
|
@@ -154,9 +164,12 @@ export function generateCypher(data: DomainDef) {
|
|
|
154
164
|
|
|
155
165
|
// -- Rule nodes --
|
|
156
166
|
for (const rule of data.rules ?? []) {
|
|
167
|
+
const ruleMeta = rule.metadata
|
|
168
|
+
? `,\n r.metadata = '${jsonStr(rule.metadata)}'`
|
|
169
|
+
: "";
|
|
157
170
|
nodes.push(
|
|
158
171
|
`MERGE (r:Rule {name: '${esc(rule.name)}'})
|
|
159
|
-
SET r.description = '${esc(rule.description)}'`,
|
|
172
|
+
SET r.description = '${esc(rule.description)}'${ruleMeta}`,
|
|
160
173
|
);
|
|
161
174
|
}
|
|
162
175
|
|
package/src/graph/helpers.ts
CHANGED
package/src/graph/resolver.ts
CHANGED
|
@@ -50,6 +50,7 @@ export type ResolvedEndpoint = {
|
|
|
50
50
|
response: string;
|
|
51
51
|
dependencies: EndpointDependency[];
|
|
52
52
|
rules: string[];
|
|
53
|
+
metadata: string;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
export type ResolvedService = {
|
|
@@ -58,6 +59,7 @@ export type ResolvedService = {
|
|
|
58
59
|
builtInId: string;
|
|
59
60
|
description: string;
|
|
60
61
|
rules: string[];
|
|
62
|
+
metadata: string;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
export type ResolvedContext = {
|
|
@@ -79,6 +81,7 @@ type EndpointRow = {
|
|
|
79
81
|
params: string | null;
|
|
80
82
|
body: string | null;
|
|
81
83
|
response: string | null;
|
|
84
|
+
metadata: string | null;
|
|
82
85
|
dependencies: (EndpointDependency | { depPath: null })[];
|
|
83
86
|
rules: (string | null)[];
|
|
84
87
|
};
|
|
@@ -88,6 +91,7 @@ type ServiceRow = {
|
|
|
88
91
|
serviceName: string;
|
|
89
92
|
builtInId: string;
|
|
90
93
|
description: string | null;
|
|
94
|
+
metadata: string | null;
|
|
91
95
|
rules: (string | null)[];
|
|
92
96
|
};
|
|
93
97
|
|
|
@@ -158,6 +162,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
158
162
|
e.params AS params,
|
|
159
163
|
e.body AS body,
|
|
160
164
|
e.response AS response,
|
|
165
|
+
e.metadata AS metadata,
|
|
161
166
|
dependencies,
|
|
162
167
|
collect(DISTINCT rEndpoint.description) +
|
|
163
168
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
@@ -177,6 +182,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
177
182
|
params: row.params ?? "[]",
|
|
178
183
|
body: row.body ?? "[]",
|
|
179
184
|
response: row.response ?? "[]",
|
|
185
|
+
metadata: row.metadata ?? "{}",
|
|
180
186
|
dependencies: row.dependencies.filter(
|
|
181
187
|
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
182
188
|
),
|
|
@@ -198,6 +204,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
198
204
|
svc.name AS serviceName,
|
|
199
205
|
svc.builtInId AS builtInId,
|
|
200
206
|
svc.description AS description,
|
|
207
|
+
svc.metadata AS metadata,
|
|
201
208
|
collect(DISTINCT rService.description) +
|
|
202
209
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
203
210
|
{ names: conceptNames },
|
|
@@ -210,6 +217,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
210
217
|
serviceName: row.serviceName,
|
|
211
218
|
builtInId: row.builtInId,
|
|
212
219
|
description: row.description ?? "",
|
|
220
|
+
metadata: row.metadata ?? "{}",
|
|
213
221
|
rules: row.rules.filter((rule): rule is string => Boolean(rule)),
|
|
214
222
|
};
|
|
215
223
|
}),
|
|
@@ -239,6 +247,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
239
247
|
e.params AS params,
|
|
240
248
|
e.body AS body,
|
|
241
249
|
e.response AS response,
|
|
250
|
+
e.metadata AS metadata,
|
|
242
251
|
dependencies,
|
|
243
252
|
collect(DISTINCT rEndpoint.description) +
|
|
244
253
|
collect(DISTINCT rConcept.description) AS rules`,
|
|
@@ -259,6 +268,7 @@ export async function resolveFromGraph(prompt: string, config: ResolverConfig) {
|
|
|
259
268
|
params: row.params ?? "[]",
|
|
260
269
|
body: row.body ?? "[]",
|
|
261
270
|
response: row.response ?? "[]",
|
|
271
|
+
metadata: row.metadata ?? "{}",
|
|
262
272
|
dependencies: row.dependencies.filter(
|
|
263
273
|
(dep): dep is EndpointDependency => dep.depPath != null,
|
|
264
274
|
),
|
package/src/graph/seed.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { EmbeddingModel } from "ai";
|
|
|
10
10
|
import { embed } from "ai";
|
|
11
11
|
import type { Neo4jClient, Neo4jConfig } from "./neo4j.ts";
|
|
12
12
|
import { createNeo4jClient } from "./neo4j.ts";
|
|
13
|
+
import { expandDomains } from "./expand-domains.ts";
|
|
13
14
|
import { generateCypher, type GeneratedCypher } from "./generate-cypher.ts";
|
|
14
15
|
import type { DomainDef } from "./types.ts";
|
|
15
16
|
import { validateDomain } from "./validate.ts";
|
|
@@ -122,8 +123,10 @@ async function generateEmbeddings(client: Neo4jClient, embeddingConfig: Embeddin
|
|
|
122
123
|
// ---------------------------------------------------------------------------
|
|
123
124
|
|
|
124
125
|
export async function seedGraph(config: SeedGraphConfig, domains: Record<string, DomainDef>) {
|
|
126
|
+
const expandedDomains = expandDomains(domains);
|
|
127
|
+
|
|
125
128
|
// Validate all domains
|
|
126
|
-
const validationErrors = Object.entries(
|
|
129
|
+
const validationErrors = Object.entries(expandedDomains).flatMap(([name, domain]) =>
|
|
127
130
|
validateDomain(domain).map((err) => `[${name}] ${err}`),
|
|
128
131
|
);
|
|
129
132
|
|
|
@@ -133,7 +136,7 @@ export async function seedGraph(config: SeedGraphConfig, domains: Record<string,
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
const client = createNeo4jClient(config.neo4j, config.embedding.model);
|
|
136
|
-
const targets = Object.entries(
|
|
139
|
+
const targets = Object.entries(expandedDomains);
|
|
137
140
|
|
|
138
141
|
console.log(`Seeding ${targets.length} domain(s)...`);
|
|
139
142
|
|
package/src/graph/types.ts
CHANGED
|
@@ -52,6 +52,7 @@ export type ConceptDef = {
|
|
|
52
52
|
aliases?: string[];
|
|
53
53
|
parentConcept?: ConceptDef;
|
|
54
54
|
governedBy?: RuleDef[];
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +85,7 @@ export type EndpointDef = {
|
|
|
84
85
|
}[];
|
|
85
86
|
|
|
86
87
|
governedBy?: RuleDef[];
|
|
88
|
+
metadata?: Record<string, unknown>;
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
// ---------------------------------------------------------------------------
|
|
@@ -112,6 +114,7 @@ export type EndpointInput = {
|
|
|
112
114
|
}[];
|
|
113
115
|
|
|
114
116
|
governedBy?: RuleDef[];
|
|
117
|
+
metadata?: Record<string, unknown>;
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
// ---------------------------------------------------------------------------
|
|
@@ -125,6 +128,7 @@ export type ServiceDef = {
|
|
|
125
128
|
builtInId: string;
|
|
126
129
|
belongsTo: ConceptDef;
|
|
127
130
|
governedBy?: RuleDef[];
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
128
132
|
};
|
|
129
133
|
|
|
130
134
|
// ---------------------------------------------------------------------------
|
|
@@ -135,6 +139,7 @@ export type RuleDef = {
|
|
|
135
139
|
readonly __brand: "rule";
|
|
136
140
|
name: string;
|
|
137
141
|
description: string;
|
|
142
|
+
metadata?: Record<string, unknown>;
|
|
138
143
|
};
|
|
139
144
|
|
|
140
145
|
// ---------------------------------------------------------------------------
|
|
@@ -149,4 +154,5 @@ export type DomainDef = {
|
|
|
149
154
|
endpoints?: EndpointDef[];
|
|
150
155
|
services?: ServiceDef[];
|
|
151
156
|
rules?: RuleDef[];
|
|
157
|
+
metadata?: Record<string, unknown>;
|
|
152
158
|
};
|
package/src/index.ts
CHANGED
package/src/routes/chat.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { HTTPException } from "hono/http-exception";
|
|
|
5
5
|
import type { CortexAppEnv } from "../types.ts";
|
|
6
6
|
import { requireAuth } from "../auth/middleware.ts";
|
|
7
7
|
import { stream } from "../ai/index.ts";
|
|
8
|
+
import { subscribe, abortStream } from "../ai/active-streams.ts";
|
|
9
|
+
import { notify } from "../ws/index.ts";
|
|
10
|
+
import { toThreadSummary } from "../types.ts";
|
|
8
11
|
|
|
9
12
|
export function createChatRoutes() {
|
|
10
13
|
const app = new Hono<CortexAppEnv>();
|
|
@@ -21,18 +24,60 @@ export function createChatRoutes() {
|
|
|
21
24
|
),
|
|
22
25
|
async function (c) {
|
|
23
26
|
const config = c.get("agentConfig");
|
|
27
|
+
const agentId = c.get("agentId");
|
|
24
28
|
const { id: userId, token } = c.get("user");
|
|
25
29
|
const { messages, id: threadId } = c.req.valid("json");
|
|
26
30
|
|
|
27
31
|
const thread = await config.db.threads.getById(userId, threadId);
|
|
28
32
|
|
|
29
|
-
if (!thread) {
|
|
33
|
+
if (!thread || thread.agentId !== agentId) {
|
|
30
34
|
throw new HTTPException(404, { message: "Not found" });
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
return stream(messages, thread, userId, token, config
|
|
37
|
+
return stream(messages, thread, userId, token, config);
|
|
34
38
|
},
|
|
35
39
|
);
|
|
36
40
|
|
|
41
|
+
app.get("/chat/:chatId/stream", requireAuth, async function (c) {
|
|
42
|
+
const config = c.get("agentConfig");
|
|
43
|
+
const agentId = c.get("agentId");
|
|
44
|
+
const chatId = c.req.param("chatId");
|
|
45
|
+
const thread = await config.db.threads.getById(c.get("user").id, chatId);
|
|
46
|
+
if (!thread || thread.agentId !== agentId) {
|
|
47
|
+
throw new HTTPException(404, { message: "Not found" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const subscriberStream = subscribe(chatId);
|
|
51
|
+
if (!subscriberStream) {
|
|
52
|
+
return c.body(null, 204);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Response(subscriberStream.pipeThrough(new TextEncoderStream()), {
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "text/event-stream",
|
|
58
|
+
"Cache-Control": "no-cache",
|
|
59
|
+
Connection: "keep-alive",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
app.post("/chat/:chatId/abort", requireAuth, async function (c) {
|
|
65
|
+
const config = c.get("agentConfig");
|
|
66
|
+
const agentId = c.get("agentId");
|
|
67
|
+
const userId = c.get("user").id;
|
|
68
|
+
const chatId = c.req.param("chatId");
|
|
69
|
+
const thread = await config.db.threads.getById(userId, chatId);
|
|
70
|
+
if (!thread || thread.agentId !== agentId) {
|
|
71
|
+
throw new HTTPException(404, { message: "Not found" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
abortStream(chatId);
|
|
75
|
+
notify(userId, agentId, {
|
|
76
|
+
type: "thread:run-finished",
|
|
77
|
+
payload: { thread: toThreadSummary(thread, false) },
|
|
78
|
+
});
|
|
79
|
+
return c.body(null, 204);
|
|
80
|
+
});
|
|
81
|
+
|
|
37
82
|
return app;
|
|
38
83
|
}
|
package/src/routes/threads.ts
CHANGED
|
@@ -4,6 +4,9 @@ import z from "zod";
|
|
|
4
4
|
import type { CortexAppEnv } from "../types.ts";
|
|
5
5
|
import { requireAuth } from "../auth/middleware.ts";
|
|
6
6
|
import { generateTitle } from "../ai/index.ts";
|
|
7
|
+
import { abortStream, isStreamRunning } from "../ai/active-streams.ts";
|
|
8
|
+
import { notify } from "../ws/index.ts";
|
|
9
|
+
import { toThreadSummary } from "../types.ts";
|
|
7
10
|
|
|
8
11
|
export function createThreadRoutes() {
|
|
9
12
|
const app = new Hono<CortexAppEnv>();
|
|
@@ -13,7 +16,7 @@ export function createThreadRoutes() {
|
|
|
13
16
|
const agentId = c.get("agentId");
|
|
14
17
|
const threads = await config.db.threads
|
|
15
18
|
.list(c.get("user").id, agentId)
|
|
16
|
-
.then((x) => x.map((y) => (
|
|
19
|
+
.then((x) => x.map((y) => toThreadSummary(y, isStreamRunning(y.id))));
|
|
17
20
|
return c.json(threads);
|
|
18
21
|
});
|
|
19
22
|
|
|
@@ -24,28 +27,55 @@ export function createThreadRoutes() {
|
|
|
24
27
|
async function (c) {
|
|
25
28
|
const config = c.get("agentConfig");
|
|
26
29
|
const agentId = c.get("agentId");
|
|
30
|
+
const userId = c.get("user").id;
|
|
27
31
|
const { prompt } = c.req.valid("json");
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const thread = await config.db.threads.create(userId, agentId);
|
|
33
|
+
|
|
34
|
+
notify(userId, agentId, {
|
|
35
|
+
type: "thread:created",
|
|
36
|
+
payload: { thread: toThreadSummary(thread, false) },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
generateTitle(thread.id, prompt, userId, config);
|
|
40
|
+
|
|
41
|
+
return c.json(toThreadSummary(thread, false));
|
|
34
42
|
},
|
|
35
43
|
);
|
|
36
44
|
|
|
37
45
|
app.delete("/threads/:threadId", requireAuth, async function (c) {
|
|
38
46
|
const config = c.get("agentConfig");
|
|
47
|
+
const agentId = c.get("agentId");
|
|
48
|
+
const userId = c.get("user").id;
|
|
39
49
|
const threadId = c.req.param("threadId");
|
|
40
|
-
|
|
50
|
+
|
|
51
|
+
const thread = await config.db.threads.getById(userId, threadId);
|
|
52
|
+
if (!thread || thread.agentId !== agentId) {
|
|
53
|
+
return c.body(null, 404);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
abortStream(threadId);
|
|
57
|
+
await config.db.threads.delete(userId, threadId);
|
|
58
|
+
|
|
59
|
+
notify(userId, agentId, {
|
|
60
|
+
type: "thread:deleted",
|
|
61
|
+
payload: { threadId },
|
|
62
|
+
});
|
|
63
|
+
|
|
41
64
|
return c.body(null, 204);
|
|
42
65
|
});
|
|
43
66
|
|
|
44
67
|
app.get("/threads/:threadId/messages", requireAuth, async function (c) {
|
|
45
68
|
const config = c.get("agentConfig");
|
|
69
|
+
const agentId = c.get("agentId");
|
|
70
|
+
const userId = c.get("user").id;
|
|
46
71
|
const threadId = c.req.param("threadId");
|
|
72
|
+
const thread = await config.db.threads.getById(userId, threadId);
|
|
73
|
+
if (!thread || thread.agentId !== agentId) {
|
|
74
|
+
return c.body(null, 404);
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
const messages = await config.db.messages
|
|
48
|
-
.list(
|
|
78
|
+
.list(userId, threadId)
|
|
49
79
|
.then((x) => x.map((y) => y.content));
|
|
50
80
|
return c.json(messages);
|
|
51
81
|
});
|
|
@@ -56,8 +86,15 @@ export function createThreadRoutes() {
|
|
|
56
86
|
zValidator("json", z.object({ session: z.record(z.unknown()) })),
|
|
57
87
|
async function (c) {
|
|
58
88
|
const config = c.get("agentConfig");
|
|
89
|
+
const agentId = c.get("agentId");
|
|
90
|
+
const userId = c.get("user").id;
|
|
59
91
|
const threadId = c.req.param("threadId");
|
|
60
92
|
const { session } = c.req.valid("json");
|
|
93
|
+
const thread = await config.db.threads.getById(userId, threadId);
|
|
94
|
+
if (!thread || thread.agentId !== agentId) {
|
|
95
|
+
return c.body(null, 404);
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
await config.db.threads.updateSession(threadId, session);
|
|
62
99
|
return c.body(null, 204);
|
|
63
100
|
},
|