@m6d/cortex-server 1.1.1 → 1.2.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 +38 -38
- package/dist/src/ai/interceptors/{resolve-captured-files.d.ts → request-interceptor.d.ts} +3 -2
- package/dist/src/config.d.ts +6 -3
- package/dist/src/factory.d.ts +13 -1
- package/dist/src/index.d.ts +2 -0
- 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 +167 -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 +106 -114
- package/src/ai/interceptors/request-interceptor.ts +61 -0
- 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 +128 -135
- 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 +12 -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/ai/interceptors/resolve-captured-files.ts +0 -64
package/src/ai/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
type ToolSet,
|
|
3
|
+
convertToModelMessages,
|
|
4
|
+
generateId,
|
|
5
|
+
generateText,
|
|
6
|
+
safeValidateUIMessages,
|
|
7
|
+
streamText,
|
|
8
8
|
} from "ai";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
10
|
import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
@@ -15,131 +15,123 @@ import { createQueryGraphTool } from "./tools/query-graph.tool.ts";
|
|
|
15
15
|
import { createCallEndpointTool } from "./tools/call-endpoint.tool.ts";
|
|
16
16
|
import { createExecuteCodeTool } from "./tools/execute-code.tool.ts";
|
|
17
17
|
import { captureFilesTool } from "./tools/capture-files.tool.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { createRequestInterceptor } from "./interceptors/request-interceptor.ts";
|
|
19
19
|
import { createNeo4jClient } from "../graph/neo4j.ts";
|
|
20
20
|
import { resolveFromGraph } from "../graph/resolver.ts";
|
|
21
21
|
import { notify } from "../ws/index.ts";
|
|
22
22
|
|
|
23
23
|
export async function stream(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
messages: unknown[],
|
|
25
|
+
thread: Thread,
|
|
26
|
+
userId: string,
|
|
27
|
+
token: string,
|
|
28
|
+
config: ResolvedCortexAgentConfig,
|
|
29
29
|
) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
if (config.backendFetch) {
|
|
68
|
-
const backendFetchWithInterceptor = {
|
|
69
|
-
...config.backendFetch,
|
|
70
|
-
transformRequestBody:
|
|
71
|
-
config.backendFetch.transformRequestBody ??
|
|
72
|
-
createCapturedFileInterceptor(config.db, config.storage),
|
|
30
|
+
const validationResult = await safeValidateUIMessages({ messages });
|
|
31
|
+
if (!validationResult.success) {
|
|
32
|
+
throw new HTTPException(423, { message: "Invalid messages format" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const validatedMessages = validationResult.data;
|
|
36
|
+
await config.db.messages.upsert(thread.id, validatedMessages);
|
|
37
|
+
|
|
38
|
+
const originalMessages = await config.db.messages
|
|
39
|
+
.list(userId, thread.id, { limit: 20 })
|
|
40
|
+
.then((x) => x.map((y) => y.content));
|
|
41
|
+
|
|
42
|
+
const recentMessages = await convertToModelMessages(originalMessages);
|
|
43
|
+
|
|
44
|
+
const prompt =
|
|
45
|
+
originalMessages
|
|
46
|
+
.filter((x) => x.role === "user")
|
|
47
|
+
.at(-1)
|
|
48
|
+
?.parts.find((x) => x.type === "text")?.text ?? "";
|
|
49
|
+
|
|
50
|
+
const model = createModel(config.model);
|
|
51
|
+
const embeddingModel = createEmbeddingModel(config.embedding);
|
|
52
|
+
const neo4j = createNeo4jClient(config.neo4j, embeddingModel);
|
|
53
|
+
|
|
54
|
+
// Pre-resolve graph context
|
|
55
|
+
const resolved = await resolveFromGraph(prompt, {
|
|
56
|
+
neo4j,
|
|
57
|
+
embeddingModel,
|
|
58
|
+
reranker: config.reranker,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Build tools
|
|
62
|
+
const builtInTools: Record<string, unknown> = {
|
|
63
|
+
captureFiles: captureFilesTool,
|
|
64
|
+
queryGraph: createQueryGraphTool(neo4j),
|
|
73
65
|
};
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
67
|
+
if (config.backendFetch) {
|
|
68
|
+
const backendFetchWithInterceptor = {
|
|
69
|
+
...config.backendFetch,
|
|
70
|
+
transformRequestBody:
|
|
71
|
+
config.backendFetch.transformRequestBody ??
|
|
72
|
+
createRequestInterceptor(
|
|
73
|
+
config.db,
|
|
74
|
+
config.storage,
|
|
75
|
+
config.backendFetch.interceptor,
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
builtInTools["callEndpoint"] = createCallEndpointTool(backendFetchWithInterceptor, token);
|
|
80
|
+
builtInTools["executeCode"] = createExecuteCodeTool(backendFetchWithInterceptor, token);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tools = {
|
|
84
|
+
...builtInTools,
|
|
85
|
+
...config.tools,
|
|
86
|
+
} as ToolSet;
|
|
87
|
+
|
|
88
|
+
const systemPrompt = await buildSystemPrompt(config, prompt, thread, token, resolved);
|
|
89
|
+
|
|
90
|
+
const result = streamText({
|
|
91
|
+
model,
|
|
92
|
+
system: systemPrompt,
|
|
93
|
+
tools,
|
|
94
|
+
messages: recentMessages,
|
|
95
|
+
onAbort: () => {
|
|
96
|
+
console.log("Stream aborted");
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return result.toUIMessageStreamResponse({
|
|
101
|
+
originalMessages,
|
|
102
|
+
generateMessageId: generateId,
|
|
103
|
+
onFinish: async ({ messages: finishedMessages, isAborted }) => {
|
|
104
|
+
if (isAborted) {
|
|
105
|
+
console.log("Stream was aborted");
|
|
106
|
+
}
|
|
107
|
+
await config.db.messages.upsert(thread.id, finishedMessages);
|
|
108
|
+
config.onStreamFinish?.({ messages: finishedMessages, isAborted });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
119
111
|
}
|
|
120
112
|
|
|
121
113
|
export async function generateTitle(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
threadId: string,
|
|
115
|
+
prompt: string,
|
|
116
|
+
userId: string,
|
|
117
|
+
config: ResolvedCortexAgentConfig,
|
|
126
118
|
) {
|
|
127
|
-
|
|
119
|
+
const model = createModel(config.model);
|
|
128
120
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
const { output } = await generateText({
|
|
122
|
+
model,
|
|
123
|
+
system: `You are an expert in generating titles for threads of chats
|
|
132
124
|
given the first message in that thread.
|
|
133
125
|
|
|
134
126
|
When asked, only respond with the title without saying you're
|
|
135
127
|
going to do so or any other speech. Spit out only the title.`,
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
prompt: `Generate a title for this prompt: ${prompt}`,
|
|
129
|
+
});
|
|
138
130
|
|
|
139
|
-
|
|
131
|
+
await config.db.threads.updateTitle(threadId, output ?? "");
|
|
140
132
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
133
|
+
notify(userId, {
|
|
134
|
+
type: "thread:title-updated",
|
|
135
|
+
payload: { threadId, title: output ?? "" },
|
|
136
|
+
});
|
|
145
137
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from "../../adapters/database.ts";
|
|
2
|
+
import type { StorageAdapter } from "../../adapters/storage.ts";
|
|
3
|
+
import { streamToBase64, tokenToUserId } from "../helpers.ts";
|
|
4
|
+
|
|
5
|
+
export type ResolvedFile = {
|
|
6
|
+
name: string;
|
|
7
|
+
bytes: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RequestInterceptorOptions = {
|
|
11
|
+
transformFile?: (file: ResolvedFile) => unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createRequestInterceptor(
|
|
15
|
+
db: DatabaseAdapter,
|
|
16
|
+
storage: StorageAdapter,
|
|
17
|
+
options?: RequestInterceptorOptions,
|
|
18
|
+
) {
|
|
19
|
+
const transformFile = options?.transformFile ?? ((file: ResolvedFile) => file);
|
|
20
|
+
|
|
21
|
+
return async function resolveRequestBody(
|
|
22
|
+
body: Record<string, unknown>,
|
|
23
|
+
context: { token: string },
|
|
24
|
+
) {
|
|
25
|
+
const userId = tokenToUserId(context.token);
|
|
26
|
+
|
|
27
|
+
async function traverse(obj: Record<string, unknown>) {
|
|
28
|
+
await Promise.all(
|
|
29
|
+
Object.keys(obj).map(async (key) => {
|
|
30
|
+
const value = obj[key];
|
|
31
|
+
if (typeof value === "string" && value.startsWith("capturedFile")) {
|
|
32
|
+
const [_, uploadId] = value.split("#");
|
|
33
|
+
const file = await db.capturedFiles.getById(uploadId!, userId);
|
|
34
|
+
if (file) {
|
|
35
|
+
const fileStream = await storage.stream(`captured_files/${uploadId}`);
|
|
36
|
+
const bytes = await streamToBase64(fileStream);
|
|
37
|
+
obj[key] = transformFile({ name: file.name, bytes });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
await Promise.all(
|
|
43
|
+
value.map(async (child) => {
|
|
44
|
+
if (typeof child === "object" && child !== null) {
|
|
45
|
+
await traverse(child as Record<string, unknown>);
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
52
|
+
await traverse(value as Record<string, unknown>);
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await traverse(body);
|
|
59
|
+
return body;
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/ai/prompt.ts
CHANGED
|
@@ -3,118 +3,115 @@ import type { ResolvedCortexAgentConfig } from "../config.ts";
|
|
|
3
3
|
import type { Thread } from "../types.ts";
|
|
4
4
|
|
|
5
5
|
export async function buildSystemPrompt(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
config: ResolvedCortexAgentConfig,
|
|
7
|
+
prompt: string,
|
|
8
|
+
thread: Thread,
|
|
9
|
+
token: string,
|
|
10
|
+
resolved: ResolvedContext | null,
|
|
11
11
|
) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
// Resolve the consumer's base system prompt
|
|
13
|
+
let basePrompt: string;
|
|
14
|
+
if (typeof config.systemPrompt === "function") {
|
|
15
|
+
// Resolve session data with caching
|
|
16
|
+
let session: Record<string, unknown> | null = thread.session as Record<
|
|
17
|
+
string,
|
|
18
|
+
unknown
|
|
19
|
+
> | null;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
if (!session && config.loadSessionData) {
|
|
22
|
+
session = await config.loadSessionData(token);
|
|
23
|
+
// Persist to DB for future cache hits
|
|
24
|
+
await config.db.threads.updateSession(thread.id, session);
|
|
25
|
+
thread.session = session;
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
basePrompt = await config.systemPrompt(session);
|
|
29
|
+
} else {
|
|
30
|
+
basePrompt = config.systemPrompt;
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const sections: string[] = [basePrompt];
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Pre-resolved endpoints from knowledge graph
|
|
36
|
+
if (resolved) {
|
|
37
|
+
sections.push(buildResolvedSection(resolved));
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
return sections.join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function buildResolvedSection(resolved: ResolvedContext) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const parts: string[] = [
|
|
45
|
+
`
|
|
46
46
|
## Pre-resolved API Endpoints
|
|
47
47
|
The following endpoints were automatically matched to the user's message.`,
|
|
48
|
-
|
|
48
|
+
];
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
50
|
+
for (const ep of resolved.readEndpoints) {
|
|
51
|
+
const rules = ep.rules.length > 0 ? `\n Rules: ${ep.rules.join("; ")}` : "";
|
|
52
|
+
const deps =
|
|
53
|
+
ep.dependencies.length > 0
|
|
54
|
+
? "\n Dependencies:\n" +
|
|
55
|
+
ep.dependencies
|
|
56
|
+
.map(
|
|
57
|
+
(d) =>
|
|
58
|
+
` - Call ${d.depMethod} ${d.depPath} first → use its "${d.fromField}" as "${d.paramName}"`,
|
|
59
|
+
)
|
|
60
|
+
.join("\n")
|
|
61
|
+
: "";
|
|
62
|
+
parts.push(
|
|
63
|
+
`
|
|
65
64
|
### ${ep.concept} (read)
|
|
66
65
|
- Endpoint: ${ep.name}
|
|
67
66
|
- ${ep.method} ${ep.path}
|
|
68
67
|
- Params: ${ep.params}
|
|
69
68
|
- Body: ${ep.body}
|
|
70
69
|
- Response: ${ep.response}${rules}${deps}`,
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
);
|
|
71
|
+
}
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
73
|
+
for (const ep of resolved.writeEndpoints) {
|
|
74
|
+
const rules = ep.rules.length > 0 ? `\n Rules: ${ep.rules.join("; ")}` : "";
|
|
75
|
+
const deps =
|
|
76
|
+
ep.dependencies.length > 0
|
|
77
|
+
? "\n Dependencies:\n" +
|
|
78
|
+
ep.dependencies
|
|
79
|
+
.map(
|
|
80
|
+
(d) =>
|
|
81
|
+
` - Call ${d.depMethod} ${d.depPath} first → use its "${d.fromField}" as "${d.paramName}"`,
|
|
82
|
+
)
|
|
83
|
+
.join("\n")
|
|
84
|
+
: "";
|
|
85
|
+
parts.push(
|
|
86
|
+
`
|
|
89
87
|
### ${ep.concept} (write)
|
|
90
88
|
- Endpoint: ${ep.name}
|
|
91
89
|
- ${ep.method} ${ep.path}
|
|
92
90
|
- Params: ${ep.params}
|
|
93
91
|
- Body: ${ep.body}
|
|
94
92
|
- Response: ${ep.response}${rules}${deps}`,
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
);
|
|
94
|
+
}
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`
|
|
96
|
+
for (const svc of resolved.services) {
|
|
97
|
+
const rules = svc.rules.length > 0 ? `\n Rules: ${svc.rules.join("; ")}` : "";
|
|
98
|
+
parts.push(
|
|
99
|
+
`
|
|
103
100
|
### ${svc.concept} via ${svc.serviceName} (service)
|
|
104
101
|
- Built-in ID: ${svc.builtInId}
|
|
105
102
|
- Description: ${svc.description || "N/A"}${rules}`,
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
);
|
|
104
|
+
}
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
if (
|
|
107
|
+
resolved.readEndpoints.length === 0 &&
|
|
108
|
+
resolved.writeEndpoints.length === 0 &&
|
|
109
|
+
resolved.services.length === 0
|
|
110
|
+
) {
|
|
111
|
+
parts.push(
|
|
112
|
+
"\nNo matching endpoints found. Use queryGraph to search the knowledge graph manually.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
118
115
|
|
|
119
|
-
|
|
116
|
+
return parts.join("");
|
|
120
117
|
}
|