@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.
Files changed (42) hide show
  1. package/README.md +38 -38
  2. package/dist/src/factory.d.ts +13 -1
  3. package/dist/src/ws/index.d.ts +1 -1
  4. package/package.json +54 -54
  5. package/src/adapters/database.ts +21 -28
  6. package/src/adapters/minio.ts +69 -69
  7. package/src/adapters/mssql.ts +171 -195
  8. package/src/adapters/storage.ts +4 -4
  9. package/src/ai/fetch.ts +31 -31
  10. package/src/ai/helpers.ts +18 -22
  11. package/src/ai/index.ts +101 -113
  12. package/src/ai/interceptors/resolve-captured-files.ts +42 -49
  13. package/src/ai/prompt.ts +80 -83
  14. package/src/ai/tools/call-endpoint.tool.ts +75 -82
  15. package/src/ai/tools/capture-files.tool.ts +15 -17
  16. package/src/ai/tools/execute-code.tool.ts +73 -80
  17. package/src/ai/tools/query-graph.tool.ts +17 -17
  18. package/src/auth/middleware.ts +51 -51
  19. package/src/cli/extract-endpoints.ts +436 -474
  20. package/src/config.ts +124 -134
  21. package/src/db/migrate.ts +13 -13
  22. package/src/db/migrations/20260309012148_cloudy_maria_hill/snapshot.json +303 -303
  23. package/src/db/schema.ts +46 -58
  24. package/src/factory.ts +136 -139
  25. package/src/graph/generate-cypher.ts +97 -97
  26. package/src/graph/helpers.ts +37 -37
  27. package/src/graph/index.ts +20 -20
  28. package/src/graph/neo4j.ts +82 -89
  29. package/src/graph/resolver.ts +201 -211
  30. package/src/graph/seed.ts +101 -114
  31. package/src/graph/types.ts +88 -88
  32. package/src/graph/validate.ts +55 -57
  33. package/src/index.ts +5 -5
  34. package/src/routes/chat.ts +23 -23
  35. package/src/routes/files.ts +75 -80
  36. package/src/routes/threads.ts +52 -54
  37. package/src/routes/ws.ts +22 -22
  38. package/src/types.ts +30 -30
  39. package/src/ws/connections.ts +11 -11
  40. package/src/ws/events.ts +2 -2
  41. package/src/ws/index.ts +1 -5
  42. package/src/ws/notify.ts +4 -4
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";
@@ -21,125 +21,113 @@ 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
+ createCapturedFileInterceptor(config.db, config.storage),
73
+ };
74
+
75
+ builtInTools["callEndpoint"] = createCallEndpointTool(backendFetchWithInterceptor, token);
76
+ builtInTools["executeCode"] = createExecuteCodeTool(backendFetchWithInterceptor, token);
77
+ }
78
+
79
+ const tools = {
80
+ ...builtInTools,
81
+ ...config.tools,
82
+ } as ToolSet;
83
+
84
+ const systemPrompt = await buildSystemPrompt(config, prompt, thread, token, resolved);
85
+
86
+ const result = streamText({
87
+ model,
88
+ system: systemPrompt,
89
+ tools,
90
+ messages: recentMessages,
91
+ onAbort: () => {
92
+ console.log("Stream aborted");
93
+ },
94
+ });
95
+
96
+ return result.toUIMessageStreamResponse({
97
+ originalMessages,
98
+ generateMessageId: generateId,
99
+ onFinish: async ({ messages: finishedMessages, isAborted }) => {
100
+ if (isAborted) {
101
+ console.log("Stream was aborted");
102
+ }
103
+ await config.db.messages.upsert(thread.id, finishedMessages);
104
+ config.onStreamFinish?.({ messages: finishedMessages, isAborted });
105
+ },
106
+ });
119
107
  }
120
108
 
