@cortexmemory/cli 0.26.2 → 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 +210 -36
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +273 -43
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +102 -46
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +94 -7
- package/dist/commands/status.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +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/config.d.ts +11 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +20 -0
- package/dist/utils/config.js.map +1 -1
- 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 +25 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/dist/utils/init/quickstart-setup.d.ts +87 -0
- package/dist/utils/init/quickstart-setup.d.ts.map +1 -0
- package/dist/utils/init/quickstart-setup.js +462 -0
- package/dist/utils/init/quickstart-setup.js.map +1 -0
- package/dist/utils/schema-sync.d.ts.map +1 -1
- package/dist/utils/schema-sync.js +27 -21
- package/dist/utils/schema-sync.js.map +1 -1
- package/package.json +3 -2
- package/templates/vercel-ai-quickstart/.env.local.example +45 -0
- package/templates/vercel-ai-quickstart/README.md +280 -0
- 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 +277 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/api/facts/route.ts +39 -0
- package/templates/vercel-ai-quickstart/app/api/health/route.ts +99 -0
- package/templates/vercel-ai-quickstart/app/api/memories/route.ts +37 -0
- package/templates/vercel-ai-quickstart/app/globals.css +275 -0
- package/templates/vercel-ai-quickstart/app/layout.tsx +19 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +216 -0
- 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 +334 -0
- package/templates/vercel-ai-quickstart/components/ConvexClientProvider.tsx +21 -0
- package/templates/vercel-ai-quickstart/components/DataPreview.tsx +57 -0
- package/templates/vercel-ai-quickstart/components/HealthStatus.tsx +214 -0
- package/templates/vercel-ai-quickstart/components/LayerCard.tsx +263 -0
- package/templates/vercel-ai-quickstart/components/LayerFlowDiagram.tsx +195 -0
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/components/MemorySpaceSwitcher.tsx +93 -0
- package/templates/vercel-ai-quickstart/convex/conversations.ts +67 -0
- package/templates/vercel-ai-quickstart/convex/facts.ts +131 -0
- package/templates/vercel-ai-quickstart/convex/health.ts +15 -0
- package/templates/vercel-ai-quickstart/convex/memories.ts +104 -0
- package/templates/vercel-ai-quickstart/convex/schema.ts +20 -0
- package/templates/vercel-ai-quickstart/convex/users.ts +105 -0
- package/templates/vercel-ai-quickstart/jest.config.js +45 -0
- package/templates/vercel-ai-quickstart/lib/animations.ts +146 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/layer-tracking.ts +214 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/next.config.js +27 -0
- package/templates/vercel-ai-quickstart/package.json +46 -0
- package/templates/vercel-ai-quickstart/postcss.config.js +5 -0
- package/templates/vercel-ai-quickstart/tailwind.config.js +37 -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 +33 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { createCortexMemoryAsync } from "@cortexmemory/vercel-ai-provider";
|
|
2
|
+
import type {
|
|
3
|
+
LayerObserver,
|
|
4
|
+
CortexMemoryConfig,
|
|
5
|
+
} from "@cortexmemory/vercel-ai-provider";
|
|
6
|
+
import { openai, createOpenAI } from "@ai-sdk/openai";
|
|
7
|
+
import {
|
|
8
|
+
streamText,
|
|
9
|
+
embed,
|
|
10
|
+
convertToModelMessages,
|
|
11
|
+
createUIMessageStream,
|
|
12
|
+
createUIMessageStreamResponse,
|
|
13
|
+
} from "ai";
|
|
14
|
+
import { getCortex } from "@/lib/cortex";
|
|
15
|
+
|
|
16
|
+
// Create OpenAI client for embeddings
|
|
17
|
+
const openaiClient = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
18
|
+
|
|
19
|
+
// System prompt for the assistant
|
|
20
|
+
const SYSTEM_PROMPT = `You are a helpful AI assistant with long-term memory powered by Cortex.
|
|
21
|
+
|
|
22
|
+
Your capabilities:
|
|
23
|
+
- You remember everything users tell you across conversations
|
|
24
|
+
- You can recall facts, preferences, and context from past interactions
|
|
25
|
+
- You naturally reference what you've learned about the user
|
|
26
|
+
|
|
27
|
+
Behavior guidelines:
|
|
28
|
+
- When you remember something from a previous conversation, mention it naturally
|
|
29
|
+
- If asked about something you learned, reference it specifically
|
|
30
|
+
- Be conversational and friendly
|
|
31
|
+
- Help demonstrate the memory system by showing what you remember
|
|
32
|
+
|
|
33
|
+
Example interactions:
|
|
34
|
+
- User: "My name is Alex" → Remember and use their name
|
|
35
|
+
- User: "I work at Acme Corp" → Remember their employer
|
|
36
|
+
- User: "My favorite color is blue" → Remember their preference
|
|
37
|
+
- User: "What do you know about me?" → List everything you remember`;
|
|
38
|
+
|
|
39
|
+
// Create Cortex Memory config factory
|
|
40
|
+
// Uses createCortexMemoryAsync for graph support when CORTEX_GRAPH_SYNC=true
|
|
41
|
+
function getCortexMemoryConfig(
|
|
42
|
+
memorySpaceId: string,
|
|
43
|
+
userId: string,
|
|
44
|
+
conversationId: string,
|
|
45
|
+
layerObserver?: LayerObserver,
|
|
46
|
+
): CortexMemoryConfig {
|
|
47
|
+
return {
|
|
48
|
+
convexUrl: process.env.CONVEX_URL!,
|
|
49
|
+
memorySpaceId,
|
|
50
|
+
|
|
51
|
+
// User identification
|
|
52
|
+
userId,
|
|
53
|
+
userName: "Demo User",
|
|
54
|
+
|
|
55
|
+
// Agent identification (required for user-agent conversations in SDK v0.17.0+)
|
|
56
|
+
agentId: "quickstart-assistant",
|
|
57
|
+
agentName: "Cortex Demo Assistant",
|
|
58
|
+
|
|
59
|
+
// Conversation ID for chat history isolation
|
|
60
|
+
conversationId,
|
|
61
|
+
|
|
62
|
+
// Enable graph memory sync (auto-configured via env vars)
|
|
63
|
+
// When true, uses CypherGraphAdapter to sync to Neo4j/Memgraph
|
|
64
|
+
enableGraphMemory: process.env.CORTEX_GRAPH_SYNC === "true",
|
|
65
|
+
|
|
66
|
+
// Enable fact extraction (auto-configured via env vars)
|
|
67
|
+
enableFactExtraction: process.env.CORTEX_FACT_EXTRACTION === "true",
|
|
68
|
+
|
|
69
|
+
// Belief Revision (v0.24.0+)
|
|
70
|
+
// Automatically handles fact updates, supersessions, and deduplication
|
|
71
|
+
// When a user changes their preference (e.g., "I now prefer purple"),
|
|
72
|
+
// the system intelligently updates or supersedes the old fact.
|
|
73
|
+
beliefRevision: {
|
|
74
|
+
enabled: true, // Enable the belief revision pipeline
|
|
75
|
+
slotMatching: true, // Fast slot-based conflict detection (subject-predicate matching)
|
|
76
|
+
llmResolution: true, // LLM-based resolution for nuanced conflicts
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Embedding provider for semantic matching (required for semantic dedup & belief revision)
|
|
80
|
+
embeddingProvider: {
|
|
81
|
+
generate: async (text: string) => {
|
|
82
|
+
const result = await embed({
|
|
83
|
+
model: openaiClient.embedding("text-embedding-3-small"),
|
|
84
|
+
value: text,
|
|
85
|
+
});
|
|
86
|
+
return result.embedding;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Streaming enhancements
|
|
91
|
+
streamingOptions: {
|
|
92
|
+
storePartialResponse: true,
|
|
93
|
+
progressiveFactExtraction: true,
|
|
94
|
+
enableAdaptiveProcessing: true,
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Memory recall configuration (v0.23.0 - unified retrieval across all layers)
|
|
98
|
+
memorySearchLimit: 20, // Results from combined vector + facts + graph search
|
|
99
|
+
|
|
100
|
+
// Real-time layer tracking (v0.24.0+)
|
|
101
|
+
// Events are emitted as each layer processes, enabling live UI updates
|
|
102
|
+
layerObserver,
|
|
103
|
+
|
|
104
|
+
// Debug in development
|
|
105
|
+
debug: process.env.NODE_ENV === "development",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
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
|
+
export async function POST(req: Request) {
|
|
126
|
+
try {
|
|
127
|
+
const body = await req.json();
|
|
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;
|
|
134
|
+
|
|
135
|
+
// Convert UIMessage[] from useChat to ModelMessage[] for streamText
|
|
136
|
+
// Note: In AI SDK v6+, convertToModelMessages may return a Promise
|
|
137
|
+
const modelMessagesResult = convertToModelMessages(messages);
|
|
138
|
+
const modelMessages =
|
|
139
|
+
modelMessagesResult instanceof Promise
|
|
140
|
+
? await modelMessagesResult
|
|
141
|
+
: modelMessagesResult;
|
|
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
|
+
|
|
165
|
+
// Use createUIMessageStream to send both LLM text and layer events
|
|
166
|
+
return createUIMessageStreamResponse({
|
|
167
|
+
stream: createUIMessageStream({
|
|
168
|
+
execute: async ({ writer }) => {
|
|
169
|
+
// Create observer that writes layer events to the stream
|
|
170
|
+
// These events are transient (not persisted in message history)
|
|
171
|
+
const layerObserver: LayerObserver = {
|
|
172
|
+
onOrchestrationStart: (orchestrationId) => {
|
|
173
|
+
writer.write({
|
|
174
|
+
type: "data-orchestration-start",
|
|
175
|
+
data: { orchestrationId },
|
|
176
|
+
transient: true,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
onLayerUpdate: (event) => {
|
|
180
|
+
writer.write({
|
|
181
|
+
type: "data-layer-update",
|
|
182
|
+
data: {
|
|
183
|
+
layer: event.layer,
|
|
184
|
+
status: event.status,
|
|
185
|
+
timestamp: event.timestamp,
|
|
186
|
+
latencyMs: event.latencyMs,
|
|
187
|
+
data: event.data,
|
|
188
|
+
error: event.error,
|
|
189
|
+
revisionAction: event.revisionAction,
|
|
190
|
+
supersededFacts: event.supersededFacts,
|
|
191
|
+
},
|
|
192
|
+
transient: true,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
onOrchestrationComplete: (summary) => {
|
|
196
|
+
writer.write({
|
|
197
|
+
type: "data-orchestration-complete",
|
|
198
|
+
data: {
|
|
199
|
+
orchestrationId: summary.orchestrationId,
|
|
200
|
+
totalLatencyMs: summary.totalLatencyMs,
|
|
201
|
+
createdIds: summary.createdIds,
|
|
202
|
+
},
|
|
203
|
+
transient: true,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Build config with the observer and conversation ID
|
|
209
|
+
const config = getCortexMemoryConfig(
|
|
210
|
+
memorySpaceId || "quickstart-demo",
|
|
211
|
+
userId || "demo-user",
|
|
212
|
+
conversationId,
|
|
213
|
+
layerObserver,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Create memory-augmented model with async initialization (enables graph support)
|
|
217
|
+
// This connects to Neo4j/Memgraph if CORTEX_GRAPH_SYNC=true
|
|
218
|
+
const cortexMemory = await createCortexMemoryAsync(config);
|
|
219
|
+
|
|
220
|
+
// Stream response with automatic memory integration
|
|
221
|
+
const result = streamText({
|
|
222
|
+
model: cortexMemory(openai("gpt-4o-mini")),
|
|
223
|
+
messages: modelMessages,
|
|
224
|
+
system: SYSTEM_PROMPT,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Merge LLM stream into the UI message stream
|
|
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
|
+
}
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error("[Chat API Error]", error);
|
|
266
|
+
|
|
267
|
+
return new Response(
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
270
|
+
}),
|
|
271
|
+
{
|
|
272
|
+
status: 500,
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Cortex } from "@cortexmemory/sdk";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
function getCortex() {
|
|
6
|
+
return new Cortex({ convexUrl: process.env.CONVEX_URL! });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
try {
|
|
11
|
+
const { searchParams } = new URL(req.url);
|
|
12
|
+
const memorySpaceId =
|
|
13
|
+
searchParams.get("memorySpaceId") || "quickstart-demo";
|
|
14
|
+
const userId = searchParams.get("userId");
|
|
15
|
+
const limit = parseInt(searchParams.get("limit") || "50");
|
|
16
|
+
|
|
17
|
+
const cortex = getCortex();
|
|
18
|
+
|
|
19
|
+
// Fetch facts for the user/memory space
|
|
20
|
+
const facts = await cortex.facts.list({
|
|
21
|
+
memorySpaceId,
|
|
22
|
+
...(userId ? { userId } : {}),
|
|
23
|
+
limit,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return Response.json({
|
|
27
|
+
facts,
|
|
28
|
+
count: facts.length,
|
|
29
|
+
memorySpaceId,
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("[Facts API Error]", error);
|
|
33
|
+
|
|
34
|
+
return Response.json(
|
|
35
|
+
{ error: error instanceof Error ? error.message : "Unknown error" },
|
|
36
|
+
{ status: 500 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|