@cortexmemory/cli 0.27.1 → 0.27.3
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 +771 -144
- 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 +425 -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 +83 -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 +83 -2
- 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 +93 -8
- 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 +113 -16
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +45 -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/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +7 -2
- 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
|
@@ -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,83 @@
|
|
|
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
|
+
export async function POST(req: Request) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await req.json();
|
|
13
|
+
const { username, password } = 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
|
+
const cortex = getCortex();
|
|
31
|
+
const sanitizedUsername = username.toLowerCase();
|
|
32
|
+
|
|
33
|
+
// Get user profile
|
|
34
|
+
const user = await cortex.users.get(sanitizedUsername);
|
|
35
|
+
if (!user) {
|
|
36
|
+
return Response.json(
|
|
37
|
+
{ error: "Invalid username or password" },
|
|
38
|
+
{ status: 401 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verify password
|
|
43
|
+
const storedHash = user.data.passwordHash as string;
|
|
44
|
+
if (!storedHash) {
|
|
45
|
+
return Response.json(
|
|
46
|
+
{ error: "Invalid username or password" },
|
|
47
|
+
{ status: 401 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isValid = await verifyPassword(password, storedHash);
|
|
52
|
+
if (!isValid) {
|
|
53
|
+
return Response.json(
|
|
54
|
+
{ error: "Invalid username or password" },
|
|
55
|
+
{ status: 401 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Update last login time
|
|
60
|
+
await cortex.users.update(sanitizedUsername, {
|
|
61
|
+
lastLoginAt: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Generate session token
|
|
65
|
+
const sessionToken = generateSessionToken();
|
|
66
|
+
|
|
67
|
+
return Response.json({
|
|
68
|
+
success: true,
|
|
69
|
+
user: {
|
|
70
|
+
id: sanitizedUsername,
|
|
71
|
+
displayName: (user.data.displayName as string) || sanitizedUsername,
|
|
72
|
+
},
|
|
73
|
+
sessionToken,
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("[Login Error]", error);
|
|
77
|
+
|
|
78
|
+
return Response.json(
|
|
79
|
+
{ error: "Failed to authenticate" },
|
|
80
|
+
{ status: 500 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -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,10 +106,31 @@ 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
|
+
|
|
104
125
|
export async function POST(req: Request) {
|
|
105
126
|
try {
|
|
106
127
|
const body = await req.json();
|
|
107
|
-
const { messages, memorySpaceId, userId } = body;
|
|
128
|
+
const { messages, memorySpaceId, userId, conversationId: providedConversationId } = body;
|
|
129
|
+
|
|
130
|
+
// Generate conversation ID if not provided (new chat)
|
|
131
|
+
const conversationId = providedConversationId ||
|
|
132
|
+
`conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
133
|
+
const isNewConversation = !providedConversationId;
|
|
108
134
|
|
|
109
135
|
// Convert UIMessage[] from useChat to ModelMessage[] for streamText
|
|
110
136
|
// Note: In AI SDK v6+, convertToModelMessages may return a Promise
|
|
@@ -114,6 +140,28 @@ export async function POST(req: Request) {
|
|
|
114
140
|
? await modelMessagesResult
|
|
115
141
|
: modelMessagesResult;
|
|
116
142
|
|
|
143
|
+
// Get the first user message for title generation
|
|
144
|
+
// AI SDK v5+ uses `parts` array instead of `content` string
|
|
145
|
+
const firstUserMessage = messages.find((m: { role: string }) => m.role === "user") as {
|
|
146
|
+
role: string;
|
|
147
|
+
content?: string;
|
|
148
|
+
parts?: Array<{ type: string; text?: string }>;
|
|
149
|
+
} | undefined;
|
|
150
|
+
|
|
151
|
+
let messageText = "";
|
|
152
|
+
if (firstUserMessage) {
|
|
153
|
+
if (typeof firstUserMessage.content === "string") {
|
|
154
|
+
// Legacy format: content is a string
|
|
155
|
+
messageText = firstUserMessage.content;
|
|
156
|
+
} else if (firstUserMessage.parts && Array.isArray(firstUserMessage.parts)) {
|
|
157
|
+
// AI SDK v5+ format: extract text from parts array
|
|
158
|
+
messageText = firstUserMessage.parts
|
|
159
|
+
.filter((part) => part.type === "text" && part.text)
|
|
160
|
+
.map((part) => part.text)
|
|
161
|
+
.join("");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
117
165
|
// Use createUIMessageStream to send both LLM text and layer events
|
|
118
166
|
return createUIMessageStreamResponse({
|
|
119
167
|
stream: createUIMessageStream({
|
|
@@ -157,10 +205,11 @@ export async function POST(req: Request) {
|
|
|
157
205
|
},
|
|
158
206
|
};
|
|
159
207
|
|
|
160
|
-
// Build config with the observer
|
|
208
|
+
// Build config with the observer and conversation ID
|
|
161
209
|
const config = getCortexMemoryConfig(
|
|
162
210
|
memorySpaceId || "quickstart-demo",
|
|
163
211
|
userId || "demo-user",
|
|
212
|
+
conversationId,
|
|
164
213
|
layerObserver,
|
|
165
214
|
);
|
|
166
215
|
|
|
@@ -177,6 +226,38 @@ export async function POST(req: Request) {
|
|
|
177
226
|
|
|
178
227
|
// Merge LLM stream into the UI message stream
|
|
179
228
|
writer.merge(result.toUIMessageStream());
|
|
229
|
+
|
|
230
|
+
// If this is a new conversation, create it in the SDK and update the title
|
|
231
|
+
if (isNewConversation && messageText) {
|
|
232
|
+
try {
|
|
233
|
+
const cortex = getCortex();
|
|
234
|
+
const title = generateTitle(messageText);
|
|
235
|
+
|
|
236
|
+
// Create the conversation with the SDK
|
|
237
|
+
await cortex.conversations.create({
|
|
238
|
+
memorySpaceId: memorySpaceId || "quickstart-demo",
|
|
239
|
+
conversationId,
|
|
240
|
+
type: "user-agent",
|
|
241
|
+
participants: {
|
|
242
|
+
userId: userId || "demo-user",
|
|
243
|
+
agentId: "quickstart-assistant",
|
|
244
|
+
},
|
|
245
|
+
metadata: { title },
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Send conversation update to the client
|
|
249
|
+
writer.write({
|
|
250
|
+
type: "data-conversation-update",
|
|
251
|
+
data: {
|
|
252
|
+
conversationId,
|
|
253
|
+
title,
|
|
254
|
+
},
|
|
255
|
+
transient: true,
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("Failed to create conversation:", error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
180
261
|
},
|
|
181
262
|
}),
|
|
182
263
|
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversations API Route
|
|
3
|
+
*
|
|
4
|
+
* GET: List conversations for a user (chat history)
|
|
5
|
+
* POST: Create a new conversation
|
|
6
|
+
* DELETE: Delete a conversation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getCortex } from "@/lib/cortex";
|
|
10
|
+
|
|
11
|
+
export async function GET(req: Request) {
|
|
12
|
+
try {
|
|
13
|
+
const { searchParams } = new URL(req.url);
|
|
14
|
+
const conversationId = searchParams.get("conversationId");
|
|
15
|
+
const userId = searchParams.get("userId");
|
|
16
|
+
const memorySpaceId = searchParams.get("memorySpaceId") || "quickstart-demo";
|
|
17
|
+
|
|
18
|
+
const cortex = getCortex();
|
|
19
|
+
|
|
20
|
+
// If conversationId is provided, fetch single conversation with messages
|
|
21
|
+
if (conversationId) {
|
|
22
|
+
const conversation = await cortex.conversations.get(conversationId, {
|
|
23
|
+
includeMessages: true,
|
|
24
|
+
messageLimit: 100,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!conversation) {
|
|
28
|
+
return Response.json(
|
|
29
|
+
{ error: "Conversation not found" },
|
|
30
|
+
{ status: 404 }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Transform messages to the format expected by AI SDK useChat
|
|
35
|
+
const messages = (conversation.messages || []).map((msg) => ({
|
|
36
|
+
id: msg.id,
|
|
37
|
+
role: msg.role as "user" | "assistant",
|
|
38
|
+
content: msg.content,
|
|
39
|
+
createdAt: new Date(msg.timestamp),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
return Response.json({
|
|
43
|
+
conversation: {
|
|
44
|
+
id: conversation.conversationId,
|
|
45
|
+
title: (conversation.metadata?.title as string) || getDefaultTitle(conversation),
|
|
46
|
+
createdAt: conversation.createdAt,
|
|
47
|
+
updatedAt: conversation.updatedAt,
|
|
48
|
+
messageCount: conversation.messageCount || 0,
|
|
49
|
+
},
|
|
50
|
+
messages,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// List conversations for user (requires userId)
|
|
55
|
+
if (!userId) {
|
|
56
|
+
return Response.json(
|
|
57
|
+
{ error: "userId is required" },
|
|
58
|
+
{ status: 400 }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get conversations for the user
|
|
63
|
+
const result = await cortex.conversations.list({
|
|
64
|
+
memorySpaceId,
|
|
65
|
+
userId,
|
|
66
|
+
limit: 50,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Map conversations to a simpler format for the UI
|
|
70
|
+
const conversations = result.conversations.map((conv) => ({
|
|
71
|
+
id: conv.conversationId,
|
|
72
|
+
title: (conv.metadata?.title as string) || getDefaultTitle(conv),
|
|
73
|
+
createdAt: conv.createdAt,
|
|
74
|
+
updatedAt: conv.updatedAt,
|
|
75
|
+
messageCount: conv.messageCount || 0,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// Sort by updatedAt descending (most recent first)
|
|
79
|
+
conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
80
|
+
|
|
81
|
+
return Response.json({ conversations });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("[Conversations Error]", error);
|
|
84
|
+
|
|
85
|
+
return Response.json(
|
|
86
|
+
{ error: "Failed to fetch conversations" },
|
|
87
|
+
{ status: 500 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function POST(req: Request) {
|
|
93
|
+
try {
|
|
94
|
+
const body = await req.json();
|
|
95
|
+
const { userId, memorySpaceId = "quickstart-demo", title } = body;
|
|
96
|
+
|
|
97
|
+
if (!userId) {
|
|
98
|
+
return Response.json(
|
|
99
|
+
{ error: "userId is required" },
|
|
100
|
+
{ status: 400 }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cortex = getCortex();
|
|
105
|
+
|
|
106
|
+
// Create a new conversation
|
|
107
|
+
const conversationId = `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
108
|
+
|
|
109
|
+
const conversation = await cortex.conversations.create({
|
|
110
|
+
memorySpaceId,
|
|
111
|
+
conversationId,
|
|
112
|
+
type: "user-agent",
|
|
113
|
+
participants: {
|
|
114
|
+
userId,
|
|
115
|
+
agentId: "quickstart-assistant",
|
|
116
|
+
},
|
|
117
|
+
metadata: {
|
|
118
|
+
title: title || "New Chat",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return Response.json({
|
|
123
|
+
success: true,
|
|
124
|
+
conversation: {
|
|
125
|
+
id: conversation.conversationId,
|
|
126
|
+
title: (conversation.metadata?.title as string) || "New Chat",
|
|
127
|
+
createdAt: conversation.createdAt,
|
|
128
|
+
updatedAt: conversation.updatedAt,
|
|
129
|
+
messageCount: 0,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("[Conversation Create Error]", error);
|
|
134
|
+
|
|
135
|
+
return Response.json(
|
|
136
|
+
{ error: "Failed to create conversation" },
|
|
137
|
+
{ status: 500 }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function DELETE(req: Request) {
|
|
143
|
+
try {
|
|
144
|
+
const { searchParams } = new URL(req.url);
|
|
145
|
+
const conversationId = searchParams.get("conversationId");
|
|
146
|
+
|
|
147
|
+
if (!conversationId) {
|
|
148
|
+
return Response.json(
|
|
149
|
+
{ error: "conversationId is required" },
|
|
150
|
+
{ status: 400 }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cortex = getCortex();
|
|
155
|
+
|
|
156
|
+
await cortex.conversations.delete(conversationId);
|
|
157
|
+
|
|
158
|
+
return Response.json({ success: true });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("[Conversation Delete Error]", error);
|
|
161
|
+
|
|
162
|
+
return Response.json(
|
|
163
|
+
{ error: "Failed to delete conversation" },
|
|
164
|
+
{ status: 500 }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate a default title from conversation data
|
|
171
|
+
*/
|
|
172
|
+
function getDefaultTitle(conv: { createdAt: number; messageCount?: number }): string {
|
|
173
|
+
const date = new Date(conv.createdAt);
|
|
174
|
+
const timeStr = date.toLocaleTimeString("en-US", {
|
|
175
|
+
hour: "numeric",
|
|
176
|
+
minute: "2-digit",
|
|
177
|
+
});
|
|
178
|
+
return `Chat at ${timeStr}`;
|
|
179
|
+
}
|