diff --git a/examples/vercel-ai-sdk-chat-app/package.json b/examples/vercel-ai-sdk-chat-app/package.json index c0aed9cd..47a3e4c4 100644 --- a/examples/vercel-ai-sdk-chat-app/package.json +++ b/examples/vercel-ai-sdk-chat-app/package.json @@ -10,7 +10,7 @@ "preview": "vite preview" }, "dependencies": { - "@mem0/vercel-ai-provider": "^0.0.7", + "@mem0/vercel-ai-provider": "0.0.12", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.1", @@ -18,7 +18,7 @@ "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", - "ai": "^3.4.31", + "ai": "4.1.42", "buffer": "^6.0.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/examples/vercel-ai-sdk-chat-app/src/components/api-settings-popup.tsx b/examples/vercel-ai-sdk-chat-app/src/components/api-settings-popup.tsx index 75cea86c..8a4eac39 100644 --- a/examples/vercel-ai-sdk-chat-app/src/components/api-settings-popup.tsx +++ b/examples/vercel-ai-sdk-chat-app/src/components/api-settings-popup.tsx @@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import GlobalContext from '@/contexts/GlobalContext' - +import { Provider } from '@/constants/messages' export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Dispatch> }) { const {isOpen, setIsOpen} = props const [mem0ApiKey, setMem0ApiKey] = useState('') @@ -15,7 +15,7 @@ export default function ApiSettingsPopup(props: { isOpen: boolean, setIsOpen: Di const handleSave = () => { // Here you would typically save the settings to your backend or local storage - selectorHandler(mem0ApiKey, providerApiKey, provider); + selectorHandler(mem0ApiKey, providerApiKey, provider as Provider); setIsOpen(false) } diff --git a/examples/vercel-ai-sdk-chat-app/src/constants/messages.ts b/examples/vercel-ai-sdk-chat-app/src/constants/messages.ts new file mode 100644 index 00000000..af3280a0 --- /dev/null +++ b/examples/vercel-ai-sdk-chat-app/src/constants/messages.ts @@ -0,0 +1,31 @@ +import { Message } from "@/types"; + +export const WELCOME_MESSAGE: Message = { + id: "1", + content: "👋 Hi there! I'm your personal assistant. How can I help you today? 😊", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const INVALID_CONFIG_MESSAGE: Message = { + id: "2", + content: "Invalid configuration. Please check your API keys, and add a user and try again.", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const ERROR_MESSAGE: Message = { + id: "3", + content: "Something went wrong. Please try again.", + sender: "assistant", + timestamp: new Date().toLocaleTimeString(), +}; + +export const AI_MODELS = { + openai: "gpt-4o", + anthropic: "claude-3-haiku-20240307", + cohere: "command-r-plus", + groq: "gemma2-9b-it", +} as const; + +export type Provider = keyof typeof AI_MODELS; \ No newline at end of file diff --git a/examples/vercel-ai-sdk-chat-app/src/contexts/GlobalContext.tsx b/examples/vercel-ai-sdk-chat-app/src/contexts/GlobalContext.tsx index 4f959b56..755ea829 100644 --- a/examples/vercel-ai-sdk-chat-app/src/contexts/GlobalContext.tsx +++ b/examples/vercel-ai-sdk-chat-app/src/contexts/GlobalContext.tsx @@ -1,281 +1,84 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { createContext, useEffect, useState } from "react"; -import { createMem0, searchMemories } from "@mem0/vercel-ai-provider"; -import { LanguageModelV1Prompt, streamText } from "ai"; -import { Message, Memory, FileInfo } from "@/types"; -import { Buffer } from 'buffer'; +import { createContext } from 'react'; +import { Message, Memory, FileInfo } from '@/types'; +import { useAuth } from '@/hooks/useAuth'; +import { useChat } from '@/hooks/useChat'; +import { useFileHandler } from '@/hooks/useFileHandler'; +import { Provider } from '@/constants/messages'; -const GlobalContext = createContext({}); - -const WelcomeMessage: Message = { - id: "1", - content: - "👋 Hi there! I'm your personal assistant. How can I help you today? 😊", - sender: "assistant", - timestamp: new Date().toLocaleTimeString(), -}; - -const InvalidConfigMessage: Message = { - id: "2", - content: - "Invalid configuration. Please check your API keys, and add a user and try again.", - sender: "assistant", - timestamp: new Date().toLocaleTimeString(), -}; - -const SomethingWentWrongMessage: Message = { - id: "3", - content: "Something went wrong. Please try again.", - sender: "assistant", - timestamp: new Date().toLocaleTimeString(), -}; - -const models = { - "openai": "gpt-4o", - "anthropic": "claude-3-haiku-20240307", - "cohere": "command-r-plus", - "groq": "gemma2-9b-it" +interface GlobalContextType { + selectedUser: string; + selectUserHandler: (user: string) => void; + clearUserHandler: () => void; + messages: Message[]; + memories: Memory[]; + handleSend: (content: string) => Promise; + thinking: boolean; + selectedMem0Key: string; + selectedOpenAIKey: string; + selectedProvider: Provider; + selectorHandler: (mem0: string, openai: string, provider: Provider) => void; + clearConfiguration: () => void; + selectedFile: FileInfo | null; + setSelectedFile: (file: FileInfo | null) => void; + file: File | null; + setFile: (file: File | null) => void; } -const getModel = (provider: string) => { - switch (provider) { - case "openai": - return models.openai; - case "anthropic": - return models.anthropic; - case "cohere": - return models.cohere; - case "groq": - return models.groq; - default: - return models.openai; - } -} +const GlobalContext = createContext({} as GlobalContextType); -const GlobalState = (props: any) => { - const [memories, setMemories] = useState([]); - const [messages, setMessages] = useState([]); - const [selectedUser, setSelectedUser] = useState(""); - const [thinking, setThinking] = useState(false); - const [selectedOpenAIKey, setSelectedOpenAIKey] = useState(""); - const [selectedMem0Key, setSelectedMem0Key] = useState(""); - const [selectedProvider, setSelectedProvider] = useState("openai"); - const [selectedFile, setSelectedFile] = useState(null) - const [file, setFile] = useState(null) - - const mem0 = createMem0({ - provider: selectedProvider, +const GlobalState = (props: { children: React.ReactNode }) => { + const { mem0ApiKey: selectedMem0Key, - apiKey: selectedOpenAIKey, + openaiApiKey: selectedOpenAIKey, + provider: selectedProvider, + user: selectedUser, + setAuth: selectorHandler, + setUser: selectUserHandler, + clearAuth: clearConfiguration, + clearUser: clearUserHandler, + } = useAuth(); + + const { + selectedFile, + file, + fileData, + setSelectedFile, + handleFile, + clearFile, + } = useFileHandler(); + + const { + messages, + memories, + thinking, + sendMessage, + } = useChat({ + user: selectedUser, + mem0ApiKey: selectedMem0Key, + openaiApiKey: selectedOpenAIKey, + provider: selectedProvider, }); - const clearConfiguration = () => { - localStorage.removeItem("mem0ApiKey"); - localStorage.removeItem("openaiApiKey"); - localStorage.removeItem("provider"); - setSelectedMem0Key(""); - setSelectedOpenAIKey(""); - setSelectedProvider("openai"); - setSelectedUser(""); - setMessages([WelcomeMessage]); - setMemories([]); - setFile(null); - }; - - const selectorHandler = (mem0: string, openai: string, provider: string) => { - setSelectedMem0Key(mem0); - setSelectedOpenAIKey(openai); - setSelectedProvider(provider); - localStorage.setItem("mem0ApiKey", mem0); - localStorage.setItem("openaiApiKey", openai); - localStorage.setItem("provider", provider); - }; - - - useEffect(() => { - const mem0 = localStorage.getItem("mem0ApiKey"); - const openai = localStorage.getItem("openaiApiKey"); - const provider = localStorage.getItem("provider"); - const user = localStorage.getItem("user"); - if (mem0 && openai && provider) { - selectorHandler(mem0, openai, provider); - } - if (user) { - setSelectedUser(user); - } - }, []); - - const selectUserHandler = (user: string) => { - setSelectedUser(user); - localStorage.setItem("user", user); - }; - - const clearUserHandler = () => { - setSelectedUser(""); - setMemories([]); - }; - - const getMemories = async (messages: LanguageModelV1Prompt) => { - try { - const smemories = await searchMemories(messages, { - user_id: selectedUser || "", - mem0ApiKey: selectedMem0Key, - }); - - const newMemories = smemories.map((memory: any) => ({ - id: memory.id, - content: memory.memory, - timestamp: memory.updated_at, - tags: memory.categories, - })); - setMemories(newMemories); - } catch (error) { - console.error("Error in getMemories:", error); - } - }; - - const handleSend = async (inputValue: string) => { - if (!inputValue.trim() && !file) return; - if (!selectedUser) { - const newMessage: Message = { - id: Date.now().toString(), - content: inputValue, - sender: "user", - timestamp: new Date().toLocaleTimeString(), - }; - setMessages((prev) => [...prev, newMessage, InvalidConfigMessage]); - return; - } - - const userMessage: Message = { - id: Date.now().toString(), - content: inputValue, - sender: "user", - timestamp: new Date().toLocaleTimeString(), - }; - - let fileData; + const handleSend = async (content: string) => { if (file) { - if (file.type.startsWith("image/")) { - // Convert image to Base64 - fileData = await convertToBase64(file); - userMessage.image = fileData; - } else if (file.type.startsWith("audio/")) { - // Convert audio to ArrayBuffer - fileData = await getFileBuffer(file); - userMessage.audio = fileData; - } - } - - // Update the state with the new user message - setMessages((prev) => [...prev, userMessage]); - setThinking(true); - - // Transform messages into the required format - const messagesForPrompt: LanguageModelV1Prompt = []; - messages.map((message) => { - const messageContent: any = { - role: message.sender, - content: [ - { - type: "text", - text: message.content, - }, - ], - }; - if (message.image) { - messageContent.content.push({ - type: "image", - image: message.image, - }); - } - if (message.audio) { - messageContent.content.push({ - type: 'file', - mimeType: 'audio/mpeg', - data: message.audio, - }); - } - if(!message.audio) messagesForPrompt.push(messageContent); - }); - - const newMessage: any = { - role: "user", - content: [ - { - type: "text", - text: inputValue, - }, - ], - }; - if (file) { - if (file.type.startsWith("image/")) { - newMessage.content.push({ - type: "image", - image: userMessage.image, - }); - } else if (file.type.startsWith("audio/")) { - newMessage.content.push({ - type: 'file', - mimeType: 'audio/mpeg', - data: userMessage.audio, - }); - } - } - - messagesForPrompt.push(newMessage); - getMemories(messagesForPrompt); - - setFile(null); - setSelectedFile(null); - - try { - const { textStream } = await streamText({ - model: mem0(getModel(selectedProvider), { - user_id: selectedUser || "", - }), - messages: messagesForPrompt, + await sendMessage(content, { + type: file.type, + data: fileData!, }); - - const assistantMessageId = Date.now() + 1; - const assistantMessage: Message = { - id: assistantMessageId.toString(), - content: "", - sender: "assistant", - timestamp: new Date().toLocaleTimeString(), - }; - - setMessages((prev) => [...prev, assistantMessage]); - - // Stream the text part by part - for await (const textPart of textStream) { - assistantMessage.content += textPart; - setThinking(false); - setFile(null); - setSelectedFile(null); - - setMessages((prev) => - prev.map((msg) => - msg.id === assistantMessageId.toString() - ? { ...msg, content: assistantMessage.content } - : msg - ) - ); - } - - setThinking(false); - } catch (error) { - console.error("Error in handleSend:", error); - setMessages((prev) => [...prev, SomethingWentWrongMessage]); - setThinking(false); - setFile(null); - setSelectedFile(null); + clearFile(); + } else { + await sendMessage(content); } }; - useEffect(() => { - setMessages([WelcomeMessage]); - }, []); + const setFile = async (newFile: File | null) => { + if (newFile) { + await handleFile(newFile); + } else { + clearFile(); + } + }; return ( { selectedFile, setSelectedFile, file, - setFile + setFile, }} > {props.children} @@ -304,21 +107,4 @@ const GlobalState = (props: any) => { }; export default GlobalContext; -export { GlobalState }; - - -const convertToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => resolve(reader.result as string); // Resolve with Base64 string - reader.onerror = error => reject(error); // Reject on error - }); -}; - -async function getFileBuffer(file: any) { - const response = await fetch(file); - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - return buffer; -} \ No newline at end of file +export { GlobalState }; \ No newline at end of file diff --git a/examples/vercel-ai-sdk-chat-app/src/hooks/useAuth.ts b/examples/vercel-ai-sdk-chat-app/src/hooks/useAuth.ts new file mode 100644 index 00000000..5687442c --- /dev/null +++ b/examples/vercel-ai-sdk-chat-app/src/hooks/useAuth.ts @@ -0,0 +1,73 @@ +import { useState, useEffect } from 'react'; +import { Provider } from '@/constants/messages'; + +interface UseAuthReturn { + mem0ApiKey: string; + openaiApiKey: string; + provider: Provider; + user: string; + setAuth: (mem0: string, openai: string, provider: Provider) => void; + setUser: (user: string) => void; + clearAuth: () => void; + clearUser: () => void; +} + +export const useAuth = (): UseAuthReturn => { + const [mem0ApiKey, setMem0ApiKey] = useState(''); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const [provider, setProvider] = useState('openai'); + const [user, setUser] = useState(''); + + useEffect(() => { + const mem0 = localStorage.getItem('mem0ApiKey'); + const openai = localStorage.getItem('openaiApiKey'); + const savedProvider = localStorage.getItem('provider') as Provider; + const savedUser = localStorage.getItem('user'); + + if (mem0 && openai && savedProvider) { + setAuth(mem0, openai, savedProvider); + } + if (savedUser) { + setUser(savedUser); + } + }, []); + + const setAuth = (mem0: string, openai: string, provider: Provider) => { + setMem0ApiKey(mem0); + setOpenaiApiKey(openai); + setProvider(provider); + localStorage.setItem('mem0ApiKey', mem0); + localStorage.setItem('openaiApiKey', openai); + localStorage.setItem('provider', provider); + }; + + const clearAuth = () => { + localStorage.removeItem('mem0ApiKey'); + localStorage.removeItem('openaiApiKey'); + localStorage.removeItem('provider'); + setMem0ApiKey(''); + setOpenaiApiKey(''); + setProvider('openai'); + }; + + const updateUser = (user: string) => { + setUser(user); + localStorage.setItem('user', user); + }; + + const clearUser = () => { + localStorage.removeItem('user'); + setUser(''); + }; + + return { + mem0ApiKey, + openaiApiKey, + provider, + user, + setAuth, + setUser: updateUser, + clearAuth, + clearUser, + }; +}; \ No newline at end of file diff --git a/examples/vercel-ai-sdk-chat-app/src/hooks/useChat.ts b/examples/vercel-ai-sdk-chat-app/src/hooks/useChat.ts new file mode 100644 index 00000000..66eb7311 --- /dev/null +++ b/examples/vercel-ai-sdk-chat-app/src/hooks/useChat.ts @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import { createMem0, getMemories } from '@mem0/vercel-ai-provider'; +import { LanguageModelV1Prompt, streamText } from 'ai'; +import { Message, Memory } from '@/types'; +import { WELCOME_MESSAGE, INVALID_CONFIG_MESSAGE, ERROR_MESSAGE, AI_MODELS, Provider } from '@/constants/messages'; + +interface UseChatProps { + user: string; + mem0ApiKey: string; + openaiApiKey: string; + provider: Provider; +} + +interface UseChatReturn { + messages: Message[]; + memories: Memory[]; + thinking: boolean; + sendMessage: (content: string, fileData?: { type: string; data: string | Buffer }) => Promise; +} + +interface MemoryResponse { + id: string; + memory: string; + updated_at: string; + categories: string[]; +} + +type MessageContent = + | { type: 'text'; text: string } + | { type: 'image'; image: string } + | { type: 'file'; mimeType: string; data: Buffer }; + +interface PromptMessage { + role: string; + content: MessageContent[]; +} + +export const useChat = ({ user, mem0ApiKey, openaiApiKey, provider }: UseChatProps): UseChatReturn => { + const [messages, setMessages] = useState([WELCOME_MESSAGE]); + const [memories, setMemories] = useState([]); + const [thinking, setThinking] = useState(false); + + const mem0 = createMem0({ + provider, + mem0ApiKey, + apiKey: openaiApiKey, + }); + + const updateMemories = async (messages: LanguageModelV1Prompt) => { + try { + const fetchedMemories = await getMemories(messages, { + user_id: user, + mem0ApiKey, + }); + + const newMemories = fetchedMemories.map((memory: MemoryResponse) => ({ + id: memory.id, + content: memory.memory, + timestamp: memory.updated_at, + tags: memory.categories, + })); + setMemories(newMemories); + } catch (error) { + console.error('Error in getMemories:', error); + } + }; + + const formatMessagesForPrompt = (messages: Message[]): PromptMessage[] => { + return messages.map((message) => { + const messageContent: MessageContent[] = [ + { type: 'text', text: message.content } + ]; + + if (message.image) { + messageContent.push({ + type: 'image', + image: message.image, + }); + } + + if (message.audio) { + messageContent.push({ + type: 'file', + mimeType: 'audio/mpeg', + data: message.audio as Buffer, + }); + } + + return { + role: message.sender, + content: messageContent, + }; + }); + }; + + const sendMessage = async (content: string, fileData?: { type: string; data: string | Buffer }) => { + if (!content.trim() && !fileData) return; + + if (!user) { + const newMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + }; + setMessages((prev) => [...prev, newMessage, INVALID_CONFIG_MESSAGE]); + return; + } + + const userMessage: Message = { + id: Date.now().toString(), + content, + sender: 'user', + timestamp: new Date().toLocaleTimeString(), + ...(fileData?.type.startsWith('image/') && { image: fileData.data.toString() }), + ...(fileData?.type.startsWith('audio/') && { audio: fileData.data as Buffer }), + }; + + setMessages((prev) => [...prev, userMessage]); + setThinking(true); + + const messagesForPrompt = formatMessagesForPrompt([...messages, userMessage]); + await updateMemories(messagesForPrompt as LanguageModelV1Prompt); + + try { + const { textStream } = await streamText({ + model: mem0(AI_MODELS[provider], { + user_id: user, + }), + messages: messagesForPrompt as LanguageModelV1Prompt, + }); + + const assistantMessageId = Date.now() + 1; + const assistantMessage: Message = { + id: assistantMessageId.toString(), + content: '', + sender: 'assistant', + timestamp: new Date().toLocaleTimeString(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + for await (const textPart of textStream) { + assistantMessage.content += textPart; + setThinking(false); + + setMessages((prev) => + prev.map((msg) => + msg.id === assistantMessageId.toString() + ? { ...msg, content: assistantMessage.content } + : msg + ) + ); + } + } catch (error) { + console.error('Error in sendMessage:', error); + setMessages((prev) => [...prev, ERROR_MESSAGE]); + } finally { + setThinking(false); + } + }; + + return { + messages, + memories, + thinking, + sendMessage, + }; +}; \ No newline at end of file diff --git a/examples/vercel-ai-sdk-chat-app/src/hooks/useFileHandler.ts b/examples/vercel-ai-sdk-chat-app/src/hooks/useFileHandler.ts new file mode 100644 index 00000000..3353a8cf --- /dev/null +++ b/examples/vercel-ai-sdk-chat-app/src/hooks/useFileHandler.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { FileInfo } from '@/types'; +import { convertToBase64, getFileBuffer } from '@/utils/fileUtils'; + +interface UseFileHandlerReturn { + selectedFile: FileInfo | null; + file: File | null; + fileData: string | Buffer | null; + setSelectedFile: (file: FileInfo | null) => void; + handleFile: (file: File) => Promise; + clearFile: () => void; +} + +export const useFileHandler = (): UseFileHandlerReturn => { + const [selectedFile, setSelectedFile] = useState(null); + const [file, setFile] = useState(null); + const [fileData, setFileData] = useState(null); + + const handleFile = async (file: File) => { + setFile(file); + + if (file.type.startsWith('image/')) { + const base64Data = await convertToBase64(file); + setFileData(base64Data); + } else if (file.type.startsWith('audio/')) { + const bufferData = await getFileBuffer(file); + setFileData(bufferData); + } + }; + + const clearFile = () => { + setSelectedFile(null); + setFile(null); + setFileData(null); + }; + + return { + selectedFile, + file, + fileData, + setSelectedFile, + handleFile, + clearFile, + }; +}; \ No newline at end of file diff --git a/examples/vercel-ai-sdk-chat-app/src/utils/fileUtils.ts b/examples/vercel-ai-sdk-chat-app/src/utils/fileUtils.ts new file mode 100644 index 00000000..cd86f807 --- /dev/null +++ b/examples/vercel-ai-sdk-chat-app/src/utils/fileUtils.ts @@ -0,0 +1,16 @@ +import { Buffer } from 'buffer'; + +export const convertToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); +}; + +export const getFileBuffer = async (file: File): Promise => { + const response = await fetch(URL.createObjectURL(file)); + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); +}; \ No newline at end of file