@cortexmemory/cli 0.27.1 → 0.27.4
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/commands/convex.js +1 -1
- package/dist/commands/convex.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +839 -141
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +89 -26
- package/dist/commands/dev.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/app-template-sync.d.ts +95 -0
- package/dist/utils/app-template-sync.d.ts.map +1 -0
- package/dist/utils/app-template-sync.js +445 -0
- package/dist/utils/app-template-sync.js.map +1 -0
- package/dist/utils/deployment-selector.d.ts +21 -0
- package/dist/utils/deployment-selector.d.ts.map +1 -1
- package/dist/utils/deployment-selector.js +32 -0
- package/dist/utils/deployment-selector.js.map +1 -1
- package/dist/utils/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +13 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +128 -0
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +139 -3
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +333 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/globals.css +161 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +110 -11
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +117 -17
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +52 -0
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +11 -2
- package/templates/vercel-ai-quickstart/test-api.mjs +272 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +454 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
- package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
- package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Check API Route
|
|
3
|
+
*
|
|
4
|
+
* GET: Check if admin has been set up (first-run detection)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCortex } from "@/lib/cortex";
|
|
8
|
+
|
|
9
|
+
const ADMIN_NAMESPACE = "quickstart-config";
|
|
10
|
+
const ADMIN_KEY = "admin_password_hash";
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
try {
|
|
14
|
+
const cortex = getCortex();
|
|
15
|
+
|
|
16
|
+
// Check if admin password hash exists in mutable store
|
|
17
|
+
const adminHash = await cortex.mutable.get(ADMIN_NAMESPACE, ADMIN_KEY);
|
|
18
|
+
|
|
19
|
+
return Response.json({
|
|
20
|
+
isSetup: adminHash !== null,
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("[Auth Check Error]", error);
|
|
24
|
+
|
|
25
|
+
return Response.json(
|
|
26
|
+
{ error: "Failed to check admin setup status" },
|
|
27
|
+
{ status: 500 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Login API Route
|
|
3
|
+
*
|
|
4
|
+
* POST: Authenticate user and return session
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCortex } from "@/lib/cortex";
|
|
8
|
+
import { verifyPassword, generateSessionToken } from "@/lib/password";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates login request body structure.
|
|
12
|
+
* Returns validated credentials or null if invalid.
|
|
13
|
+
*/
|
|
14
|
+
function validateLoginBody(
|
|
15
|
+
body: unknown
|
|
16
|
+
): { username: string; password: string } | null {
|
|
17
|
+
if (typeof body !== "object" || body === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const record = body as Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
// Validate username field exists and is a non-empty string
|
|
24
|
+
const hasValidUsername =
|
|
25
|
+
"username" in record &&
|
|
26
|
+
typeof record.username === "string" &&
|
|
27
|
+
record.username.length > 0 &&
|
|
28
|
+
record.username.length <= 256;
|
|
29
|
+
|
|
30
|
+
// Validate password field exists and is a non-empty string
|
|
31
|
+
const hasValidPassword =
|
|
32
|
+
"password" in record &&
|
|
33
|
+
typeof record.password === "string" &&
|
|
34
|
+
record.password.length > 0 &&
|
|
35
|
+
record.password.length <= 1024;
|
|
36
|
+
|
|
37
|
+
if (!hasValidUsername || !hasValidPassword) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
username: record.username as string,
|
|
43
|
+
password: record.password as string,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Safely extracts an error message for logging without exposing user data.
|
|
49
|
+
*/
|
|
50
|
+
function getSafeErrorMessage(error: unknown): string {
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
// Only include error name and a sanitized message
|
|
53
|
+
// Avoid logging full stack traces which may contain user data
|
|
54
|
+
return `${error.name}: ${error.message.slice(0, 200)}`;
|
|
55
|
+
}
|
|
56
|
+
return "Unknown error";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function POST(req: Request) {
|
|
60
|
+
try {
|
|
61
|
+
const body = await req.json();
|
|
62
|
+
|
|
63
|
+
// Validate input structure before extracting values
|
|
64
|
+
const credentials = validateLoginBody(body);
|
|
65
|
+
if (!credentials) {
|
|
66
|
+
return Response.json(
|
|
67
|
+
{ error: "Username and password are required" },
|
|
68
|
+
{ status: 400 }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { username, password } = credentials;
|
|
73
|
+
|
|
74
|
+
const cortex = getCortex();
|
|
75
|
+
const sanitizedUsername = username.toLowerCase();
|
|
76
|
+
|
|
77
|
+
// Get user profile
|
|
78
|
+
const user = await cortex.users.get(sanitizedUsername);
|
|
79
|
+
if (!user) {
|
|
80
|
+
return Response.json(
|
|
81
|
+
{ error: "Invalid username or password" },
|
|
82
|
+
{ status: 401 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Verify password
|
|
87
|
+
const storedHash = user.data.passwordHash as string;
|
|
88
|
+
if (!storedHash) {
|
|
89
|
+
return Response.json(
|
|
90
|
+
{ error: "Invalid username or password" },
|
|
91
|
+
{ status: 401 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isValid = await verifyPassword(password, storedHash);
|
|
96
|
+
if (!isValid) {
|
|
97
|
+
return Response.json(
|
|
98
|
+
{ error: "Invalid username or password" },
|
|
99
|
+
{ status: 401 }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update last login time
|
|
104
|
+
await cortex.users.update(sanitizedUsername, {
|
|
105
|
+
lastLoginAt: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Generate session token
|
|
109
|
+
const sessionToken = generateSessionToken();
|
|
110
|
+
|
|
111
|
+
return Response.json({
|
|
112
|
+
success: true,
|
|
113
|
+
user: {
|
|
114
|
+
id: sanitizedUsername,
|
|
115
|
+
displayName: (user.data.displayName as string) || sanitizedUsername,
|
|
116
|
+
},
|
|
117
|
+
sessionToken,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Log sanitized error to prevent log injection
|
|
121
|
+
console.error("[Login Error]", getSafeErrorMessage(error));
|
|
122
|
+
|
|
123
|
+
return Response.json(
|
|
124
|
+
{ error: "Failed to authenticate" },
|
|
125
|
+
{ status: 500 }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Registration API Route
|
|
3
|
+
*
|
|
4
|
+
* POST: Register a new user account
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCortex } from "@/lib/cortex";
|
|
8
|
+
import { hashPassword, generateSessionToken } from "@/lib/password";
|
|
9
|
+
|
|
10
|
+
export async function POST(req: Request) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await req.json();
|
|
13
|
+
const { username, password, displayName } = body;
|
|
14
|
+
|
|
15
|
+
// Validate input
|
|
16
|
+
if (!username || typeof username !== "string") {
|
|
17
|
+
return Response.json(
|
|
18
|
+
{ error: "Username is required" },
|
|
19
|
+
{ status: 400 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!password || typeof password !== "string") {
|
|
24
|
+
return Response.json(
|
|
25
|
+
{ error: "Password is required" },
|
|
26
|
+
{ status: 400 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (username.length < 2) {
|
|
31
|
+
return Response.json(
|
|
32
|
+
{ error: "Username must be at least 2 characters" },
|
|
33
|
+
{ status: 400 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (password.length < 4) {
|
|
38
|
+
return Response.json(
|
|
39
|
+
{ error: "Password must be at least 4 characters" },
|
|
40
|
+
{ status: 400 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Sanitize username (alphanumeric, underscore, hyphen only)
|
|
45
|
+
const sanitizedUsername = username.toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
46
|
+
if (sanitizedUsername !== username.toLowerCase()) {
|
|
47
|
+
return Response.json(
|
|
48
|
+
{ error: "Username can only contain letters, numbers, underscores, and hyphens" },
|
|
49
|
+
{ status: 400 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cortex = getCortex();
|
|
54
|
+
|
|
55
|
+
// Check if user already exists
|
|
56
|
+
const existingUser = await cortex.users.get(sanitizedUsername);
|
|
57
|
+
if (existingUser) {
|
|
58
|
+
return Response.json(
|
|
59
|
+
{ error: "Username already taken" },
|
|
60
|
+
{ status: 409 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Hash password and create user profile
|
|
65
|
+
const passwordHash = await hashPassword(password);
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
|
|
68
|
+
await cortex.users.update(sanitizedUsername, {
|
|
69
|
+
displayName: displayName || sanitizedUsername,
|
|
70
|
+
passwordHash,
|
|
71
|
+
createdAt: now,
|
|
72
|
+
lastLoginAt: now,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Generate session token
|
|
76
|
+
const sessionToken = generateSessionToken();
|
|
77
|
+
|
|
78
|
+
return Response.json({
|
|
79
|
+
success: true,
|
|
80
|
+
user: {
|
|
81
|
+
id: sanitizedUsername,
|
|
82
|
+
displayName: displayName || sanitizedUsername,
|
|
83
|
+
},
|
|
84
|
+
sessionToken,
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("[Register Error]", error);
|
|
88
|
+
|
|
89
|
+
return Response.json(
|
|
90
|
+
{ error: "Failed to register user" },
|
|
91
|
+
{ status: 500 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Setup API Route
|
|
3
|
+
*
|
|
4
|
+
* POST: Set up initial admin password (first-run only)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCortex } from "@/lib/cortex";
|
|
8
|
+
import { hashPassword } from "@/lib/password";
|
|
9
|
+
|
|
10
|
+
const ADMIN_NAMESPACE = "quickstart-config";
|
|
11
|
+
const ADMIN_KEY = "admin_password_hash";
|
|
12
|
+
|
|
13
|
+
export async function POST(req: Request) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const { password } = body;
|
|
17
|
+
|
|
18
|
+
if (!password || typeof password !== "string") {
|
|
19
|
+
return Response.json(
|
|
20
|
+
{ error: "Password is required" },
|
|
21
|
+
{ status: 400 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (password.length < 4) {
|
|
26
|
+
return Response.json(
|
|
27
|
+
{ error: "Password must be at least 4 characters" },
|
|
28
|
+
{ status: 400 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cortex = getCortex();
|
|
33
|
+
|
|
34
|
+
// Check if admin already exists
|
|
35
|
+
const existingHash = await cortex.mutable.get(ADMIN_NAMESPACE, ADMIN_KEY);
|
|
36
|
+
if (existingHash !== null) {
|
|
37
|
+
return Response.json(
|
|
38
|
+
{ error: "Admin already configured" },
|
|
39
|
+
{ status: 409 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hash and store admin password
|
|
44
|
+
const passwordHash = await hashPassword(password);
|
|
45
|
+
await cortex.mutable.set(ADMIN_NAMESPACE, ADMIN_KEY, passwordHash);
|
|
46
|
+
|
|
47
|
+
return Response.json({
|
|
48
|
+
success: true,
|
|
49
|
+
message: "Admin password configured successfully",
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("[Admin Setup Error]", error);
|
|
53
|
+
|
|
54
|
+
return Response.json(
|
|
55
|
+
{ error: "Failed to configure admin password" },
|
|
56
|
+
{ status: 500 }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createUIMessageStream,
|
|
12
12
|
createUIMessageStreamResponse,
|
|
13
13
|
} from "ai";
|
|
14
|
+
import { getCortex } from "@/lib/cortex";
|
|
14
15
|
|
|
15
16
|
// Create OpenAI client for embeddings
|
|
16
17
|
const openaiClient = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
@@ -40,6 +41,7 @@ Example interactions:
|
|
|
40
41
|
function getCortexMemoryConfig(
|
|
41
42
|
memorySpaceId: string,
|
|
42
43
|
userId: string,
|
|
44
|
+
conversationId: string,
|
|
43
45
|
layerObserver?: LayerObserver,
|
|
44
46
|
): CortexMemoryConfig {
|
|
45
47
|
return {
|
|
@@ -54,6 +56,9 @@ function getCortexMemoryConfig(
|
|
|
54
56
|
agentId: "quickstart-assistant",
|
|
55
57
|
agentName: "Cortex Demo Assistant",
|
|
56
58
|
|
|
59
|
+
// Conversation ID for chat history isolation
|
|
60
|
+
conversationId,
|
|
61
|
+
|
|
57
62
|
// Enable graph memory sync (auto-configured via env vars)
|
|
58
63
|
// When true, uses CypherGraphAdapter to sync to Neo4j/Memgraph
|
|
59
64
|
enableGraphMemory: process.env.CORTEX_GRAPH_SYNC === "true",
|
|
@@ -101,19 +106,117 @@ function getCortexMemoryConfig(
|
|
|
101
106
|
};
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Generate a title from the first user message
|
|
111
|
+
*/
|
|
112
|
+
function generateTitle(message: string): string {
|
|
113
|
+
// Take first 50 chars, cut at word boundary
|
|
114
|
+
let title = message.slice(0, 50);
|
|
115
|
+
if (message.length > 50) {
|
|
116
|
+
const lastSpace = title.lastIndexOf(" ");
|
|
117
|
+
if (lastSpace > 20) {
|
|
118
|
+
title = title.slice(0, lastSpace);
|
|
119
|
+
}
|
|
120
|
+
title += "...";
|
|
121
|
+
}
|
|
122
|
+
return title;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize messages to ensure they have the `parts` array format
|
|
127
|
+
* expected by AI SDK v6's convertToModelMessages.
|
|
128
|
+
*
|
|
129
|
+
* Handles:
|
|
130
|
+
* - Messages with `content` string (legacy format) -> converts to `parts` array
|
|
131
|
+
* - Messages with `role: "agent"` -> converts to `role: "assistant"`
|
|
132
|
+
* - Messages already in v6 format -> passes through unchanged
|
|
133
|
+
*/
|
|
134
|
+
function normalizeMessages(messages: unknown[]): unknown[] {
|
|
135
|
+
return messages.map((msg: unknown) => {
|
|
136
|
+
const m = msg as Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
// Normalize role: "agent" -> "assistant"
|
|
139
|
+
let role = m.role as string;
|
|
140
|
+
if (role === "agent") {
|
|
141
|
+
role = "assistant";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Ensure parts array exists
|
|
145
|
+
let parts = m.parts as Array<{ type: string; text?: string }> | undefined;
|
|
146
|
+
if (!parts) {
|
|
147
|
+
// Convert content string to parts array
|
|
148
|
+
const content = m.content as string | undefined;
|
|
149
|
+
if (content) {
|
|
150
|
+
parts = [{ type: "text", text: content }];
|
|
151
|
+
} else {
|
|
152
|
+
parts = [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...m,
|
|
158
|
+
role,
|
|
159
|
+
parts,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract text from a message (handles both content string and parts array)
|
|
166
|
+
*/
|
|
167
|
+
function getMessageText(message: { content?: string; parts?: Array<{ type: string; text?: string }> }): string {
|
|
168
|
+
if (typeof message.content === "string") {
|
|
169
|
+
return message.content;
|
|
170
|
+
}
|
|
171
|
+
if (message.parts && Array.isArray(message.parts)) {
|
|
172
|
+
return message.parts
|
|
173
|
+
.filter((part) => part.type === "text" && part.text)
|
|
174
|
+
.map((part) => part.text)
|
|
175
|
+
.join("");
|
|
176
|
+
}
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
|
|
104
180
|
export async function POST(req: Request) {
|
|
105
181
|
try {
|
|
106
182
|
const body = await req.json();
|
|
107
|
-
const { messages, memorySpaceId, userId } = body;
|
|
183
|
+
const { messages, memorySpaceId, userId, conversationId: providedConversationId } = body;
|
|
184
|
+
|
|
185
|
+
// Validate messages array exists
|
|
186
|
+
if (!messages || !Array.isArray(messages)) {
|
|
187
|
+
return new Response(
|
|
188
|
+
JSON.stringify({ error: "messages array is required" }),
|
|
189
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generate conversation ID if not provided (new chat)
|
|
194
|
+
const conversationId = providedConversationId ||
|
|
195
|
+
`conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
196
|
+
const isNewConversation = !providedConversationId;
|
|
197
|
+
|
|
198
|
+
// Normalize messages to ensure they have the `parts` array format
|
|
199
|
+
// expected by AI SDK v6's convertToModelMessages
|
|
200
|
+
const normalizedMessages = normalizeMessages(messages);
|
|
108
201
|
|
|
109
202
|
// Convert UIMessage[] from useChat to ModelMessage[] for streamText
|
|
110
203
|
// Note: In AI SDK v6+, convertToModelMessages may return a Promise
|
|
111
|
-
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
205
|
+
const modelMessagesResult = convertToModelMessages(normalizedMessages as any);
|
|
112
206
|
const modelMessages =
|
|
113
207
|
modelMessagesResult instanceof Promise
|
|
114
208
|
? await modelMessagesResult
|
|
115
209
|
: modelMessagesResult;
|
|
116
210
|
|
|
211
|
+
// Get the first user message for title generation
|
|
212
|
+
const firstUserMessage = messages.find((m: { role: string }) => m.role === "user") as {
|
|
213
|
+
role: string;
|
|
214
|
+
content?: string;
|
|
215
|
+
parts?: Array<{ type: string; text?: string }>;
|
|
216
|
+
} | undefined;
|
|
217
|
+
|
|
218
|
+
const messageText = firstUserMessage ? getMessageText(firstUserMessage) : "";
|
|
219
|
+
|
|
117
220
|
// Use createUIMessageStream to send both LLM text and layer events
|
|
118
221
|
return createUIMessageStreamResponse({
|
|
119
222
|
stream: createUIMessageStream({
|
|
@@ -157,10 +260,11 @@ export async function POST(req: Request) {
|
|
|
157
260
|
},
|
|
158
261
|
};
|
|
159
262
|
|
|
160
|
-
// Build config with the observer
|
|
263
|
+
// Build config with the observer and conversation ID
|
|
161
264
|
const config = getCortexMemoryConfig(
|
|
162
265
|
memorySpaceId || "quickstart-demo",
|
|
163
266
|
userId || "demo-user",
|
|
267
|
+
conversationId,
|
|
164
268
|
layerObserver,
|
|
165
269
|
);
|
|
166
270
|
|
|
@@ -177,6 +281,38 @@ export async function POST(req: Request) {
|
|
|
177
281
|
|
|
178
282
|
// Merge LLM stream into the UI message stream
|
|
179
283
|
writer.merge(result.toUIMessageStream());
|
|
284
|
+
|
|
285
|
+
// If this is a new conversation, create it in the SDK and update the title
|
|
286
|
+
if (isNewConversation && messageText) {
|
|
287
|
+
try {
|
|
288
|
+
const cortex = getCortex();
|
|
289
|
+
const title = generateTitle(messageText);
|
|
290
|
+
|
|
291
|
+
// Create the conversation with the SDK
|
|
292
|
+
await cortex.conversations.create({
|
|
293
|
+
memorySpaceId: memorySpaceId || "quickstart-demo",
|
|
294
|
+
conversationId,
|
|
295
|
+
type: "user-agent",
|
|
296
|
+
participants: {
|
|
297
|
+
userId: userId || "demo-user",
|
|
298
|
+
agentId: "quickstart-assistant",
|
|
299
|
+
},
|
|
300
|
+
metadata: { title },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Send conversation update to the client
|
|
304
|
+
writer.write({
|
|
305
|
+
type: "data-conversation-update",
|
|
306
|
+
data: {
|
|
307
|
+
conversationId,
|
|
308
|
+
title,
|
|
309
|
+
},
|
|
310
|
+
transient: true,
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error("Failed to create conversation:", error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
180
316
|
},
|
|
181
317
|
}),
|
|
182
318
|
});
|