@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
package/src/factory.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { websocket } from "hono/bun";
|
|
3
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
4
|
+
import type { CortexConfig, ResolvedCortexAgentConfig } from "./config.ts";
|
|
5
|
+
import type { AppEnv, CortexAppEnv } from "./types.ts";
|
|
6
|
+
import type { DomainDef } from "./graph/types.ts";
|
|
7
|
+
import { createUserLoaderMiddleware } from "./auth/middleware.ts";
|
|
8
|
+
import { createThreadRoutes } from "./routes/threads.ts";
|
|
9
|
+
import { createChatRoutes } from "./routes/chat.ts";
|
|
10
|
+
import { createFileRoutes } from "./routes/files.ts";
|
|
11
|
+
import { createWsRoute } from "./routes/ws.ts";
|
|
12
|
+
import { runMigrations } from "./db/migrate.ts";
|
|
13
|
+
import { createMssqlAdapter } from "./adapters/mssql.ts";
|
|
14
|
+
import { createMinioAdapter } from "./adapters/minio.ts";
|
|
15
|
+
import { seedGraph as seedGraphFn } from "./graph/seed.ts";
|
|
16
|
+
import { extractEndpoints as extractEndpointsFn } from "./cli/extract-endpoints.ts";
|
|
17
|
+
|
|
18
|
+
export type CortexInstance = {
|
|
19
|
+
serve(): Promise<{
|
|
20
|
+
app: Hono<AppEnv>;
|
|
21
|
+
fetch: Hono<AppEnv>["fetch"];
|
|
22
|
+
websocket: typeof websocket;
|
|
23
|
+
}>;
|
|
24
|
+
seedGraph(): Promise<void>;
|
|
25
|
+
extractEndpoints(options: {
|
|
26
|
+
domainsDir: string;
|
|
27
|
+
write?: boolean;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createCortex(config: CortexConfig): CortexInstance {
|
|
32
|
+
function collectAllDomains() {
|
|
33
|
+
const all: Record<string, DomainDef> = {};
|
|
34
|
+
if (config.knowledge?.domains) Object.assign(all, config.knowledge.domains);
|
|
35
|
+
for (const agent of Object.values(config.agents)) {
|
|
36
|
+
if (agent.knowledge && agent.knowledge.domains) {
|
|
37
|
+
Object.assign(all, agent.knowledge.domains);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return all;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function collectAllSwaggerUrls() {
|
|
44
|
+
const urls = new Set<string>();
|
|
45
|
+
if (config.knowledge?.swagger?.url) urls.add(config.knowledge.swagger.url);
|
|
46
|
+
for (const agent of Object.values(config.agents)) {
|
|
47
|
+
if (agent.knowledge && agent.knowledge.swagger?.url) {
|
|
48
|
+
urls.add(agent.knowledge.swagger.url);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return [...urls];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
async serve() {
|
|
56
|
+
const storage = createMinioAdapter(config.storage);
|
|
57
|
+
const db = createMssqlAdapter(config.database.connectionString, storage);
|
|
58
|
+
|
|
59
|
+
await runMigrations(config.database.connectionString);
|
|
60
|
+
|
|
61
|
+
const app = new Hono<AppEnv>();
|
|
62
|
+
|
|
63
|
+
const userLoader = createUserLoaderMiddleware(config.auth);
|
|
64
|
+
app.use("*", userLoader);
|
|
65
|
+
|
|
66
|
+
// WebSocket route (global, not agent-scoped)
|
|
67
|
+
app.route("/", createWsRoute());
|
|
68
|
+
|
|
69
|
+
// Agent-scoped routes
|
|
70
|
+
const agentApp = new Hono<CortexAppEnv>();
|
|
71
|
+
|
|
72
|
+
agentApp.use("*", async function (c, next) {
|
|
73
|
+
const agentId = c.req.param("agentId") as string;
|
|
74
|
+
const agentDef = config.agents[agentId];
|
|
75
|
+
if (!agentDef) return c.json({ error: "Agent not found" }, 404);
|
|
76
|
+
|
|
77
|
+
const resolvedConfig: ResolvedCortexAgentConfig = {
|
|
78
|
+
db,
|
|
79
|
+
storage,
|
|
80
|
+
model: agentDef.model ?? config.model,
|
|
81
|
+
embedding: agentDef.embedding ?? config.embedding,
|
|
82
|
+
neo4j: agentDef.neo4j ?? config.neo4j,
|
|
83
|
+
reranker: agentDef.reranker ?? config.reranker,
|
|
84
|
+
knowledge:
|
|
85
|
+
agentDef.knowledge === null
|
|
86
|
+
? undefined
|
|
87
|
+
: (agentDef.knowledge ?? config.knowledge),
|
|
88
|
+
systemPrompt: agentDef.systemPrompt,
|
|
89
|
+
tools: agentDef.tools,
|
|
90
|
+
backendFetch: agentDef.backendFetch,
|
|
91
|
+
loadSessionData: agentDef.loadSessionData,
|
|
92
|
+
onToolCall: agentDef.onToolCall,
|
|
93
|
+
onStreamFinish: agentDef.onStreamFinish,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
c.set("agentConfig", resolvedConfig);
|
|
97
|
+
c.set("agentId", agentId);
|
|
98
|
+
await next();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
agentApp.route("/", createThreadRoutes());
|
|
102
|
+
agentApp.route("/", createChatRoutes());
|
|
103
|
+
agentApp.route("/", createFileRoutes());
|
|
104
|
+
|
|
105
|
+
app.route("/agents/:agentId", agentApp);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
app,
|
|
109
|
+
fetch: app.fetch,
|
|
110
|
+
websocket,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async seedGraph() {
|
|
115
|
+
const domains = collectAllDomains();
|
|
116
|
+
if (!Object.keys(domains).length) {
|
|
117
|
+
throw new Error("No domains found in knowledge config");
|
|
118
|
+
}
|
|
119
|
+
if (!config.neo4j) {
|
|
120
|
+
throw new Error("neo4j config is required for seedGraph");
|
|
121
|
+
}
|
|
122
|
+
if (!config.embedding) {
|
|
123
|
+
throw new Error("embedding config is required for seedGraph");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const provider = createOpenAICompatible({
|
|
127
|
+
name: "embedding-provider",
|
|
128
|
+
baseURL: config.embedding.baseURL,
|
|
129
|
+
apiKey: config.embedding.apiKey,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await seedGraphFn(
|
|
133
|
+
{
|
|
134
|
+
neo4j: config.neo4j,
|
|
135
|
+
embedding: {
|
|
136
|
+
model: provider.textEmbeddingModel(config.embedding.modelName),
|
|
137
|
+
dimension: config.embedding.dimension,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
domains,
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async extractEndpoints(options) {
|
|
145
|
+
const swaggerUrls = collectAllSwaggerUrls();
|
|
146
|
+
if (!swaggerUrls.length) {
|
|
147
|
+
throw new Error("No swagger URLs found in knowledge config");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const swaggerUrl of swaggerUrls) {
|
|
151
|
+
await extractEndpointsFn({
|
|
152
|
+
swaggerUrl,
|
|
153
|
+
domainsDir: options.domainsDir,
|
|
154
|
+
write: options.write ?? true,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transforms declarative DomainDef into idempotent MERGE-based Cypher.
|
|
3
|
+
*
|
|
4
|
+
* Returns two phases:
|
|
5
|
+
* - `nodes` -- MERGE statements that create/update Domain, Concept, Endpoint,
|
|
6
|
+
* Service, and Rule nodes.
|
|
7
|
+
* - `edges` -- MATCH+MERGE statements that wire up relationships between nodes.
|
|
8
|
+
*
|
|
9
|
+
* The caller MUST run all `nodes` from every module before running any `edges`,
|
|
10
|
+
* because edges may reference nodes defined in a different module (e.g. a Rule
|
|
11
|
+
* in the leaves module that GOVERNS a Concept created by the servicing module).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { DomainDef } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export type GeneratedCypher = {
|
|
17
|
+
nodes: string[];
|
|
18
|
+
edges: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function esc(s: string) {
|
|
22
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function jsonStr(value: unknown) {
|
|
26
|
+
return esc(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function generateCypher(data: DomainDef) {
|
|
30
|
+
const nodes: string[] = [];
|
|
31
|
+
const edges: string[] = [];
|
|
32
|
+
|
|
33
|
+
// -- Domain node --
|
|
34
|
+
nodes.push(
|
|
35
|
+
`MERGE (d:Domain {name: '${esc(data.name)}'})
|
|
36
|
+
SET d.description = '${esc(data.description)}'`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// -- Concept nodes --
|
|
40
|
+
for (const c of data.concepts ?? []) {
|
|
41
|
+
const aliasClause =
|
|
42
|
+
c.aliases && c.aliases.length > 0
|
|
43
|
+
? `, c.aliases = [${c.aliases.map((a) => `'${esc(a)}'`).join(", ")}]`
|
|
44
|
+
: "";
|
|
45
|
+
|
|
46
|
+
nodes.push(
|
|
47
|
+
`MERGE (c:Concept {name: '${esc(c.name)}'})
|
|
48
|
+
SET c.description = '${esc(c.description)}'${aliasClause}`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
edges.push(
|
|
52
|
+
`MATCH (c:Concept {name: '${esc(c.name)}'})
|
|
53
|
+
MATCH (d:Domain {name: '${esc(data.name)}'})
|
|
54
|
+
MERGE (c)-[:BELONGS_TO]->(d)`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (c.parentConcept) {
|
|
58
|
+
edges.push(
|
|
59
|
+
`MATCH (child:Concept {name: '${esc(c.name)}'})
|
|
60
|
+
MATCH (parent:Concept {name: '${esc(c.parentConcept.name)}'})
|
|
61
|
+
MERGE (child)-[:SPECIALIZES]->(parent)`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
c.governedBy?.forEach((rule) => {
|
|
66
|
+
edges.push(
|
|
67
|
+
`MATCH (r:Rule {name: '${esc(rule.name)}'})
|
|
68
|
+
MATCH (t:Concept {name: '${esc(c.name)}'})
|
|
69
|
+
MERGE (r)-[:GOVERNS]->(t)`,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// -- Endpoint nodes --
|
|
75
|
+
for (const ep of data.endpoints ?? []) {
|
|
76
|
+
nodes.push(
|
|
77
|
+
`MERGE (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
78
|
+
SET e.name = '${esc(ep.name)}',
|
|
79
|
+
e.description = '${esc(ep.description)}',
|
|
80
|
+
e.params = '${jsonStr(ep.params)}',
|
|
81
|
+
e.body = '${jsonStr(ep.body)}',
|
|
82
|
+
e.response = '${jsonStr(ep.response)}',
|
|
83
|
+
e.propertiesDescriptions = '${jsonStr(ep.propertiesDescriptions)}',
|
|
84
|
+
e.successStatus = ${ep.successStatus},
|
|
85
|
+
e.errorStatuses = [${ep.errorStatuses.join(", ")}]`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
for (const concept of ep.queries ?? []) {
|
|
89
|
+
edges.push(
|
|
90
|
+
`MATCH (c:Concept {name: '${esc(concept.name)}'})
|
|
91
|
+
MATCH (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
92
|
+
MERGE (c)-[:QUERIED_VIA]->(e)`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const concept of ep.mutates ?? []) {
|
|
97
|
+
edges.push(
|
|
98
|
+
`MATCH (c:Concept {name: '${esc(concept.name)}'})
|
|
99
|
+
MATCH (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
100
|
+
MERGE (c)-[:MUTATED_VIA]->(e)`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const ret of ep.returns ?? []) {
|
|
105
|
+
const fieldClause = ret.field ? ` SET r.field = '${esc(ret.field)}'` : "";
|
|
106
|
+
|
|
107
|
+
edges.push(
|
|
108
|
+
`MATCH (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
109
|
+
MATCH (c:Concept {name: '${esc(ret.concept.name)}'})
|
|
110
|
+
MERGE (e)-[r:RETURNS]->(c)${fieldClause}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const rule of ep.governedBy ?? []) {
|
|
115
|
+
edges.push(
|
|
116
|
+
`MATCH (r:Rule {name: '${esc(rule.name)}'})
|
|
117
|
+
MATCH (t:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
118
|
+
MERGE (r)-[:GOVERNS]->(t)`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const dep of ep.dependsOn ?? []) {
|
|
123
|
+
edges.push(
|
|
124
|
+
`MATCH (e:Endpoint {path: '${esc(ep.path)}', method: '${ep.method}'})
|
|
125
|
+
MATCH (dep:Endpoint {path: '${esc(dep.endpoint.path)}', method: '${dep.endpoint.method}'})
|
|
126
|
+
MERGE (e)-[d:DEPENDS_ON]->(dep)
|
|
127
|
+
SET d.paramName = '${esc(dep.paramName)}', d.fromField = '${esc(dep.fromField)}'`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// -- Service nodes --
|
|
133
|
+
for (const svc of data.services ?? []) {
|
|
134
|
+
nodes.push(
|
|
135
|
+
`MERGE (s:Service {builtInId: '${esc(svc.builtInId)}'})
|
|
136
|
+
SET s.name = '${esc(svc.name)}',
|
|
137
|
+
s.description = '${esc(svc.description)}'`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
edges.push(
|
|
141
|
+
`MATCH (c:Concept {name: '${esc(svc.belongsTo.name)}'})
|
|
142
|
+
MATCH (s:Service {builtInId: '${esc(svc.builtInId)}'})
|
|
143
|
+
MERGE (c)-[:REQUESTED_VIA]->(s)`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
svc.governedBy?.forEach((rule) => {
|
|
147
|
+
edges.push(
|
|
148
|
+
`MATCH (r:Rule {name: '${esc(rule.name)}'})
|
|
149
|
+
MATCH (t:Service {builtInId: '${esc(svc.builtInId)}'})
|
|
150
|
+
MERGE (r)-[:GOVERNS]->(t)`,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -- Rule nodes --
|
|
156
|
+
for (const rule of data.rules ?? []) {
|
|
157
|
+
nodes.push(
|
|
158
|
+
`MERGE (r:Rule {name: '${esc(rule.name)}'})
|
|
159
|
+
SET r.description = '${esc(rule.description)}'`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { nodes, edges } satisfies GeneratedCypher;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function toCypherScript(data: DomainDef) {
|
|
167
|
+
const { nodes, edges } = generateCypher(data);
|
|
168
|
+
return (
|
|
169
|
+
`// Auto-generated seed script for module: ${data.name}\n` +
|
|
170
|
+
`// Generated at: ${new Date().toISOString()}\n` +
|
|
171
|
+
`// Safe to re-run (uses MERGE)\n\n` +
|
|
172
|
+
`// --- Nodes ---\n` +
|
|
173
|
+
nodes.join(";\n\n") +
|
|
174
|
+
";\n\n" +
|
|
175
|
+
`// --- Edges ---\n` +
|
|
176
|
+
edges.join(";\n\n") +
|
|
177
|
+
";\n"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for building knowledge graph seed data.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ConceptDef,
|
|
7
|
+
DomainDef,
|
|
8
|
+
EndpointDef,
|
|
9
|
+
EndpointInput,
|
|
10
|
+
RuleDef,
|
|
11
|
+
ServiceDef,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Identity helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export function defineConcept(def: Omit<ConceptDef, "__brand">) {
|
|
19
|
+
return { __brand: "concept" as const, ...def };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defineRule(def: Omit<RuleDef, "__brand">) {
|
|
23
|
+
return { __brand: "rule" as const, ...def };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defineService(def: Omit<ServiceDef, "__brand">) {
|
|
27
|
+
return { __brand: "service" as const, ...def };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defineDomain(def: Omit<DomainDef, "__brand">) {
|
|
31
|
+
return { __brand: "domain" as const, ...def };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Endpoint builder
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export function defineEndpoint(input: EndpointInput) {
|
|
39
|
+
const propertiesDescriptions = Object.fromEntries(
|
|
40
|
+
Object.entries({
|
|
41
|
+
...(input.propertiesDescriptions ?? {}),
|
|
42
|
+
...(input.paramDescriptions ?? {}),
|
|
43
|
+
...(input.responseDescriptions ?? {}),
|
|
44
|
+
}).filter(([, value]) => value != null),
|
|
45
|
+
) as Record<string, string>;
|
|
46
|
+
|
|
47
|
+
const normalized = input.autoGenerated;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
__brand: "endpoint" as const,
|
|
51
|
+
name: input.name,
|
|
52
|
+
description: input.description ?? input.name,
|
|
53
|
+
path: input.path,
|
|
54
|
+
method: input.method,
|
|
55
|
+
propertiesDescriptions,
|
|
56
|
+
params: normalized ? [...normalized.params] : [],
|
|
57
|
+
body: normalized ? [...normalized.body] : [],
|
|
58
|
+
response: normalized ? [...normalized.response] : [],
|
|
59
|
+
successStatus: normalized?.successStatus ?? 200,
|
|
60
|
+
errorStatuses: normalized ? [...normalized.errorStatuses] : [],
|
|
61
|
+
responseKind: normalized?.responseKind ?? "object",
|
|
62
|
+
queries: input.queries,
|
|
63
|
+
mutates: input.mutates,
|
|
64
|
+
returns: input.returns,
|
|
65
|
+
dependsOn: input.dependsOn,
|
|
66
|
+
governedBy: input.governedBy,
|
|
67
|
+
} satisfies EndpointDef;
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
EndpointScalarType,
|
|
4
|
+
EndpointProperty,
|
|
5
|
+
ResponseKind,
|
|
6
|
+
AutoGenerated,
|
|
7
|
+
ConceptDef,
|
|
8
|
+
EndpointDef,
|
|
9
|
+
EndpointInput,
|
|
10
|
+
ServiceDef,
|
|
11
|
+
RuleDef,
|
|
12
|
+
DomainDef,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
// Helpers
|
|
16
|
+
export {
|
|
17
|
+
defineConcept,
|
|
18
|
+
defineRule,
|
|
19
|
+
defineService,
|
|
20
|
+
defineDomain,
|
|
21
|
+
defineEndpoint,
|
|
22
|
+
} from "./helpers.ts";
|
|
23
|
+
|
|
24
|
+
// Cypher generation
|
|
25
|
+
export type { GeneratedCypher } from "./generate-cypher.ts";
|
|
26
|
+
export { generateCypher, toCypherScript } from "./generate-cypher.ts";
|
|
27
|
+
|
|
28
|
+
// Validation
|
|
29
|
+
export { validateDomain } from "./validate.ts";
|
|
30
|
+
|
|
31
|
+
// Neo4j client
|
|
32
|
+
export type { Neo4jConfig, Neo4jClient } from "./neo4j.ts";
|
|
33
|
+
export { createNeo4jClient } from "./neo4j.ts";
|
|
34
|
+
|
|
35
|
+
// Seed orchestrator
|
|
36
|
+
export type { EmbeddingConfig, SeedGraphConfig } from "./seed.ts";
|
|
37
|
+
export { seedGraph } from "./seed.ts";
|
|
38
|
+
|
|
39
|
+
// Graph resolver
|
|
40
|
+
export type {
|
|
41
|
+
RerankerConfig,
|
|
42
|
+
ResolverConfig,
|
|
43
|
+
ResolvedEndpoint,
|
|
44
|
+
ResolvedService,
|
|
45
|
+
ResolvedContext,
|
|
46
|
+
} from "./resolver.ts";
|
|
47
|
+
export { resolveFromGraph } from "./resolver.ts";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable Neo4j HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the hardcoded process.env-based client from packages/sample-backend.
|
|
5
|
+
* All configuration is passed explicitly — no environment variables are read.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EmbeddingModel } from "ai";
|
|
9
|
+
import { embed } from "ai";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Config types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export type Neo4jConfig = {
|
|
16
|
+
url: string;
|
|
17
|
+
user: string;
|
|
18
|
+
password: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Neo4jClient = {
|
|
22
|
+
query(cypher: string, parameters?: Record<string, unknown>): Promise<string>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Client factory
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export function createNeo4jClient(
|
|
30
|
+
config: Neo4jConfig,
|
|
31
|
+
embeddingModel?: EmbeddingModel,
|
|
32
|
+
) {
|
|
33
|
+
async function query(cypher: string, parameters?: Record<string, unknown>) {
|
|
34
|
+
// Auto-embed parameters whose keys start with '#'
|
|
35
|
+
const embeddedParams = Object.keys(parameters ?? {}).filter((x) =>
|
|
36
|
+
x.startsWith("#"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (embeddedParams.length) {
|
|
40
|
+
if (!embeddingModel) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"An embedding model is required when using # parameters",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const embeddingResults = await Promise.all(
|
|
47
|
+
embeddedParams.map(
|
|
48
|
+
async (x) =>
|
|
49
|
+
await embed({
|
|
50
|
+
model: embeddingModel,
|
|
51
|
+
value: parameters![x] as string,
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
embeddedParams.forEach((k, idx) => {
|
|
57
|
+
delete parameters![k];
|
|
58
|
+
parameters![k.slice(1)] = embeddingResults[idx]!.embedding;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch(`${config.url}/db/neo4j/tx/commit`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
Authorization: `Basic ${btoa(`${config.user}:${config.password}`)}`,
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
statements: [
|
|
70
|
+
{
|
|
71
|
+
statement: cypher,
|
|
72
|
+
parameters: parameters ?? {},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
error: true,
|
|
81
|
+
status: response.status,
|
|
82
|
+
message: `Neo4j request failed with status ${response.status}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = (await response.json()) as {
|
|
87
|
+
errors?: { message: string }[];
|
|
88
|
+
results: { columns: string[]; data: { row: unknown[] }[] }[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (result.errors && result.errors.length > 0) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
error: true,
|
|
94
|
+
message: result.errors.map((e) => e.message).join("; "),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Transform Neo4j response into a clean array of objects
|
|
99
|
+
const queryResult = result.results[0];
|
|
100
|
+
if (!queryResult) {
|
|
101
|
+
return JSON.stringify([]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { columns, data } = queryResult;
|
|
105
|
+
const rows = data.map((entry) => {
|
|
106
|
+
const obj: Record<string, unknown> = {};
|
|
107
|
+
columns.forEach((col, i) => {
|
|
108
|
+
obj[col] = entry.row[i];
|
|
109
|
+
});
|
|
110
|
+
return obj;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return JSON.stringify(rows);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { query } satisfies Neo4jClient;
|
|
117
|
+
}
|