Skip to content

Commit e830809

Browse files
committed
Update to AI SDK v5 and latest dependencies
- Upgrade ai package from 4.x to 5.x - Upgrade Next.js to 15.5.9 - Upgrade @assistant-ui/react to 0.11.57 and @assistant-ui/react-ai-sdk to 1.1.21 - Upgrade @mem0/vercel-ai-provider to 2.0.5 - Migrate API route to use createUIMessageStream and convertToModelMessages - Update client to use DefaultChatTransport for useChatRuntime - Update memory-ui to read DataMessagePart from message content
1 parent 6d965eb commit e830809

File tree

5 files changed

+2057
-2057
lines changed

5 files changed

+2057
-2057
lines changed

app/api/chat/route.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22

3-
import { createDataStreamResponse, jsonSchema, streamText } from "ai";
3+
import {
4+
streamText,
5+
UIMessage,
6+
convertToModelMessages,
7+
createUIMessageStream,
8+
createUIMessageStreamResponse,
9+
} from "ai";
410
import { addMemories, getMemories } from "@mem0/vercel-ai-provider";
511
import { openai } from "@ai-sdk/openai";
12+
import { frontendTools } from "@assistant-ui/react-ai-sdk";
613

7-
export const runtime = "edge";
814
export const maxDuration = 30;
915

