From d9e933a67ccc9dfcb9ff8bb461f67d93e7ec215b Mon Sep 17 00:00:00 2001 From: Oburusule Dunstan Date: Wed, 27 Mar 2024 07:16:18 +0300 Subject: [PATCH] persist messages --- .../src/app/components/Presentation.tsx | 2 + .../src/app/components/vacation-list.tsx | 2 + .../copilot-provider/copilotkit.tsx | 37 +++++++- .../src/context/copilot-context.tsx | 22 +++-- .../packages/react-core/src/hooks/use-chat.ts | 27 +++++- .../src/hooks/use-flat-category-store.ts | 28 ++++-- .../packages/react-core/src/hooks/use-tree.ts | 89 ++++++++++++++++--- 7 files changed, 180 insertions(+), 27 deletions(-) diff --git a/CopilotKit/examples/next-openai/src/app/components/Presentation.tsx b/CopilotKit/examples/next-openai/src/app/components/Presentation.tsx index 44ef1016d94..cbf0e4c85c6 100644 --- a/CopilotKit/examples/next-openai/src/app/components/Presentation.tsx +++ b/CopilotKit/examples/next-openai/src/app/components/Presentation.tsx @@ -15,6 +15,7 @@ import { import { resetGlobalAudio, speak } from "../utils"; import { ActionButton } from "./ActionButton"; import { SlideModel, Slide } from "./Slide"; +import Link from "next/link"; export const Presentation = ({ chatInProgress }: { chatInProgress: boolean }) => { const [slides, setSlides] = useState([ @@ -93,6 +94,7 @@ export const Presentation = ({ chatInProgress }: { chatInProgress: boolean }) => return (
+ Home {/* Add the action buttons below */}
diff --git a/CopilotKit/examples/next-openai/src/app/components/vacation-list.tsx b/CopilotKit/examples/next-openai/src/app/components/vacation-list.tsx index 2ef732ea036..067faef2e8f 100644 --- a/CopilotKit/examples/next-openai/src/app/components/vacation-list.tsx +++ b/CopilotKit/examples/next-openai/src/app/components/vacation-list.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { DestinationTable } from "./destination-table"; import { VacationNotes } from "./vacation-notes"; import { useMakeCopilotDocumentReadable, DocumentPointer } from "@copilotkit/react-core"; +import Link from "next/link"; export type Destination = { name: string; @@ -43,6 +44,7 @@ export function VacationList() {

WaterBnB (Toy Example)

+ Presentation
diff --git a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx index 57937656e82..33515582242 100644 --- a/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx +++ b/CopilotKit/packages/react-core/src/components/copilot-provider/copilotkit.tsx @@ -54,10 +54,12 @@ export function CopilotKit({ children, ...props }: CopilotKitProps) { const [entryPoints, setEntryPoints] = useState>>({}); const chatComponentsCache = useRef>({}); - const { addElement, removeElement, printTree } = useTree(); + const { addElement, removeElement, printTree, addMessageElement,getMessagesElement } = useTree(); const { addElement: addDocument, + addMessageElement: addMessage, + getMessagesElement: getMessages, removeElement: removeDocument, allElements: allDocuments, } = useFlatCategoryStore(); @@ -112,6 +114,34 @@ export function CopilotKit({ children, ...props }: CopilotKitProps) { [removeElement], ); + const addMessageContext = useCallback( + (threadId: string, messages: any[]) => { + addMessageElement(threadId, messages); + }, + [addMessageElement], + ); + + const getMessagesContext = useCallback( + (threadId: string) => { + return getMessagesElement(threadId); + }, + [getMessagesElement], + ); + + const messageContext = useCallback( + (threadId: string, messages: any[]) => { + addMessage(threadId, messages); + }, + [addMessage], + ); + + const messagesContext = useCallback( + (threadId: string) => { + return getMessages(threadId); + }, + [getMessages], + ); + const getChatCompletionFunctionDescriptions = useCallback( (customEntryPoints?: Record>) => { return entryPointsToChatCompletionFunctions(Object.values(customEntryPoints || entryPoints)); @@ -147,6 +177,7 @@ export function CopilotKit({ children, ...props }: CopilotKitProps) { [removeDocument], ); + // get the appropriate CopilotApiConfig from the props const copilotApiConfig: CopilotApiConfig = new StandardCopilotApiConfig( props.url, @@ -172,7 +203,11 @@ export function CopilotKit({ children, ...props }: CopilotKitProps) { removeContext, getDocumentsContext, addDocumentContext, + addMessageContext, + getMessagesContext, removeDocumentContext, + messageContext, + messagesContext, copilotApiConfig: copilotApiConfig, }} > diff --git a/CopilotKit/packages/react-core/src/context/copilot-context.tsx b/CopilotKit/packages/react-core/src/context/copilot-context.tsx index 7e6c6b87ee1..48258c36fdc 100644 --- a/CopilotKit/packages/react-core/src/context/copilot-context.tsx +++ b/CopilotKit/packages/react-core/src/context/copilot-context.tsx @@ -80,25 +80,37 @@ export interface CopilotContextParams { removeDocumentContext: (documentId: string) => void; getDocumentsContext: (categories: string[]) => DocumentPointer[]; + // messages + addMessageContext: (threadId: any, message: any) => void; + getMessagesContext: (threadId: any) => any[]; + messagesContext: (threadId: string) => void; + messageContext: (threadId: string, messages: any[]) => void; + // api endpoints copilotApiConfig: CopilotApiConfig; } const emptyCopilotContext: CopilotContextParams = { entryPoints: {}, - setEntryPoint: () => {}, - removeEntryPoint: () => {}, + setEntryPoint: () => { }, + removeEntryPoint: () => { }, getChatCompletionFunctionDescriptions: () => returnAndThrowInDebug([]), - getFunctionCallHandler: () => returnAndThrowInDebug(async () => {}), + getFunctionCallHandler: () => returnAndThrowInDebug(async () => { }), chatComponentsCache: { current: {} }, getContextString: (documents: DocumentPointer[], categories: string[]) => returnAndThrowInDebug(""), addContext: () => "", - removeContext: () => {}, + removeContext: () => { }, getDocumentsContext: (categories: string[]) => returnAndThrowInDebug([]), addDocumentContext: () => returnAndThrowInDebug(""), - removeDocumentContext: () => {}, + removeDocumentContext: () => { }, + + //messages + addMessageContext: (threadId: any, message: any) => returnAndThrowInDebug(""), + getMessagesContext: (threadId: any) => returnAndThrowInDebug([]), + messagesContext: (threadId: string) => returnAndThrowInDebug([]), + messageContext: () => "", copilotApiConfig: new (class implements CopilotApiConfig { get chatApiEndpoint(): string { diff --git a/CopilotKit/packages/react-core/src/hooks/use-chat.ts b/CopilotKit/packages/react-core/src/hooks/use-chat.ts index b626d9ace20..5f2cb121829 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-chat.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-chat.ts @@ -1,4 +1,5 @@ -import { useRef, useState } from "react"; +import { useRef, useState, useContext, useEffect } from "react"; +import { CopilotContext } from "../context/copilot-context"; import { Message, ToolDefinition, @@ -96,6 +97,19 @@ export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelper const threadIdRef = useRef(null); const runIdRef = useRef(null); + const { + addMessageContext, + getMessagesContext + } = useContext(CopilotContext); + + useEffect(() => { + const msgs = getMessagesContext("1"); + if(msgs){ + // console.log("msgs", msgs); + setMessages(msgs); + } + }, []); + const runChatCompletion = async (messages: Message[]): Promise => { setIsLoading(true); @@ -110,7 +124,10 @@ export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelper const abortController = new AbortController(); abortControllerRef.current = abortController; + + setMessages([...messages, ...newMessages]); + addMessageContext("1",messages); // add threadId and runId to the body if it exists const copilotConfigBody = options.copilotConfig.body || {}; @@ -130,6 +147,7 @@ export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelper signal: abortController.signal, }); + if (response.headers.get("threadid")) { threadIdRef.current = response.headers.get("threadid"); } @@ -209,7 +227,7 @@ export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelper let partialArguments: any = {}; try { partialArguments = JSON.parse(untruncateJson(value.arguments)); - } catch (e) {} + } catch (e) { } currentMessage.partialFunctionCall = { name: value.name, @@ -295,6 +313,11 @@ export function useChat(options: UseChatOptionsWithCopilotConfig): UseChatHelper abortControllerRef.current?.abort(); }; + + // console.log("message", messages); + + + return { messages, append, diff --git a/CopilotKit/packages/react-core/src/hooks/use-flat-category-store.ts b/CopilotKit/packages/react-core/src/hooks/use-flat-category-store.ts index 6153580cbb9..00bb15806e4 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-flat-category-store.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-flat-category-store.ts @@ -5,6 +5,8 @@ export type FlatCategoryStoreId = string; export interface UseFlatCategoryStoreReturn { addElement: (value: T, categories: string[]) => FlatCategoryStoreId; + addMessageElement: (threadId: string, messages: any[]) => FlatCategoryStoreId; + getMessagesElement: (threadId: string) => any[]; removeElement: (id: FlatCategoryStoreId) => void; allElements: (categories: string[]) => T[]; } @@ -15,6 +17,11 @@ interface FlatCategoryStoreElement { categories: Set; } +interface FlatMessagesStoreElement { + id: FlatCategoryStoreId; + list: any[]; +} + const useFlatCategoryStore = (): UseFlatCategoryStoreReturn => { const [elements, dispatch] = useReducer< React.Reducer>, Action> @@ -31,6 +38,15 @@ const useFlatCategoryStore = (): UseFlatCategoryStoreReturn => { return newId; }, []); + const addMessageElement = useCallback((threadId: string, messages: any[]): FlatCategoryStoreId => { + const newId = nanoid(); + return newId; + }, []); + + const getMessagesElement = useCallback((threadId: string): any[] => { + return []; + }, []); + const removeElement = useCallback((id: FlatCategoryStoreId): void => { dispatch({ type: "REMOVE_ELEMENT", id }); }, []); @@ -49,7 +65,7 @@ const useFlatCategoryStore = (): UseFlatCategoryStoreReturn => { [elements], ); - return { addElement, removeElement, allElements }; + return { addElement, removeElement, allElements, addMessageElement, getMessagesElement }; }; export default useFlatCategoryStore; @@ -57,11 +73,11 @@ export default useFlatCategoryStore; // Action types type Action = | { - type: "ADD_ELEMENT"; - value: T; - id: FlatCategoryStoreId; - categories: string[]; - } + type: "ADD_ELEMENT"; + value: T; + id: FlatCategoryStoreId; + categories: string[]; + } | { type: "REMOVE_ELEMENT"; id: FlatCategoryStoreId }; // Reducer diff --git a/CopilotKit/packages/react-core/src/hooks/use-tree.ts b/CopilotKit/packages/react-core/src/hooks/use-tree.ts index 524f56b7439..a9eea60bfee 100644 --- a/CopilotKit/packages/react-core/src/hooks/use-tree.ts +++ b/CopilotKit/packages/react-core/src/hooks/use-tree.ts @@ -9,6 +9,7 @@ export interface TreeNode { children: TreeNode[]; parentId?: TreeNodeId; categories: Set; + messages?: any[]; // Add this line } export type Tree = TreeNode[]; @@ -16,6 +17,8 @@ export type Tree = TreeNode[]; export interface UseTreeReturn { tree: Tree; addElement: (value: string, categories: string[], parentId?: TreeNodeId) => TreeNodeId; + addMessageElement: (threadId: string, messages: any[]) => TreeNodeId; + getMessagesElement: (threadId: string) => any[]; printTree: (categories: string[]) => string; removeElement: (id: TreeNodeId) => void; } @@ -33,6 +36,18 @@ const findNode = (nodes: Tree, id: TreeNodeId): TreeNode | undefined => { return undefined; }; +const findFromLocalStorage = (id: TreeNodeId) => { + const nodesString = localStorage.getItem('treeState'); + if (!nodesString) return undefined; + const nodes = JSON.parse(nodesString); + for (const node of nodes) { + if (node.id === id) { + return node; + } + } + return undefined; +} + const removeNode = (nodes: Tree, id: TreeNodeId): Tree => { return nodes.reduce((result: Tree, node) => { if (node.id !== id) { @@ -57,6 +72,40 @@ const addNode = (nodes: Tree, newNode: TreeNode, parentId?: TreeNodeId): Tree => }); }; +const addMessage = (nodes: Tree, threadId: string, messages: any[]): Tree => { + let found = false; + + const updatedNodes = nodes.map((node) => { + if (node.id === threadId) { + found = true; + const updatedMessages = node.messages ? [...node.messages, ...messages] : [...messages]; + return { ...node, messages: updatedMessages }; + } else if (node.children.length) { + const updatedChildren = addMessage(node.children, threadId, messages); + return { ...node, children: updatedChildren }; + } + return node; + }); + + if (!found) { + const newNode: TreeNode = { + id: threadId, + value: "", // Assuming a default or empty value, adjust as necessary + children: [], + categories: new Set(), // Assuming an empty set, adjust as necessary + messages: messages, + }; + updatedNodes.push(newNode); + } + + console.log("updatedNodes", updatedNodes); + localStorage.setItem('treeState', JSON.stringify(updatedNodes)); + + + return updatedNodes; +}; + + const treeIndentationRepresentation = (index: number, indentLevel: number): string => { if (indentLevel === 0) { return (index + 1).toString(); @@ -92,11 +141,11 @@ const printNode = (node: TreeNode, prefix = "", indentLevel = 0): string => { node.children.forEach( (child, index) => - (output += printNode( - child, - `${childPrePrefix}${treeIndentationRepresentation(index, indentLevel + 1)}. `, - indentLevel + 1, - )), + (output += printNode( + child, + `${childPrePrefix}${treeIndentationRepresentation(index, indentLevel + 1)}. `, + indentLevel + 1, + )), ); return output; }; @@ -104,13 +153,14 @@ const printNode = (node: TreeNode, prefix = "", indentLevel = 0): string => { // Action types type Action = | { - type: "ADD_NODE"; - value: string; - parentId?: string; - id: string; - categories: string[]; - } - | { type: "REMOVE_NODE"; id: string }; + type: "ADD_NODE"; + value: string; + parentId?: string; + id: string; + categories: string[]; + } + | { type: "REMOVE_NODE"; id: string } + | { type: "ADD_MESSAGE"; threadId: string; messages: any[] } // Reducer function function treeReducer(state: Tree, action: Action): Tree { @@ -133,6 +183,8 @@ function treeReducer(state: Tree, action: Action): Tree { } case "REMOVE_NODE": return removeNode(state, action.id); + case "ADD_MESSAGE": + return addMessage(state, action.threadId, action.messages); default: return state; } @@ -157,6 +209,17 @@ const useTree = (): UseTreeReturn => { [], ); + const addMessageElement = useCallback((threadId: string, messages: any[]): TreeNodeId => { + const newNodeId = nanoid(); // Generate new ID outside of dispatch + dispatch({ type: "ADD_MESSAGE", threadId, messages }); + return newNodeId; // Return the new ID + }, []); + + const getMessagesElement = useCallback((threadId: string): any[] => { + const node = findFromLocalStorage(threadId); + return node?.messages || []; + }, [tree]); + const removeElement = useCallback((id: TreeNodeId): void => { dispatch({ type: "REMOVE_NODE", id }); }, []); @@ -184,7 +247,7 @@ const useTree = (): UseTreeReturn => { [tree], ); - return { tree, addElement, printTree, removeElement }; + return { tree, addElement, printTree, removeElement, addMessageElement, getMessagesElement }; }; export default useTree;