@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.
Files changed (46) hide show
  1. package/README.md +38 -38
  2. package/dist/src/ai/interceptors/{resolve-captured-files.d.ts → request-interceptor.d.ts} +3 -2
  3. package/dist/src/config.d.ts +6 -3
  4. package/dist/src/factory.d.ts +13 -1
  5. package/dist/src/index.d.ts +2 -0
  6. package/dist/src/ws/index.d.ts +1 -1
  7. package/package.json +54 -54
  8. package/src/adapters/database.ts +21 -28
  9. package/src/adapters/minio.ts +69 -69
  10. package/src/adapters/mssql.ts +167 -195
  11. package/src/adapters/storage.ts +4 -4
  12. package/src/ai/fetch.ts +31 -31
  13. package/src/ai/helpers.ts +18 -22
  14. package/src/ai/index.ts +106 -114
  15. package/src/ai/interceptors/request-interceptor.ts +61 -0
  16. package/src/ai/prompt.ts +80 -83
  17. package/src/ai/tools/call-endpoint.tool.ts +75 -82
  18. package/src/ai/tools/capture-files.tool.ts +15 -17
  19. package/src/ai/tools/execute-code.tool.ts +73 -80
  20. package/src/ai/tools/query-graph.tool.ts +17 -17
  21. package/src/auth/middleware.ts +51 -51
  22. package/src/cli/extract-endpoints.ts +436 -474
  23. package/src/config.ts +128 -135
  24. package/src/db/migrate.ts +13 -13
  25. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
  26. package/src/db/schema.ts +46 -58
  27. package/src/factory.ts +136 -139
  28. package/src/graph/generate-cypher.ts +97 -97
  29. package/src/graph/helpers.ts +37 -37
  30. package/src/graph/index.ts +20 -20
  31. package/src/graph/neo4j.ts +82 -89
  32. package/src/graph/resolver.ts +201 -211
  33. package/src/graph/seed.ts +101 -114
  34. package/src/graph/types.ts +88 -88
  35. package/src/graph/validate.ts +55 -57
  36. package/src/index.ts +12 -5
  37. package/src/routes/chat.ts +23 -23
  38. package/src/routes/files.ts +75 -80
  39. package/src/routes/threads.ts +52 -54
  40. package/src/routes/ws.ts +22 -22
  41. package/src/types.ts +30 -30
  42. package/src/ws/connections.ts +11 -11
  43. package/src/ws/events.ts +2 -2
  44. package/src/ws/index.ts +1 -5
  45. package/src/ws/notify.ts +4 -4
  46. package/src/ai/interceptors/resolve-captured-files.ts +0 -64