1016
const retrieveMemories = (memories: any) => {
@@ -20,46 +26,75 @@ const retrieveMemories = (memories: any) => {
2026
return `System Message: ${systemPrompt} ${memoriesText}`;
2127
};
2228

29+
// Extract text content from messages for mem0
30+
const extractTextFromMessages = (messages: UIMessage[]): string => {
31+
return messages
32+
.map((m) => {
33+
if (Array.isArray(m.parts)) {
34+
return m.parts
35+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
36+
.map((p) => p.text)
37+
.join(" ");
38+
}
39+
return "";
40+
})
41+
.join("\n");
42+
};
43+
2344
export async function POST(req: Request) {
24-
const { messages, system, tools, userId } = await req.json();
45+
const {
46+
messages,
47+
system,
48+
tools,
49+
userId,
50+
} = (await req.json()) as {
51+
messages: UIMessage[];
52+
system?: string;
53+
tools?: Parameters<typeof frontendTools>[0];
54+
userId: string;
55+
};
56+
57+
// Convert UIMessages to model messages for streamText
58+
const modelMessages = convertToModelMessages(messages);
2559

26-
const memories = await getMemories(messages, { user_id: userId });
60+
// Extract text for mem0 (it expects string or LanguageModelV2Prompt)
61+
const textPrompt = extractTextFromMessages(messages);
62+
63+
const memories = await getMemories(textPrompt, { user_id: userId });
2764
const mem0Instructions = retrieveMemories(memories);
2865

29-
const result = streamText({
30-
model: openai("gpt-4o"),
31-
messages,
32-
// forward system prompt and tools from the frontend
33-
system: [system, mem0Instructions].filter(Boolean).join("\n"),
34-
tools: Object.fromEntries(
35-
Object.entries<{ parameters: unknown }>(tools).map(([name, tool]) => [
36-
name,
37-
{
38-
parameters: jsonSchema(tool.parameters!),
39-
},
40-
])
41-
),
42-
});
66+
// addMemories expects LanguageModelV2Prompt, use modelMessages with type cast
67+
const addMemoriesTask = addMemories(modelMessages as any, { user_id: userId });
4368

44-
const addMemoriesTask = addMemories(messages, { user_id: userId });
45-
return createDataStreamResponse({
46-
execute: async (writer) => {
69+
const stream = createUIMessageStream({
70+
execute: async ({ writer }) => {
71+
// Write memory annotation as data part with custom type
4772
if (memories.length > 0) {
48-
writer.writeMessageAnnotation({
49-
type: "mem0-get",
50-
memories,
73+
writer.write({
74+
type: "data-mem0-get",
75+
data: memories,
5176
});
5277
}
5378

54-
result.mergeIntoDataStream(writer);
79+
const result = streamText({
80+
model: openai("gpt-4o"),
81+
messages: modelMessages,
82+
system: [system, mem0Instructions].filter(Boolean).join("\n"),
83+
tools: tools ? (frontendTools(tools) as any) : undefined,
84+
});
5585

86+
writer.merge(result.toUIMessageStream());
87+
88+
// Add memories after stream completes
5689
const newMemories = await addMemoriesTask;
5790
if (newMemories.length > 0) {
58-
writer.writeMessageAnnotation({
59-
type: "mem0-update",
60-
memories: newMemories,
91+
writer.write({
92+
type: "data-mem0-update",
93+
data: newMemories,
6194
});
6295
}
6396
},
6497
});
98+
99+
return createUIMessageStreamResponse({ stream });
65100
}

app/assistant.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { AssistantRuntimeProvider } from "@assistant-ui/react";
44
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
55
import { Thread } from "@/components/assistant-ui/thread";
66
import { ThreadList } from "@/components/assistant-ui/thread-list";
7-
import { useEffect, useState } from "react";
7+
import { useEffect, useState, useMemo } from "react";
88
import { v4 as uuidv4 } from "uuid";
99
import Cookies from "js-cookie";
10+
import { DefaultChatTransport } from "ai";
1011

1112
const useUserId = () => {
12-
const [userId, setUserId] = useState<string>("");
13+
// null = loading, string = loaded
14+
const [userId, setUserId] = useState<string | null>(null);
1315

1416
useEffect(() => {
1517
let id = Cookies.get("userId");
@@ -23,12 +25,18 @@ const useUserId = () => {
2325
return userId;
2426
};
2527

26-
export const Assistant = () => {
27-
const userId = useUserId();
28-
const runtime = useChatRuntime({
29-
api: "/api/chat",
30-
body: { userId },
31-
});
28+
// Separate component that only renders when userId is ready
29+
const AssistantInner = ({ userId }: { userId: string }) => {
30+
const transport = useMemo(
31+
() =>
32+
new DefaultChatTransport({
33+
api: "/api/chat",
34+
body: { userId },
35+
}),
36+
[userId]
37+
);
38+
39+
const runtime = useChatRuntime({ transport });
3240

3341
return (
3442
<AssistantRuntimeProvider runtime={runtime}>
@@ -39,3 +47,18 @@ export const Assistant = () => {
3947
</AssistantRuntimeProvider>
4048
);
4149
};
50+
51+
export const Assistant = () => {
52+
const userId = useUserId();
53+
54+
// Don't render until userId is loaded to prevent race conditions
55+
if (!userId) {
56+
return (
57+
<div className="h-dvh flex items-center justify-center">
58+
<div className="text-muted-foreground">Loading...</div>
59+
</div>
60+
);
61+
}
62+
63+
return <AssistantInner userId={userId} />;
64+
};

components/assistant-ui/memory-ui.tsx

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,98 @@ import { FC, useMemo } from "react";
33
import MemoryIndicator, { Memory } from "./memory-indicator";
44

55
type RetrievedMemory = {
6-
isNew: boolean;
76
id: string;
87
memory: string;
98
user_id: string;
109
categories: readonly string[];
11-
immutable: boolean;
1210
created_at: string;
1311
updated_at: string;
1412
score: number;
1513
};
1614

1715
type NewMemory = {
18-
id: string;
19-
data: {
16+
id?: string;
17+
memory?: string;
18+
data?: {
2019
memory: string;
2120
};
22-
event: "ADD" | "DELETE";
21+
event?: "ADD" | "DELETE";
22+
// For async response
23+
message?: string;
24+
status?: string;
25+
};
26+
27+
// DataMessagePart from assistant-ui: type: "data" with name field
28+
type DataMessagePart<T = unknown> = {
29+
readonly type: "data";
30+
readonly name: string;
31+
readonly data: T;
2332
};
2433

25-
type NewMemoryAnnotation = {
26-
readonly type: "mem0-update";
27-
readonly memories: readonly NewMemory[];
34+
type Mem0GetDataPart = DataMessagePart<readonly RetrievedMemory[]> & {
35+
readonly name: "mem0-get";
2836
};
2937

30-
type GetMemoryAnnotation = {
31-
readonly type: "mem0-get";
32-
readonly memories: readonly RetrievedMemory[];
38+
type Mem0UpdateDataPart = DataMessagePart<readonly NewMemory[]> & {
39+
readonly name: "mem0-update";
3340
};
3441

35-
type MemoryAnnotation = NewMemoryAnnotation | GetMemoryAnnotation;
42+
const isMem0GetPart = (p: unknown): p is Mem0GetDataPart =>
43+
typeof p === "object" &&
44+
p != null &&
45+
"type" in p &&
46+
p.type === "data" &&
47+
"name" in p &&
48+
p.name === "mem0-get" &&
49+
"data" in p;
3650

37-
const isMemoryAnnotation = (a: unknown): a is MemoryAnnotation =>
38-
typeof a === "object" &&
39-
a != null &&
40-
"type" in a &&
41-
(a.type === "mem0-update" || a.type === "mem0-get");
51+
const isMem0UpdatePart = (p: unknown): p is Mem0UpdateDataPart =>
52+
typeof p === "object" &&
53+
p != null &&
54+
"type" in p &&
55+
p.type === "data" &&
56+
"name" in p &&
57+
p.name === "mem0-update" &&
58+
"data" in p;
4259

4360
const useMemories = (): Memory[] => {
44-
const annotations = useMessage((m) => m.metadata.unstable_annotations);
45-
console.log("annotations", annotations);
46-
return useMemo(
47-
() =>
48-
annotations?.filter(isMemoryAnnotation).flatMap((a) => {
49-
if (a.type === "mem0-update") {
50-
return a.memories.map(
51-
(m): Memory => ({
52-
event: m.event,
53-
id: m.id,
54-
memory: m.data.memory,
55-
score: 1,
56-
})
57-
);
58-
} else if (a.type === "mem0-get") {
59-
return a.memories.map((m) => ({
61+
const content = useMessage((m) => m.content);
62+
63+
return useMemo(() => {
64+
if (!content) return [];
65+
66+
const memories: Memory[] = [];
67+
68+
for (const part of content as unknown[]) {
69+
if (isMem0GetPart(part)) {
70+
for (const m of part.data) {
71+
memories.push({
6072
event: "GET",
6173
id: m.id,
6274
memory: m.memory,
6375
score: m.score,
64-
}));
76+
});
6577
}
66-
throw new Error("Unexpected annotation: " + JSON.stringify(a));
67-
}) ?? [],
68-
[annotations]
69-
);
78+
} else if (isMem0UpdatePart(part)) {
79+
for (const m of part.data) {
80+
// Skip async pending responses
81+
if (m.status === "PENDING") continue;
82+
83+
const memoryText = m.memory ?? m.data?.memory ?? "";
84+
if (!memoryText) continue;
85+
86+
memories.push({
87+
event: m.event ?? "ADD",
88+
id: m.id ?? "",
89+
memory: memoryText,
90+
score: 1,
91+
});
92+
}
93+
}
94+
}
95+
96+
return memories;
97+
}, [content]);
7098
};
7199

72100
export const MemoryUI: FC = () => {

package.json

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,41 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"@ai-sdk/openai": "^1.1.15",
13-
"@assistant-ui/react": "^0.8.2",
14-
"@assistant-ui/react-ai-sdk": "^0.8.0",
15-
"@assistant-ui/react-markdown": "^0.8.0",
16-
"@mem0/vercel-ai-provider": "^0.0.14",
17-
"@radix-ui/react-avatar": "^1.1.3",
18-
"@radix-ui/react-popover": "^1.1.6",
19-
"@radix-ui/react-slot": "^1.1.2",
20-
"@radix-ui/react-tooltip": "^1.1.8",
12+
"@ai-sdk/openai": "^2.0.88",
13+
"@assistant-ui/react": "^0.11.57",
14+
"@assistant-ui/react-ai-sdk": "^1.1.21",
15+
"@assistant-ui/react-markdown": "^0.11.9",
16+
"@mem0/vercel-ai-provider": "^2.0.5",
17+
"@radix-ui/react-avatar": "^1.1.11",
18+
"@radix-ui/react-popover": "^1.1.15",
19+
"@radix-ui/react-slot": "^1.2.4",
20+
"@radix-ui/react-tooltip": "^1.2.8",
2121
"@types/js-cookie": "^3.0.6",
2222
"@types/uuid": "^10.0.0",
23-
"ai": "^4.1.46",
23+
"ai": "^5.0.116",
2424
"class-variance-authority": "^0.7.1",
2525
"clsx": "^2.1.1",
2626
"js-cookie": "^3.0.5",
27-
"lucide-react": "^0.477.0",
28-
"next": "15.2.0",
29-
"react": "^19.0.0",
30-
"react-dom": "^19.0.0",
27+
"lucide-react": "^0.562.0",
28+
"next": "^15.2.8",
29+
"react": "^19.2.3",
30+
"react-dom": "^19.2.3",
3131
"remark-gfm": "^4.0.1",
32-
"tailwind-merge": "^3.0.2",
32+
"tailwind-merge": "^3.4.0",
3333
"tailwindcss-animate": "^1.0.7",
34-
"uuid": "^11.1.0"
34+
"uuid": "^11.1.0",
35+
"zod": "^3.25.0"
3536
},
3637
"devDependencies": {
37-
"@eslint/eslintrc": "^3",
38+
"@eslint/eslintrc": "^3.3.3",
3839
"@types/node": "^22",
39-
"@types/react": "^19",
40-
"@types/react-dom": "^19",
41-
"eslint": "^9",
42-
"eslint-config-next": "15.2.0",
43-
"postcss": "^8",
44-
"tailwindcss": "^3.4.1",
45-
"typescript": "^5"
40+
"@types/react": "^19.2.9",
41+
"@types/react-dom": "^19.2.3",
42+
"eslint": "^9.39.2",
43+
"eslint-config-next": "^15.2.8",
44+
"postcss": "^8.5.6",
45+
"tailwindcss": "^3.4.17",
46+
"typescript": "^5.9.3"
4647
},
4748
"packageManager": "pnpm@10.5.2"
4849
}

0 commit comments

Comments
 (0)