From 3bfaf7c257b3d6dd25e7e9316738ecd76bb646a2 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Thu, 21 Nov 2024 19:12:44 +0100 Subject: [PATCH] Wip smart links button (#206) * wip(configuration chat): send first messages. * wip(configuration chat): tool use in configuration chat. --- src/components/Chat/Chat.tsx | 3 + .../IntegrationForm/IntegrationForm.tsx | 69 +++++++++- src/features/Chat/Thread/actions.ts | 25 +++- src/features/Chat/Thread/reducer.ts | 11 ++ src/features/Chat/Thread/selectors.ts | 2 + src/features/Chat/Thread/types.ts | 1 + src/hooks/useSendChatRequest.ts | 120 ++++++++++++------ src/services/refact/chat.ts | 10 +- src/services/refact/integrations.ts | 2 +- 9 files changed, 197 insertions(+), 46 deletions(-) diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index b1e66b67..5c6a3300 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -7,6 +7,7 @@ import { useAppDispatch, useSendChatRequest, useGetPromptsQuery, + useAutoSend, } from "../../hooks"; import type { Config } from "../../features/Config/configSlice"; import { @@ -57,6 +58,8 @@ export const Chat: React.FC = ({ const dispatch = useAppDispatch(); const messages = useAppSelector(selectMessages); + useAutoSend(); + const promptsRequest = useGetPromptsQuery(); const selectedSystemPrompt = useAppSelector(getSelectedSystemPrompt); const onSetSelectedSystemPrompt = (prompt: SystemPrompts) => diff --git a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx index b50f0579..167ad40c 100644 --- a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx +++ b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx @@ -1,12 +1,15 @@ -import { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import classNames from "classnames"; import { useGetIntegrationDataByPathQuery } from "../../../hooks/useGetIntegrationDataByPathQuery"; import type { FC, FormEvent } from "react"; import type { + // ChatMessage, + ChatMessages, Integration, IntegrationField, IntegrationPrimitive, + // UserMessage, } from "../../../services/refact"; import styles from "./IntegrationForm.module.css"; @@ -18,6 +21,18 @@ import { CustomLabel, } from "../CustomFieldsAndWidgets"; import { toPascalCase } from "../../../utils/toPascalCase"; +import { type SmartLink } from "../../../services/refact"; +import { + useAppDispatch, + useAppSelector, + useSendChatRequest, +} from "../../../hooks"; +import { + setIsConfigFlag, + setToolUse, +} from "../../../features/Chat/Thread/actions"; +import { push } from "../../../features/Pages/pagesSlice"; +import { selectChatId } from "../../../features/Chat"; type IntegrationFormProps = { integrationPath: string; @@ -136,6 +151,58 @@ export const IntegrationForm: FC = ({ + {/** smart links */} +
+

Smart Links

+ {integration.data.integr_schema.smartlinks.map((smartlink, index) => { + return ; + })} +
); }; + +const SmartLink: React.FC<{ smartlink: SmartLink }> = ({ smartlink }) => { + // TODO: send chat on click and navigate away + const dispatch = useAppDispatch(); + const chatId = useAppSelector(selectChatId); + + const { sendMessages } = useSendChatRequest(); + const handleClick = React.useCallback(() => { + const messages = (smartlink.sl_chat ?? []).reduce( + (acc, message) => { + if (message.role === "user" && typeof message.content === "string") { + return [...acc, { role: message.role, content: message.content }]; + } + + // TODO: Other types. + return acc; + }, + [], + ); + + // dispatch(newChatAction()); id is out of date + dispatch(setToolUse("agent")); + dispatch(setIsConfigFlag({ id: chatId, isConfig: true })); + // TODO: make another version of send messages so there's no need to converting the messages + // eslint-disable-next-line no-console + void sendMessages(messages) + .then(() => { + dispatch(push({ name: "chat" })); + }) + // eslint-disable-next-line no-console + .catch(console.error); + }, [chatId, dispatch, sendMessages, smartlink.sl_chat]); + + const title = (smartlink.sl_chat ?? []).reduce((acc, cur) => { + if (typeof cur.content === "string") + return [...acc, `${cur.role}: ${cur.content}`]; + return acc; + }, []); + + return ( + + ); +}; diff --git a/src/features/Chat/Thread/actions.ts b/src/features/Chat/Thread/actions.ts index f00fc2a7..900a5cd8 100644 --- a/src/features/Chat/Thread/actions.ts +++ b/src/features/Chat/Thread/actions.ts @@ -34,7 +34,9 @@ export const chatAskedQuestion = createAction( ); export const backUpMessages = createAction< - PayloadWithId & { messages: ChatThread["messages"] } + PayloadWithId & { + messages: ChatThread["messages"]; + } >("chatThread/backUpMessages"); // TODO: add history actions to this, maybe not used any more @@ -75,6 +77,10 @@ export const setToolUse = createAction("chatThread/setToolUse"); export const saveTitle = createAction( "chatThread/saveTitle", ); + +export const setIsConfigFlag = createAction< + PayloadWithId & { isConfig: boolean } +>("chatThread/setConfig"); // TODO: This is the circular dep when imported from hooks :/ const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: RootState; @@ -186,17 +192,26 @@ function checkForToolLoop(message: ChatMessages): boolean { return hasDuplicates; } - +// TODO: add props for config chat export const chatAskQuestionThunk = createAppAsyncThunk< unknown, { messages: ChatMessages; chatId: string; tools: ToolCommand[] | null; + // TODO: make a separate function for this... and it'll need to be saved. } >("chatThread/sendChat", ({ messages, chatId, tools }, thunkAPI) => { const state = thunkAPI.getState(); + const thread = + chatId in state.chat.cache + ? state.chat.cache[chatId] + : state.chat.thread.id === chatId + ? state.chat.thread + : null; + const isConfig = thread?.isConfig ?? false; + const onlyDeterministicMessages = checkForToolLoop(messages); const messagesForLsp = formatMessagesForLsp(messages); @@ -211,6 +226,7 @@ export const chatAskQuestionThunk = createAppAsyncThunk< apiKey: state.config.apiKey, port: state.config.lspPort, onlyDeterministicMessages, + isConfig, }) .then((response) => { if (!response.ok) { @@ -236,3 +252,8 @@ export const chatAskQuestionThunk = createAppAsyncThunk< thunkAPI.dispatch(doneStreaming({ id: chatId })); }); }); + +// export const sendConfigurationThunk = createAppAsyncThunk( +// "chatThread/checkConfiguration", + +// ); diff --git a/src/features/Chat/Thread/reducer.ts b/src/features/Chat/Thread/reducer.ts index 745eafbf..5a7cfe9f 100644 --- a/src/features/Chat/Thread/reducer.ts +++ b/src/features/Chat/Thread/reducer.ts @@ -16,6 +16,7 @@ import { restoreChat, setPreventSend, saveTitle, + setIsConfigFlag, } from "./actions"; import { formatChatResponse } from "./utils"; @@ -26,6 +27,7 @@ const createChatThread = (tool_use: ToolUse): ChatThread => { title: "", model: "", tool_use, + isConfig: false, }; return chat; }; @@ -182,4 +184,13 @@ export const chatReducer = createReducer(initialState, (builder) => { state.thread.title = action.payload.title; state.thread.isTitleGenerated = action.payload.isTitleGenerated; }); + + builder.addCase(setIsConfigFlag, (state, action) => { + if (state.thread.id === action.payload.id) { + state.thread.isConfig = action.payload.isConfig; + } + if (action.payload.id in state.cache) { + state.cache[action.payload.id].isConfig = action.payload.isConfig; + } + }); }); diff --git a/src/features/Chat/Thread/selectors.ts b/src/features/Chat/Thread/selectors.ts index 41ad043e..aee0073a 100644 --- a/src/features/Chat/Thread/selectors.ts +++ b/src/features/Chat/Thread/selectors.ts @@ -8,6 +8,8 @@ export const selectChatId = (state: RootState) => state.chat.thread.id; export const selectModel = (state: RootState) => state.chat.thread.model; export const selectMessages = (state: RootState) => state.chat.thread.messages; export const selectToolUse = (state: RootState) => state.chat.tool_use; +export const selectThreadToolUse = (state: RootState) => + state.chat.thread.tool_use; export const selectIsWaiting = (state: RootState) => state.chat.waiting_for_response; export const selectIsStreaming = (state: RootState) => state.chat.streaming; diff --git a/src/features/Chat/Thread/types.ts b/src/features/Chat/Thread/types.ts index 40bf89be..69831889 100644 --- a/src/features/Chat/Thread/types.ts +++ b/src/features/Chat/Thread/types.ts @@ -12,6 +12,7 @@ export type ChatThread = { tool_use?: ToolUse; read?: boolean; isTitleGenerated?: boolean; + isConfig?: boolean; }; export type ToolUse = "quick" | "explore" | "agent"; diff --git a/src/hooks/useSendChatRequest.ts b/src/hooks/useSendChatRequest.ts index bc5a70f5..47928a3f 100644 --- a/src/hooks/useSendChatRequest.ts +++ b/src/hooks/useSendChatRequest.ts @@ -10,7 +10,7 @@ import { selectMessages, selectPreventSend, selectSendImmediately, - selectToolUse, + selectThreadToolUse, } from "../features/Chat/Thread/selectors"; import { useCheckForConfirmationMutation, @@ -27,9 +27,8 @@ import { backUpMessages, chatAskQuestionThunk, chatAskedQuestion, - setToolUse, } from "../features/Chat/Thread/actions"; -import { isToolUse } from "../features/Chat"; + import { selectAllImages } from "../features/AttachedImages"; import { useAbortControllers } from "./useAbortControllers"; import { @@ -42,24 +41,24 @@ let recallCounter = 0; export const useSendChatRequest = () => { const dispatch = useAppDispatch(); - const hasError = useAppSelector(selectChatError); + // const hasError = useAppSelector(selectChatError); const abortControllers = useAbortControllers(); const [triggerGetTools] = useGetToolsLazyQuery(); const [triggerCheckForConfirmation] = useCheckForConfirmationMutation(); const chatId = useAppSelector(selectChatId); - const streaming = useAppSelector(selectIsStreaming); - const chatError = useAppSelector(selectChatError); + // const streaming = useAppSelector(selectIsStreaming); + // const chatError = useAppSelector(selectChatError); - const errored: boolean = !!hasError || !!chatError; - const preventSend = useAppSelector(selectPreventSend); + // const errored: boolean = !!hasError || !!chatError; + // const preventSend = useAppSelector(selectPreventSend); const isWaiting = useAppSelector(selectIsWaiting); const currentMessages = useAppSelector(selectMessages); const systemPrompt = useAppSelector(getSelectedSystemPrompt); const sendImmediately = useAppSelector(selectSendImmediately); - const toolUse = useAppSelector(selectToolUse); + const toolUse = useAppSelector(selectThreadToolUse); const attachedImages = useAppSelector(selectAllImages); const areToolsConfirmed = useAppSelector(getToolsConfirmationStatus); @@ -79,9 +78,10 @@ export const useSendChatRequest = () => { const sendMessages = useCallback( async (messages: ChatMessages) => { let tools = await triggerGetTools(undefined).unwrap(); - if (isToolUse(toolUse)) { - dispatch(setToolUse(toolUse)); - } + // TODO: save tool use to state.chat + // if (toolUse && isToolUse(toolUse)) { + // dispatch(setToolUse(toolUse)); + // } if (toolUse === "quick") { tools = []; } else if (toolUse === "explore") { @@ -180,34 +180,34 @@ export const useSendChatRequest = () => { // TODO: Automatically calls tool calls. This means that this hook can only be used once :/ // TODO: Think of better way to manage useEffect calls, not with outer counter - useEffect(() => { - if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { - const lastMessage = currentMessages.slice(-1)[0]; - if ( - isAssistantMessage(lastMessage) && - lastMessage.tool_calls && - lastMessage.tool_calls.length > 0 - ) { - if (!areToolsConfirmed) { - abort(); - if (recallCounter < 1) { - recallCounter++; - return; - } - } - void sendMessages(currentMessages); - recallCounter = 0; - } - } - }, [ - errored, - currentMessages, - preventSend, - sendMessages, - abort, - streaming, - areToolsConfirmed, - ]); + // useEffect(() => { + // if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { + // const lastMessage = currentMessages.slice(-1)[0]; + // if ( + // isAssistantMessage(lastMessage) && + // lastMessage.tool_calls && + // lastMessage.tool_calls.length > 0 + // ) { + // if (!areToolsConfirmed) { + // abort(); + // if (recallCounter < 1) { + // recallCounter++; + // return; + // } + // } + // void sendMessages(currentMessages); + // recallCounter = 0; + // } + // } + // }, [ + // errored, + // currentMessages, + // preventSend, + // sendMessages, + // abort, + // streaming, + // areToolsConfirmed, + // ]); const retry = useCallback( (messages: ChatMessages) => { @@ -240,5 +240,45 @@ export const useSendChatRequest = () => { retry, retryFromIndex, confirmToolUsage, + sendMessages, }; }; + +// NOTE: only use this once +export function useAutoSend() { + const streaming = useAppSelector(selectIsStreaming); + const currentMessages = useAppSelector(selectMessages); + const errored = useAppSelector(selectChatError); + const preventSend = useAppSelector(selectPreventSend); + const areToolsConfirmed = useAppSelector(getToolsConfirmationStatus); + const { sendMessages, abort } = useSendChatRequest(); + + useEffect(() => { + if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { + const lastMessage = currentMessages.slice(-1)[0]; + if ( + isAssistantMessage(lastMessage) && + lastMessage.tool_calls && + lastMessage.tool_calls.length > 0 + ) { + if (!areToolsConfirmed) { + abort(); + if (recallCounter < 1) { + recallCounter++; + return; + } + } + void sendMessages(currentMessages); + recallCounter = 0; + } + } + }, [ + errored, + currentMessages, + preventSend, + sendMessages, + abort, + streaming, + areToolsConfirmed, + ]); +} diff --git a/src/services/refact/chat.ts b/src/services/refact/chat.ts index c3fdff6f..75a36ea3 100644 --- a/src/services/refact/chat.ts +++ b/src/services/refact/chat.ts @@ -41,6 +41,7 @@ type SendChatArgs = { tools: ToolCommand[] | null; port?: number; apiKey?: string | null; + isConfig?: boolean; } & StreamArgs; type GetChatTitleArgs = { @@ -94,7 +95,7 @@ export type Usage = { prompt_tokens: number; total_tokens: number; }; - +// TODO: add config url export async function sendChat({ messages, model, @@ -107,6 +108,7 @@ export async function sendChat({ tools, port = 8001, apiKey, + isConfig = false, }: SendChatArgs): Promise { // const toolsResponse = await getAvailableTools(); @@ -137,7 +139,11 @@ export async function sendChat({ ...(apiKey ? { Authorization: "Bearer " + apiKey } : {}), }; - const url = `http://127.0.0.1:${port}${CHAT_URL}`; + const url = `http://127.0.0.1:${port}${ + isConfig ? "/v1/chat-configuration" : CHAT_URL + }`; + + console.log({ isConfig }); return fetch(url, { method: "POST", diff --git a/src/services/refact/integrations.ts b/src/services/refact/integrations.ts index 34a5e19b..31dbf12b 100644 --- a/src/services/refact/integrations.ts +++ b/src/services/refact/integrations.ts @@ -244,7 +244,7 @@ function isIntegrationField( return true; } -type SmartLink = { +export type SmartLink = { sl_label: string; sl_chat?: LspChatMessage[]; sl_goto?: string;