121
109
  export async function generateTitle(
122
- threadId: string,
123
- prompt: string,
124
- userId: string,
125
- config: ResolvedCortexAgentConfig,
110
+ threadId: string,
111
+ prompt: string,
112
+ userId: string,
113
+ config: ResolvedCortexAgentConfig,
126
114
  ) {
127
- const model = createModel(config.model);
115
+ const model = createModel(config.model);
128
116
 
129
- const { output } = await generateText({
130
- model,
131
- system: `You are an expert in generating titles for threads of chats
117
+ const { output } = await generateText({
118
+ model,
119
+ system: `You are an expert in generating titles for threads of chats
132
120
  given the first message in that thread.
133
121
 
134
122
  When asked, only respond with the title without saying you're
135
123
  going to do so or any other speech. Spit out only the title.`,
136
- prompt: `Generate a title for this prompt: ${prompt}`,
137
- });
124
+ prompt: `Generate a title for this prompt: ${prompt}`,
125
+ });
138
126
 
139
- await config.db.threads.updateTitle(threadId, output ?? "");
127
+ await config.db.threads.updateTitle(threadId, output ?? "");
140
128
 
141
- notify(userId, {
142
- type: "thread:title-updated",
143
- payload: { threadId, title: output ?? "" },
144
- });
129
+ notify(userId, {
130
+ type: "thread:title-updated",
131
+ payload: { threadId, title: output ?? "" },
132
+ });
145
133
  }
@@ -3,62 +3,55 @@ import type { StorageAdapter } from "../../adapters/storage.ts";
3
3
  import { streamToBase64, tokenToUserId } from "../helpers.ts";
4
4
 
5
5
  export type ResolvedFile = {
6
- name: string;
7
- bytes: string;
6
+ name: string;
7
+ bytes: string;
8
8
  };
9
9
 
10
10
  export function createCapturedFileInterceptor(
11
- db: DatabaseAdapter,
12
- storage: StorageAdapter,
13
- options?: { transformFile?: (file: ResolvedFile) => unknown },
11
+ db: DatabaseAdapter,
12
+ storage: StorageAdapter,
13
+ options?: { transformFile?: (file: ResolvedFile) => unknown },
14
14
  ) {
15
- const transformFile =
16
- options?.transformFile ?? ((file: ResolvedFile) => file);
15
+ const transformFile = options?.transformFile ?? ((file: ResolvedFile) => file);
17
16
 
18
- return async function resolveCapturedFiles(
19
- body: Record<string, unknown>,
20
- context: { token: string },
21
- ) {
22
- const userId = tokenToUserId(context.token);
17
+ return async function resolveCapturedFiles(
18
+ body: Record<string, unknown>,
19
+ context: { token: string },
20
+ ) {
21
+ const userId = tokenToUserId(context.token);
23
22
 
24
- async function traverse(obj: Record<string, unknown>) {
25
- await Promise.all(
26
- Object.keys(obj).map(async (key) => {
27
- const value = obj[key];
28
- if (typeof value === "string" && value.startsWith("capturedFile")) {
29
- const [_, uploadId] = value.split("#");
30
- const file = await db.capturedFiles.getById(uploadId!, userId);
31
- if (file) {
32
- const fileStream = await storage.stream(
33
- `captured_files/${uploadId}`,
34
- );
35
- const bytes = await streamToBase64(fileStream);
36
- obj[key] = transformFile({ name: file.name, bytes });
37
- }
38
- }
39
-
40
- if (Array.isArray(value)) {
23
+ async function traverse(obj: Record<string, unknown>) {
41
24
  await Promise.all(
42
- value.map(async (child) => {
43
- if (typeof child === "object" && child !== null) {
44
- await traverse(child as Record<string, unknown>);
45
- }
46
- }),
47
- );
48
- }
25
+ Object.keys(obj).map(async (key) => {
26
+ const value = obj[key];
27
+ if (typeof value === "string" && value.startsWith("capturedFile")) {
28
+ const [_, uploadId] = value.split("#");
29
+ const file = await db.capturedFiles.getById(uploadId!, userId);
30
+ if (file) {
31
+ const fileStream = await storage.stream(`captured_files/${uploadId}`);
32
+ const bytes = await streamToBase64(fileStream);
33
+ obj[key] = transformFile({ name: file.name, bytes });
34
+ }
35
+ }
49
36
 
50
- if (
51
- typeof value === "object" &&
52
- value !== null &&
53
- !Array.isArray(value)
54
- ) {
55
- await traverse(value as Record<string, unknown>);
56
- }
57
- }),
58
- );
59
- }
37
+ if (Array.isArray(value)) {
38
+ await Promise.all(
39
+ value.map(async (child) => {
40
+ if (typeof child === "object" && child !== null) {
41
+ await traverse(child as Record<string, unknown>);
42
+ }
43
+ }),
44
+ );
45
+ }
46
+
47
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
48
+ await traverse(value as Record<string, unknown>);
49
+ }
50
+ }),
51
+ );
52
+ }
60
53
 
61
- await traverse(body);
62
- return body;
63
- };
54
+ await traverse(body);
55
+ return body;
56
+ };
64
57
  }
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
  }