From d30fec205523e2f8f432da07bc470296aceda00e Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Thu, 13 Feb 2025 01:39:20 -0500 Subject: [PATCH 1/8] chat completion initial --- src/Chat/ChatBase.tsx | 2 +- src/lib/ChatCraftChat.ts | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index cd120bfba..676804e6f 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -357,7 +357,7 @@ function ChatBase({ chat }: ChatBaseProps) { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [user, chat, streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] + [user, , streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] ); // Restart auto-scrolling and resume a paused response when Follow Chat is clicked diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index 57b622e55..8e1346a72 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -23,6 +23,8 @@ import { SharedChatCraftChat } from "./SharedChatCraftChat"; import { countTokensInMessages } from "./ai"; import { parseFunctionNames, loadFunctions } from "./ChatCraftFunction"; import { ChatCraftFile } from "./ChatCraftFile"; +import { ChatCraftCommandRegistry } from "./ChatCraftCommandRegistry"; +import { ChatCraftCommand } from "./ChatCraftCommand"; export type SerializedChatCraftChat = { id: string; @@ -119,6 +121,57 @@ export class ChatCraftChat { return Array.from(this._files?.values() ?? []).map(mapper); } + async completion( + prompt: string, + chat: ChatCraftChat, + user: any, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type, prettier/prettier + callChatApi: Function, + forceScroll: Function, + error: Function + ) { + try { + // Handle commands (slash commands) + if (ChatCraftCommandRegistry.isCommand(prompt)) { + const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); + if (commandFunction) { + await commandFunction(chat, user); + forceScroll(); + } else { + // If command is not recognized + const { command } = ChatCraftCommand.parseCommand(prompt)!; + const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; + await commandFunction(chat, user); + forceScroll(); + } + return; + } + + // If not a command, treat as a prompt to LLM (or another AI model) + const promptMessage = new ChatCraftHumanMessage({ text: prompt, user }); + await chat.addMessage(promptMessage); + + // Handle any functions associated with the prompt + const functions = await chat.functions((err) => + error({ + title: `Error Loading Function`, + message: err.message, + }) + ); + + const messages = chat.messages({ includeAppMessages: false }); + const response = await callChatApi(messages, { functions }); + + await chat.addMessage(response); + //forceScroll(); + } catch (err: any) { + error({ + title: `Response Error`, + message: err.message, + }); + } + } + /** * We store all message types, but they can be requested with or * without the ChatCraftAppMessages and ChatCraftSystemMessages. For From e2b19d84bdc662a73f9d6113051a5236ddb2850c Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Thu, 13 Feb 2025 01:39:59 -0500 Subject: [PATCH 2/8] quick fix --- src/Chat/ChatBase.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index 676804e6f..cd120bfba 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -357,7 +357,7 @@ function ChatBase({ chat }: ChatBaseProps) { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [user, , streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] + [user, chat, streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] ); // Restart auto-scrolling and resume a paused response when Follow Chat is clicked From c8a56afec861c2501b3099b87a163330352a53e0 Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Mon, 17 Feb 2025 23:15:07 -0500 Subject: [PATCH 3/8] completion function in ChatCraftChat class --- src/Chat/ChatBase.tsx | 322 +++++++++++++++++++++------------------ src/lib/ChatCraftChat.ts | 160 +++++++++++++++---- 2 files changed, 305 insertions(+), 177 deletions(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index cd120bfba..23c4d2bbc 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -29,13 +29,13 @@ import { useModels } from "../hooks/use-models"; import { useSettings } from "../hooks/use-settings"; import { useUser } from "../hooks/use-user"; import { ChatCraftChat } from "../lib/ChatCraftChat"; -import { ChatCraftCommand } from "../lib/ChatCraftCommand"; -import { ChatCraftFunction } from "../lib/ChatCraftFunction"; -import { ChatCraftFunctionCallMessage, ChatCraftHumanMessage } from "../lib/ChatCraftMessage"; -import { WebHandler } from "../lib/WebHandler"; -import { ChatCraftCommandRegistry } from "../lib/commands"; +// import { ChatCraftCommand } from "../lib/ChatCraftCommand"; +// import { ChatCraftFunction } from "../lib/ChatCraftFunction"; +// import { ChatCraftFunctionCallMessage, ChatCraftHumanMessage } from "../lib/ChatCraftMessage"; +// import { WebHandler } from "../lib/WebHandler"; +// import { ChatCraftCommandRegistry } from "../lib/commands"; import ChatHeader from "./ChatHeader"; -import { ChatCompletionError } from "../lib/ai"; +// import { ChatCompletionError } from "../lib/ai"; type ChatBaseProps = { chat: ChatCraftChat; @@ -206,155 +206,175 @@ function ChatBase({ chat }: ChatBaseProps) { async (prompt?: string, imageUrls?: string[]) => { setLoading(true); - // Special-case for "help", to invoke /help command - if (prompt?.toLowerCase() === "help") { - prompt = "/help"; - } - - // If we have a web handler registered for this url - const handler = WebHandler.getMatchingHandler(prompt ?? ""); - - if (prompt && handler) { - try { - const result = await handler.executeHandler(prompt); - - chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); - forceScroll(); - } catch (err: any) { - error({ - title: "Error running Web Handler", - message: err.message, - }); - } - - setLoading(false); - return; - } - - // If this is a slash command, execute that instead of prompting LLM - if (prompt && ChatCraftCommandRegistry.isCommand(prompt)) { - const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); - if (commandFunction) { - setShouldAutoScroll(true); - try { - await commandFunction(chat, user); - forceScroll(); - } catch (err: any) { - error({ - title: `Error Running Command`, - message: `There was an error running the command: ${err.message}.`, - }); - } - } else { - // The input was a command, but not a recognized one. - // Handle this case as appropriate for your application. - - // We are sure that this won't return null - // since prompt is definitely a command - const { command } = ChatCraftCommand.parseCommand(prompt)!; - const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; - setShouldAutoScroll(true); - try { - await commandFunction(chat, user); - forceScroll(); - } catch (err: any) { - error({ - title: `Error Running Command`, - message: `There was an error running the command: ${err.message}.`, - }); - } - } - - setLoading(false); - return; - } - - // Not a slash command, so pass this prompt to LLM - let promptMessage: ChatCraftHumanMessage | undefined; try { - // If the prompt text exist, package it up as a human message and add to the chat - if (prompt) { - // Add this prompt message to the chat - promptMessage = new ChatCraftHumanMessage({ text: prompt, imageUrls, user }); - await chat.addMessage(promptMessage); - } else if (imageUrls?.length) { - // Add only image to the chat - promptMessage = new ChatCraftHumanMessage({ text: "", imageUrls, user }); - await chat.addMessage(promptMessage); - } - - // If there's any problem loading referenced functions, show an error - const onError = (err: Error) => { - error({ - title: `Error Loading Function`, - message: err.message, - }); - }; - - // If there are any functions mentioned in the chat (via @fn or @fn-url), - // pass those through to the LLM to use if necessary. - const functions = await chat.functions(onError); - - // If the user has specified a single function in this prompt, ask LLM to call it. - let functionToCall: ChatCraftFunction | undefined; - if (promptMessage && functions) { - const messageFunctions = await promptMessage.functions(onError); - if (messageFunctions?.length === 1) { - functionToCall = messageFunctions[0]; - } - } - - // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. - const messages = chat.messages({ includeAppMessages: false }); - - // Clear any previous audio clips - clearAudioQueue(); - - const response = await callChatApi(messages, { - functions, - functionToCall, - }); - - // Add this response message to the chat - await chat.addMessage(response); - - // If it's a function call message, invoke the function - if (response instanceof ChatCraftFunctionCallMessage) { - const func = await ChatCraftFunction.find(response.func.id); - if (!func) { - error({ - title: `Function Error`, - message: `No such function: ${response.func.name} (${response.func.id}`, - }); - return; - } - - const result = await func.invoke(response.func.params); - // Add this result message to the chat - await chat.addMessage(result); - - // If the user has opted to always send function results back to LLM, do it now - if (settings.alwaysSendFunctionResult) { - await onPrompt(); - } - - forceScroll(); - } - } catch (err: any) { - if (err instanceof ChatCompletionError && err.incompleteResponse) { - // Add this partial response to the chat - await chat.addMessage(err.incompleteResponse); - } - - error({ - title: `Response Error`, - message: err.message, - }); - console.error(err); + await chat.completion( + prompt ?? "", + chat, + user, + settings, + clearAudioQueue, + callChatApi, + forceScroll, + error, + imageUrls + ); + } catch (err) { + console.error("Error during completion: ", err); } finally { setLoading(false); - setShouldAutoScroll(false); } + + // //Special-case for "help", to invoke /help command + // if (prompt?.toLowerCase() === "help") { + // prompt = "/help"; + // } + + // // If we have a web handler registered for this url + // const handler = WebHandler.getMatchingHandler(prompt ?? ""); + + // if (prompt && handler) { + // try { + // const result = await handler.executeHandler(prompt); + + // chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); + // forceScroll(); + // } catch (err: any) { + // error({ + // title: "Error running Web Handler", + // message: err.message, + // }); + // } + + // setLoading(false); + // return; + // } + + // // If this is a slash command, execute that instead of prompting LLM + // if (prompt && ChatCraftCommandRegistry.isCommand(prompt)) { + // const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); + // if (commandFunction) { + // setShouldAutoScroll(true); + // try { + // await commandFunction(chat, user); + // forceScroll(); + // } catch (err: any) { + // error({ + // title: `Error Running Command`, + // message: `There was an error running the command: ${err.message}.`, + // }); + // } + // } else { + // // The input was a command, but not a recognized one. + // // Handle this case as appropriate for your application. + + // // We are sure that this won't return null + // // since prompt is definitely a command + // const { command } = ChatCraftCommand.parseCommand(prompt)!; + // const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; + // setShouldAutoScroll(true); + // console.log(prompt + "aaasssss"); + + // try { + // await commandFunction(chat, user); + // forceScroll(); + // } catch (err: any) { + // error({ + // title: `Error Running Command`, + // message: `There was an error running the command: ${err.message}.`, + // }); + // } + // } + + // setLoading(false); + // return; + // } + + // // Not a slash command, so pass this prompt to LLM + // let promptMessage: ChatCraftHumanMessage | undefined; + // try { + // // If the prompt text exist, package it up as a human message and add to the chat + // if (prompt) { + // // Add this prompt message to the chat + // promptMessage = new ChatCraftHumanMessage({ text: prompt, imageUrls, user }); + // await chat.addMessage(promptMessage); + // } else if (imageUrls?.length) { + // // Add only image to the chat + // promptMessage = new ChatCraftHumanMessage({ text: "", imageUrls, user }); + // await chat.addMessage(promptMessage); + // } + + // // If there's any problem loading referenced functions, show an error + // const onError = (err: Error) => { + // error({ + // title: `Error Loading Function`, + // message: err.message, + // }); + // }; + + // // If there are any functions mentioned in the chat (via @fn or @fn-url), + // // pass those through to the LLM to use if necessary. + // const functions = await chat.functions(onError); + + // // If the user has specified a single function in this prompt, ask LLM to call it. + // let functionToCall: ChatCraftFunction | undefined; + // if (promptMessage && functions) { + // const messageFunctions = await promptMessage.functions(onError); + // if (messageFunctions?.length === 1) { + // functionToCall = messageFunctions[0]; + // } + // } + + // // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. + // const messages = chat.messages({ includeAppMessages: false }); + + // // Clear any previous audio clips + // clearAudioQueue(); + + // const response = await callChatApi(messages, { + // functions, + // functionToCall, + // }); + + // // Add this response message to the chat + // await chat.addMessage(response); + + // // If it's a function call message, invoke the function + // if (response instanceof ChatCraftFunctionCallMessage) { + // const func = await ChatCraftFunction.find(response.func.id); + // if (!func) { + // error({ + // title: `Function Error`, + // message: `No such function: ${response.func.name} (${response.func.id}`, + // }); + // return; + // } + + // const result = await func.invoke(response.func.params); + // // Add this result message to the chat + // await chat.addMessage(result); + + // // If the user has opted to always send function results back to LLM, do it now + // if (settings.alwaysSendFunctionResult) { + // await onPrompt(); + // } + + // forceScroll(); + // } + // } catch (err: any) { + // if (err instanceof ChatCompletionError && err.incompleteResponse) { + // // Add this partial response to the chat + // await chat.addMessage(err.incompleteResponse); + // } + + // error({ + // title: `Response Error`, + // message: err.message, + // }); + // console.error(err); + // } finally { + // setLoading(false); + // setShouldAutoScroll(false); + // } }, // eslint-disable-next-line react-hooks/exhaustive-deps [user, chat, streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index 8e1346a72..793833e27 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -4,6 +4,7 @@ import * as yaml from "yaml"; import { ChatCraftAiMessage, ChatCraftAppMessage, + ChatCraftFunctionCallMessage, ChatCraftHumanMessage, ChatCraftMessage, ChatCraftSystemMessage, @@ -20,11 +21,12 @@ import summarize from "./summarize"; import { createSystemMessage } from "./system-prompt"; import { createDataShareUrl, createShare } from "./share"; import { SharedChatCraftChat } from "./SharedChatCraftChat"; -import { countTokensInMessages } from "./ai"; -import { parseFunctionNames, loadFunctions } from "./ChatCraftFunction"; +import { ChatCompletionError, countTokensInMessages } from "./ai"; +import { parseFunctionNames, loadFunctions, ChatCraftFunction } from "./ChatCraftFunction"; import { ChatCraftFile } from "./ChatCraftFile"; -import { ChatCraftCommandRegistry } from "./ChatCraftCommandRegistry"; import { ChatCraftCommand } from "./ChatCraftCommand"; +import { WebHandler } from "../lib/WebHandler"; +import { ChatCraftCommandRegistry } from "../lib/commands"; export type SerializedChatCraftChat = { id: string; @@ -32,7 +34,6 @@ export type SerializedChatCraftChat = { summary?: string; messages: SerializedChatCraftMessage[]; }; - function createSummary(chat: ChatCraftChat, maxLength = 200) { // We only want to consider human prompts and ai responses for our summary const messages = chat @@ -92,7 +93,6 @@ export class ChatCraftChat { if (!files || !fileRefs) { throw new Error("Both files and fileRefs must be provided together"); } - const fileIds = new Set(files.map((f) => f.id)); const refIds = new Set(fileRefs.map((r) => r.id)); const _files = new Map(); @@ -125,50 +125,158 @@ export class ChatCraftChat { prompt: string, chat: ChatCraftChat, user: any, + settings: any, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type, prettier/prettier + clearAudioQueue: Function, callChatApi: Function, forceScroll: Function, - error: Function + error: Function, + imageUrls?: string[] ) { - try { - // Handle commands (slash commands) - if (ChatCraftCommandRegistry.isCommand(prompt)) { - const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); - if (commandFunction) { + // Special-case for "help", to invoke /help command + if (prompt?.toLowerCase() === "help") { + prompt = "/help"; + } + // If we have a web handler registered for this url + const handler = WebHandler.getMatchingHandler(prompt ?? ""); + + if (prompt && handler) { + try { + const result = await handler.executeHandler(prompt); + + chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); + forceScroll(); + } catch (err: any) { + error({ + title: "Error running Web Handler", + message: err.message, + }); + } + return; + } + // If this is a slash command, execute that instead of prompting LLM + if (prompt && ChatCraftCommandRegistry.isCommand(prompt)) { + const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); + + if (commandFunction) { + try { await commandFunction(chat, user); forceScroll(); - } else { - // If command is not recognized - const { command } = ChatCraftCommand.parseCommand(prompt)!; - const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; + } catch (err: any) { + error({ + title: `Error Running Command`, + message: `There was an error running the command: ${err.message}.`, + }); + } + } else { + // The input was a command, but not a recognized one. + // Handle this case as appropriate for your application. + + // We are sure that this won't return null + // since prompt is definitely a command + const { command } = ChatCraftCommand.parseCommand(prompt)!; + const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; + try { await commandFunction(chat, user); forceScroll(); + } catch (err: any) { + error({ + title: `Error Running Command`, + message: `There was an error running the command: ${err.message}.`, + }); } - return; + } + return; + } + try { + let promptMessage: ChatCraftHumanMessage | undefined; + if (prompt) { + // Add this prompt message to the chat + promptMessage = new ChatCraftHumanMessage({ text: prompt, imageUrls, user }); + await chat.addMessage(promptMessage); + } else if (imageUrls?.length) { + // Add only image to the chat + promptMessage = new ChatCraftHumanMessage({ text: "", imageUrls, user }); + await chat.addMessage(promptMessage); } - // If not a command, treat as a prompt to LLM (or another AI model) - const promptMessage = new ChatCraftHumanMessage({ text: prompt, user }); - await chat.addMessage(promptMessage); - - // Handle any functions associated with the prompt - const functions = await chat.functions((err) => + // If there's any problem loading referenced functions, show an error + const onError = (err: Error) => { error({ title: `Error Loading Function`, message: err.message, - }) - ); + }); + }; + + // If there are any functions mentioned in the chat (via @fn or @fn-url), + // pass those through to the LLM to use if necessary. + const functions = await chat.functions(onError); + + // If the user has specified a single function in this prompt, ask LLM to call it. + let functionToCall: ChatCraftFunction | undefined; + if (promptMessage && functions) { + const messageFunctions = await promptMessage.functions(onError); + if (messageFunctions?.length === 1) { + functionToCall = messageFunctions[0]; + } + } + // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. const messages = chat.messages({ includeAppMessages: false }); - const response = await callChatApi(messages, { functions }); + // Clear any previous audio clips + clearAudioQueue(); + + const response = await callChatApi(messages, { + functions, + functionToCall, + }); + + // Add this response message to the chat await chat.addMessage(response); - //forceScroll(); + + // If it's a function call message, invoke the function + if (response instanceof ChatCraftFunctionCallMessage) { + const func = await ChatCraftFunction.find(response.func.id); + if (!func) { + error({ + title: `Function Error`, + message: `No such function: ${response.func.name} (${response.func.id}`, + }); + return; + } + + const result = await func.invoke(response.func.params); + // Add this result message to the chat + await chat.addMessage(result); + + // If the user has opted to always send function results back to LLM, do it now + if (settings.alwaysSendFunctionResult) { + await chat.completion( + prompt ?? "", + chat, + user, + settings, + clearAudioQueue, + callChatApi, + forceScroll, + error + ); + } + + forceScroll(); + } } catch (err: any) { + if (err instanceof ChatCompletionError && err.incompleteResponse) { + // Add this partial response to the chat + await chat.addMessage(err.incompleteResponse); + } + error({ title: `Response Error`, message: err.message, }); + console.error(err); } } From 262811e110479714fd4fd90367441a07359a1cb9 Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Mon, 17 Feb 2025 23:38:39 -0500 Subject: [PATCH 4/8] clean ups cleanup refactoring --- src/Chat/ChatBase.tsx | 158 --------------------------------------- src/lib/ChatCraftChat.ts | 2 +- 2 files changed, 1 insertion(+), 159 deletions(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index 23c4d2bbc..639d4c6c6 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -29,13 +29,7 @@ import { useModels } from "../hooks/use-models"; import { useSettings } from "../hooks/use-settings"; import { useUser } from "../hooks/use-user"; import { ChatCraftChat } from "../lib/ChatCraftChat"; -// import { ChatCraftCommand } from "../lib/ChatCraftCommand"; -// import { ChatCraftFunction } from "../lib/ChatCraftFunction"; -// import { ChatCraftFunctionCallMessage, ChatCraftHumanMessage } from "../lib/ChatCraftMessage"; -// import { WebHandler } from "../lib/WebHandler"; -// import { ChatCraftCommandRegistry } from "../lib/commands"; import ChatHeader from "./ChatHeader"; -// import { ChatCompletionError } from "../lib/ai"; type ChatBaseProps = { chat: ChatCraftChat; @@ -223,158 +217,6 @@ function ChatBase({ chat }: ChatBaseProps) { } finally { setLoading(false); } - - // //Special-case for "help", to invoke /help command - // if (prompt?.toLowerCase() === "help") { - // prompt = "/help"; - // } - - // // If we have a web handler registered for this url - // const handler = WebHandler.getMatchingHandler(prompt ?? ""); - - // if (prompt && handler) { - // try { - // const result = await handler.executeHandler(prompt); - - // chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); - // forceScroll(); - // } catch (err: any) { - // error({ - // title: "Error running Web Handler", - // message: err.message, - // }); - // } - - // setLoading(false); - // return; - // } - - // // If this is a slash command, execute that instead of prompting LLM - // if (prompt && ChatCraftCommandRegistry.isCommand(prompt)) { - // const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); - // if (commandFunction) { - // setShouldAutoScroll(true); - // try { - // await commandFunction(chat, user); - // forceScroll(); - // } catch (err: any) { - // error({ - // title: `Error Running Command`, - // message: `There was an error running the command: ${err.message}.`, - // }); - // } - // } else { - // // The input was a command, but not a recognized one. - // // Handle this case as appropriate for your application. - - // // We are sure that this won't return null - // // since prompt is definitely a command - // const { command } = ChatCraftCommand.parseCommand(prompt)!; - // const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; - // setShouldAutoScroll(true); - // console.log(prompt + "aaasssss"); - - // try { - // await commandFunction(chat, user); - // forceScroll(); - // } catch (err: any) { - // error({ - // title: `Error Running Command`, - // message: `There was an error running the command: ${err.message}.`, - // }); - // } - // } - - // setLoading(false); - // return; - // } - - // // Not a slash command, so pass this prompt to LLM - // let promptMessage: ChatCraftHumanMessage | undefined; - // try { - // // If the prompt text exist, package it up as a human message and add to the chat - // if (prompt) { - // // Add this prompt message to the chat - // promptMessage = new ChatCraftHumanMessage({ text: prompt, imageUrls, user }); - // await chat.addMessage(promptMessage); - // } else if (imageUrls?.length) { - // // Add only image to the chat - // promptMessage = new ChatCraftHumanMessage({ text: "", imageUrls, user }); - // await chat.addMessage(promptMessage); - // } - - // // If there's any problem loading referenced functions, show an error - // const onError = (err: Error) => { - // error({ - // title: `Error Loading Function`, - // message: err.message, - // }); - // }; - - // // If there are any functions mentioned in the chat (via @fn or @fn-url), - // // pass those through to the LLM to use if necessary. - // const functions = await chat.functions(onError); - - // // If the user has specified a single function in this prompt, ask LLM to call it. - // let functionToCall: ChatCraftFunction | undefined; - // if (promptMessage && functions) { - // const messageFunctions = await promptMessage.functions(onError); - // if (messageFunctions?.length === 1) { - // functionToCall = messageFunctions[0]; - // } - // } - - // // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. - // const messages = chat.messages({ includeAppMessages: false }); - - // // Clear any previous audio clips - // clearAudioQueue(); - - // const response = await callChatApi(messages, { - // functions, - // functionToCall, - // }); - - // // Add this response message to the chat - // await chat.addMessage(response); - - // // If it's a function call message, invoke the function - // if (response instanceof ChatCraftFunctionCallMessage) { - // const func = await ChatCraftFunction.find(response.func.id); - // if (!func) { - // error({ - // title: `Function Error`, - // message: `No such function: ${response.func.name} (${response.func.id}`, - // }); - // return; - // } - - // const result = await func.invoke(response.func.params); - // // Add this result message to the chat - // await chat.addMessage(result); - - // // If the user has opted to always send function results back to LLM, do it now - // if (settings.alwaysSendFunctionResult) { - // await onPrompt(); - // } - - // forceScroll(); - // } - // } catch (err: any) { - // if (err instanceof ChatCompletionError && err.incompleteResponse) { - // // Add this partial response to the chat - // await chat.addMessage(err.incompleteResponse); - // } - - // error({ - // title: `Response Error`, - // message: err.message, - // }); - // console.error(err); - // } finally { - // setLoading(false); - // setShouldAutoScroll(false); - // } }, // eslint-disable-next-line react-hooks/exhaustive-deps [user, chat, streamingMessage, setLoading, setShouldAutoScroll, callChatApi, error] diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index 793833e27..e9932ada5 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -126,7 +126,7 @@ export class ChatCraftChat { chat: ChatCraftChat, user: any, settings: any, - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type, prettier/prettier + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type clearAudioQueue: Function, callChatApi: Function, forceScroll: Function, From 065954f9029561c3f36189f498a0c8bb15712dc4 Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Mon, 17 Feb 2025 23:59:34 -0500 Subject: [PATCH 5/8] removed UI forcescrolls --- src/Chat/ChatBase.tsx | 14 +++----------- src/lib/ChatCraftChat.ts | 21 +-------------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index 639d4c6c6..f1ac6e26d 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -201,21 +201,13 @@ function ChatBase({ chat }: ChatBaseProps) { setLoading(true); try { - await chat.completion( - prompt ?? "", - chat, - user, - settings, - clearAudioQueue, - callChatApi, - forceScroll, - error, - imageUrls - ); + await chat.completion(prompt ?? "", chat, user, settings, callChatApi, error, imageUrls); } catch (err) { console.error("Error during completion: ", err); } finally { setLoading(false); + // Clear any previous audio clips + clearAudioQueue(); } }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index e9932ada5..5e1cfe0b8 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -127,9 +127,7 @@ export class ChatCraftChat { user: any, settings: any, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - clearAudioQueue: Function, callChatApi: Function, - forceScroll: Function, error: Function, imageUrls?: string[] ) { @@ -145,7 +143,6 @@ export class ChatCraftChat { const result = await handler.executeHandler(prompt); chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); - forceScroll(); } catch (err: any) { error({ title: "Error running Web Handler", @@ -161,7 +158,6 @@ export class ChatCraftChat { if (commandFunction) { try { await commandFunction(chat, user); - forceScroll(); } catch (err: any) { error({ title: `Error Running Command`, @@ -178,7 +174,6 @@ export class ChatCraftChat { const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; try { await commandFunction(chat, user); - forceScroll(); } catch (err: any) { error({ title: `Error Running Command`, @@ -224,9 +219,6 @@ export class ChatCraftChat { // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. const messages = chat.messages({ includeAppMessages: false }); - // Clear any previous audio clips - clearAudioQueue(); - const response = await callChatApi(messages, { functions, functionToCall, @@ -252,19 +244,8 @@ export class ChatCraftChat { // If the user has opted to always send function results back to LLM, do it now if (settings.alwaysSendFunctionResult) { - await chat.completion( - prompt ?? "", - chat, - user, - settings, - clearAudioQueue, - callChatApi, - forceScroll, - error - ); + await chat.completion(prompt ?? "", chat, user, settings, callChatApi, error); } - - forceScroll(); } } catch (err: any) { if (err instanceof ChatCompletionError && err.incompleteResponse) { From 94782b5a6164cf026668a928b28a19455a825583 Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Tue, 18 Feb 2025 03:03:23 -0500 Subject: [PATCH 6/8] added eslint ignore for now and use error pop up instead of console --- src/Chat/ChatBase.tsx | 5 ++++- src/lib/ChatCraftChat.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index f1ac6e26d..daf0f874d 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -203,7 +203,10 @@ function ChatBase({ chat }: ChatBaseProps) { try { await chat.completion(prompt ?? "", chat, user, settings, callChatApi, error, imageUrls); } catch (err) { - console.error("Error during completion: ", err); + error({ + title: `Completion Error`, + message: `Error with chat completion: ${err}`, + }); } finally { setLoading(false); // Clear any previous audio clips diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index 5e1cfe0b8..890eebb3b 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -128,6 +128,7 @@ export class ChatCraftChat { settings: any, // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type callChatApi: Function, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type error: Function, imageUrls?: string[] ) { From 7daf6cc0712696d489a041c795dadc4fc0120caa Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Wed, 19 Feb 2025 20:23:47 -0500 Subject: [PATCH 7/8] chat completion hook cleanup --- src/Chat/ChatBase.tsx | 4 +- src/hooks/use-chat-completion.tsx | 152 ++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/hooks/use-chat-completion.tsx diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index daf0f874d..f6f13599e 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -28,6 +28,7 @@ import useChatOpenAI from "../hooks/use-chat-openai"; import { useModels } from "../hooks/use-models"; import { useSettings } from "../hooks/use-settings"; import { useUser } from "../hooks/use-user"; +import useChatCompletion from "../hooks/use-chat-completion"; import { ChatCraftChat } from "../lib/ChatCraftChat"; import ChatHeader from "./ChatHeader"; @@ -39,6 +40,7 @@ function ChatBase({ chat }: ChatBaseProps) { const { error: apiError } = useModels(); // When chatting with OpenAI, a streaming message is returned during loading const { streamingMessage, callChatApi, cancel, paused, resume, togglePause } = useChatOpenAI(); + const { chatCompletion } = useChatCompletion(); const { settings, setSettings } = useSettings(); const { isOpen: isSidebarVisible, onToggle: toggleSidebarVisible } = useDisclosure({ defaultIsOpen: settings.sidebarVisible, @@ -201,7 +203,7 @@ function ChatBase({ chat }: ChatBaseProps) { setLoading(true); try { - await chat.completion(prompt ?? "", chat, user, settings, callChatApi, error, imageUrls); + await chatCompletion(prompt ?? "", chat, imageUrls); } catch (err) { error({ title: `Completion Error`, diff --git a/src/hooks/use-chat-completion.tsx b/src/hooks/use-chat-completion.tsx new file mode 100644 index 000000000..9594b6f51 --- /dev/null +++ b/src/hooks/use-chat-completion.tsx @@ -0,0 +1,152 @@ +import { ChatCraftFunctionCallMessage, ChatCraftHumanMessage } from "../lib/ChatCraftMessage"; +import { ChatCraftFunction } from "../lib/ChatCraftFunction"; +import { ChatCraftCommand } from "../lib/ChatCraftCommand"; +import { WebHandler } from "../lib/WebHandler"; +import { ChatCraftCommandRegistry } from "../lib/commands"; +import { ChatCraftChat } from "../lib/ChatCraftChat"; +import { useAlert } from "../hooks/use-alert"; +import useChatOpenAI from "./use-chat-openai"; +import { ChatCompletionError } from "../lib/ai"; +import { useUser } from "../hooks/use-user"; +import { useSettings } from "./use-settings"; + +function useChatCompletion() { + const { error } = useAlert(); + const { callChatApi } = useChatOpenAI(); + const { user } = useUser(); + const { settings } = useSettings(); + + const chatCompletion = async (prompt: string, chat: ChatCraftChat, imageUrls?: string[]) => { + // Special-case for "help", to invoke /help command + if (prompt?.toLowerCase() === "help") { + prompt = "/help"; + } + // If we have a web handler registered for this url + const handler = WebHandler.getMatchingHandler(prompt ?? ""); + + if (prompt && handler) { + try { + const result = await handler.executeHandler(prompt); + + chat.addMessage(new ChatCraftHumanMessage({ user, text: result })); + } catch (err: any) { + error({ + title: "Error running Web Handler", + message: err.message, + }); + } + return; + } + // If this is a slash command, execute that instead of prompting LLM + if (prompt && ChatCraftCommandRegistry.isCommand(prompt)) { + const commandFunction = ChatCraftCommandRegistry.getCommand(prompt); + + if (commandFunction) { + try { + await commandFunction(chat, user); + } catch (err: any) { + error({ + title: `Error Running Command`, + message: `There was an error running the command: ${err.message}.`, + }); + } + } else { + // The input was a command, but not a recognized one. + // Handle this case as appropriate for your application. + + // We are sure that this won't return null + // since prompt is definitely a command + const { command } = ChatCraftCommand.parseCommand(prompt)!; + const commandFunction = ChatCraftCommandRegistry.getCommand(`/commands ${command}`)!; + try { + await commandFunction(chat, user); + } catch (err: any) { + error({ + title: `Error Running Command`, + message: `There was an error running the command: ${err.message}.`, + }); + } + } + return; + } + try { + let promptMessage: ChatCraftHumanMessage | undefined; + if (prompt) { + // Add this prompt message to the chat + promptMessage = new ChatCraftHumanMessage({ text: prompt, imageUrls, user }); + await chat.addMessage(promptMessage); + } else if (imageUrls?.length) { + // Add only image to the chat + promptMessage = new ChatCraftHumanMessage({ text: "", imageUrls, user }); + await chat.addMessage(promptMessage); + } + + // If there's any problem loading referenced functions, show an error + const onError = (err: Error) => { + error({ + title: `Error Loading Function`, + message: err.message, + }); + }; + + // If there are any functions mentioned in the chat (via @fn or @fn-url), + // pass those through to the LLM to use if necessary. + const functions = await chat.functions(onError); + + // If the user has specified a single function in this prompt, ask LLM to call it. + let functionToCall: ChatCraftFunction | undefined; + if (promptMessage && functions) { + const messageFunctions = await promptMessage.functions(onError); + if (messageFunctions?.length === 1) { + functionToCall = messageFunctions[0]; + } + } + + // NOTE: we strip out the ChatCraft App messages before sending to OpenAI. + const messages = chat.messages({ includeAppMessages: false }); + + const response = await callChatApi(messages, { + functions, + functionToCall, + }); + + // Add this response message to the chat + await chat.addMessage(response); + + // If it's a function call message, invoke the function + if (response instanceof ChatCraftFunctionCallMessage) { + const func = await ChatCraftFunction.find(response.func.id); + if (!func) { + error({ + title: `Function Error`, + message: `No such function: ${response.func.name} (${response.func.id}`, + }); + return; + } + + const result = await func.invoke(response.func.params); + // Add this result message to the chat + await chat.addMessage(result); + + // If the user has opted to always send function results back to LLM, do it now + if (settings.alwaysSendFunctionResult) { + await chatCompletion(prompt ?? "", chat, imageUrls); + } + } + } catch (err: any) { + if (err instanceof ChatCompletionError && err.incompleteResponse) { + // Add this partial response to the chat + await chat.addMessage(err.incompleteResponse); + } + + error({ + title: `Response Error`, + message: err.message, + }); + } + }; + + return { chatCompletion }; +} + +export default useChatCompletion; From 0f55df1691f4b5e5a6439fe447a46b895215cd8b Mon Sep 17 00:00:00 2001 From: aldrin312 Date: Wed, 5 Mar 2025 21:22:10 -0500 Subject: [PATCH 8/8] arguement cleanups --- src/Chat/ChatBase.tsx | 2 +- src/hooks/use-chat-completion.tsx | 7 +++++-- src/lib/ChatCraftChat.ts | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Chat/ChatBase.tsx b/src/Chat/ChatBase.tsx index f6f13599e..e26094cc1 100644 --- a/src/Chat/ChatBase.tsx +++ b/src/Chat/ChatBase.tsx @@ -203,7 +203,7 @@ function ChatBase({ chat }: ChatBaseProps) { setLoading(true); try { - await chatCompletion(prompt ?? "", chat, imageUrls); + await chatCompletion(prompt, chat, imageUrls); } catch (err) { error({ title: `Completion Error`, diff --git a/src/hooks/use-chat-completion.tsx b/src/hooks/use-chat-completion.tsx index 9594b6f51..7324b0e7c 100644 --- a/src/hooks/use-chat-completion.tsx +++ b/src/hooks/use-chat-completion.tsx @@ -16,7 +16,10 @@ function useChatCompletion() { const { user } = useUser(); const { settings } = useSettings(); - const chatCompletion = async (prompt: string, chat: ChatCraftChat, imageUrls?: string[]) => { + const chatCompletion = async (prompt: string = "", chat: ChatCraftChat, imageUrls?: string[]) => { + if (!chat) { + throw new Error("Chat is not defined"); + } // Special-case for "help", to invoke /help command if (prompt?.toLowerCase() === "help") { prompt = "/help"; @@ -130,7 +133,7 @@ function useChatCompletion() { // If the user has opted to always send function results back to LLM, do it now if (settings.alwaysSendFunctionResult) { - await chatCompletion(prompt ?? "", chat, imageUrls); + await chatCompletion(prompt, chat, imageUrls); } } } catch (err: any) { diff --git a/src/lib/ChatCraftChat.ts b/src/lib/ChatCraftChat.ts index 890eebb3b..a8a76d8aa 100644 --- a/src/lib/ChatCraftChat.ts +++ b/src/lib/ChatCraftChat.ts @@ -122,7 +122,7 @@ export class ChatCraftChat { } async completion( - prompt: string, + prompt: string = "", chat: ChatCraftChat, user: any, settings: any, @@ -245,7 +245,7 @@ export class ChatCraftChat { // If the user has opted to always send function results back to LLM, do it now if (settings.alwaysSendFunctionResult) { - await chat.completion(prompt ?? "", chat, user, settings, callChatApi, error); + await chat.completion(prompt, chat, user, settings, callChatApi, error); } } } catch (err: any) {