package/src/ai/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
- type ToolSet,
3
- convertToModelMessages,
4
- generateId,
5
- generateText,
6
- safeValidateUIMessages,
7
- streamText,
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 { createCapturedFileInterceptor } from "./interceptors/resolve-captured-files.ts";
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
- messages: unknown[],
25
- thread: Thread,
26
- userId: string,
27
- token: string,
28
- config: ResolvedCortexAgentConfig,
24
+ messages: unknown[],
25
+ thread: Thread,
26
+ userId: string,
27
+ token: string,
28
+ config: ResolvedCortexAgentConfig,
29
29
  ) {
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),
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
- builtInTools["callEndpoint"] = createCallEndpointTool(
76
- backendFetchWithInterceptor,
77
- token,
78
- );
79
- builtInTools["executeCode"] = createExecuteCodeTool(
80
- backendFetchWithInterceptor,
81
- token,
82
- );
83
- }
84
-
85
- const tools = {
86
- ...builtInTools,
87
- ...config.tools,
88
- } as ToolSet;
89
-
90
- const systemPrompt = await buildSystemPrompt(
91
- config,
92
- prompt,
93
- thread,
94
- token,
95
- resolved,
96
- );
97
-
98
- const result = streamText({
99
- model,
100
- system: systemPrompt,
101
- tools,
102
- messages: recentMessages,
103
- onAbort: () => {
104
- console.log("Stream aborted");
105
- },
106
- });
107
-
108
- return result.toUIMessageStreamResponse({
109
- originalMessages,
110
- generateMessageId: generateId,
111
- onFinish: async ({ messages: finishedMessages, isAborted }) => {
112
- if (isAborted) {
113
- console.log("Stream was aborted");
114
- }
115
- await config.db.messages.upsert(thread.id, finishedMessages);
116
- config.onStreamFinish?.({ messages: finishedMessages, isAborted });
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
- threadId: string,
123
- prompt: string,
124
- userId: string,
125
- config: ResolvedCortexAgentConfig,
114
+ threadId: string,
115
+ prompt: string,
116
+ userId: string,
117
+ config: ResolvedCortexAgentConfig,
126
118
  ) {
127
- const model = createModel(config.model);
119
+ const model = createModel(config.model);
128
120
 
129
- const { output } = await generateText({
130
- model,
131
- system: `You are an expert in generating titles for threads of chats
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
- prompt: `Generate a title for this prompt: ${prompt}`,
137
- });
128
+ prompt: `Generate a title for this prompt: ${prompt}`,
129
+ });
138
130
 
139
- await config.db.threads.updateTitle(threadId, output ?? "");
131
+ await config.db.threads.updateTitle(threadId, output ?? "");
140
132
 
141
- notify(userId, {
142
- type: "thread:title-updated",
143
- payload: { threadId, title: output ?? "" },
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
- config: ResolvedCortexAgentConfig,
7
- prompt: string,
8
- thread: Thread,
9
- token: string,
10
- resolved: ResolvedContext | null,
6
+ config: ResolvedCortexAgentConfig,
7
+ prompt: string,
8
+ thread: Thread,
9
+ token: string,
10
+ resolved: ResolvedContext | null,
11
11
  ) {
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;
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
- 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
- }
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
- basePrompt = await config.systemPrompt(session);
29
- } else {
30
- basePrompt = config.systemPrompt;
31
- }
28
+ basePrompt = await config.systemPrompt(session);
29
+ } else {
30
+ basePrompt = config.systemPrompt;
31
+ }
32
32
 
33
- const sections: string[] = [basePrompt];
33
+ const sections: string[] = [basePrompt];
34
34
 
35
- // Pre-resolved endpoints from knowledge graph
36
- if (resolved) {
37
- sections.push(buildResolvedSection(resolved));
38
- }
35
+ // Pre-resolved endpoints from knowledge graph
36
+ if (resolved) {
37
+ sections.push(buildResolvedSection(resolved));
38
+ }
39
39
 
40
- return sections.join("\n");
40
+ return sections.join("\n");
41
41
  }
42
42
 
43
43
  function buildResolvedSection(resolved: ResolvedContext) {
44
- const parts: string[] = [
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
- for (const ep of resolved.readEndpoints) {
51
- const rules =
52
- ep.rules.length > 0 ? `\n Rules: ${ep.rules.join("; ")}` : "";
53
- const deps =
54
- ep.dependencies.length > 0
55
- ? "\n Dependencies:\n" +
56
- ep.dependencies
57
- .map(
58
- (d) =>
59
- ` - Call ${d.depMethod} ${d.depPath} first → use its "${d.fromField}" as "${d.paramName}"`,
60
- )
61
- .join("\n")
62
- : "";
63
- parts.push(
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
- for (const ep of resolved.writeEndpoints) {
75
- const rules =
76
- ep.rules.length > 0 ? `\n Rules: ${ep.rules.join("; ")}` : "";
77
- const deps =
78
- ep.dependencies.length > 0
79
- ? "\n Dependencies:\n" +
80
- ep.dependencies
81
- .map(
82
- (d) =>
83
- ` - Call ${d.depMethod} ${d.depPath} first → use its "${d.fromField}" as "${d.paramName}"`,
84
- )
85
- .join("\n")
86
- : "";
87
- parts.push(
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
- for (const svc of resolved.services) {
99
- const rules =
100
- svc.rules.length > 0 ? `\n Rules: ${svc.rules.join("; ")}` : "";
101
- parts.push(
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
- if (
110
- resolved.readEndpoints.length === 0 &&
111
- resolved.writeEndpoints.length === 0 &&
112
- resolved.services.length === 0
113
- ) {
114
- parts.push(
115
- "\nNo matching endpoints found. Use queryGraph to search the knowledge graph manually.",
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
- return parts.join("");
116
+ return parts.join("");
120
117
  }