From 5f8c1b67980632a228c4cada06c66d9728e66e5c Mon Sep 17 00:00:00 2001 From: Willy Douhard Date: Tue, 19 Dec 2023 20:38:03 +0100 Subject: [PATCH] do not ask confirmation for new chat if no interaction happened (#605) * do not ask confirmation for new chat if no interaction happened * fix test --- backend/chainlit/data/__init__.py | 2 +- backend/chainlit/emitter.py | 35 ++++++++++--------- backend/chainlit/session.py | 2 +- backend/chainlit/socket.py | 4 +-- .../e2e/chat_profiles/.chainlit/config.toml | 23 +++++++++--- cypress/e2e/chat_profiles/spec.cy.ts | 16 +++++++-- .../src/components/molecules/chatProfiles.tsx | 22 ++++++++---- libs/react-client/src/state.ts | 4 +-- libs/react-client/src/useChatInteract.ts | 6 ++-- libs/react-client/src/useChatMessages.ts | 6 ++-- libs/react-client/src/useChatSession.ts | 8 ++--- 11 files changed, 83 insertions(+), 45 deletions(-) diff --git a/backend/chainlit/data/__init__.py b/backend/chainlit/data/__init__.py index cc18a7b661..9d4a1f140d 100644 --- a/backend/chainlit/data/__init__.py +++ b/backend/chainlit/data/__init__.py @@ -29,7 +29,7 @@ def decorator(method): async def wrapper(self, *args, **kwargs): if ( isinstance(context.session, WebsocketSession) - and not context.session.has_user_message + and not context.session.has_first_interaction ): # Queue the method invocation waiting for the first user message queues = context.session.thread_queues diff --git a/backend/chainlit/emitter.py b/backend/chainlit/emitter.py index 4cf8168b64..bdf2b85901 100644 --- a/backend/chainlit/emitter.py +++ b/backend/chainlit/emitter.py @@ -64,8 +64,7 @@ async def clear_ask(self): """Stub method to clear the prompt from the UI.""" pass - async def init_thread(self, step_dict: StepDict): - """Signal the UI that a new thread (with a user message) exists""" + async def init_thread(self, interaction: str): pass async def process_user_message(self, payload: UIMessagePayload) -> Message: @@ -167,7 +166,7 @@ def clear_ask(self): return self.emit("clear_ask", {}) - async def flush_thread_queues(self, name: str): + async def flush_thread_queues(self, interaction: str): if data_layer := get_data_layer(): if isinstance(self.session.user, PersistedUser): user_id = self.session.user.id @@ -176,14 +175,13 @@ async def flush_thread_queues(self, name: str): await data_layer.update_thread( thread_id=self.session.thread_id, user_id=user_id, - metadata={"name": name}, + metadata={"name": interaction}, ) await self.session.flush_method_queue() - async def init_thread(self, step: StepDict): - """Signal the UI that a new thread (with a user message) exists""" - await self.flush_thread_queues(name=step["output"]) - await self.emit("init_thread", step) + async def init_thread(self, interaction: str): + await self.flush_thread_queues(interaction) + await self.emit("first_interaction", interaction) async def process_user_message(self, payload: UIMessagePayload): step_dict = payload["message"] @@ -197,9 +195,9 @@ async def process_user_message(self, payload: UIMessagePayload): asyncio.create_task(message._create()) - if not self.session.has_user_message: - self.session.has_user_message = True - asyncio.create_task(self.init_thread(message.to_dict())) + if not self.session.has_first_interaction: + self.session.has_first_interaction = True + asyncio.create_task(self.init_thread(message.content)) if file_refs: files = [ @@ -239,11 +237,13 @@ async def send_ask_user( ] = None if user_res: + interaction = None if spec.type == "text": message_dict_res = cast(StepDict, user_res) await self.process_user_message( {"message": message_dict_res, "fileReferences": None} ) + interaction = message_dict_res["output"] final_res = message_dict_res elif spec.type == "file": file_refs = cast(List[FileReference], user_res) @@ -253,12 +253,7 @@ async def send_ask_user( if file["id"] in self.session.files ] final_res = files - if not self.session.has_user_message: - self.session.has_user_message = True - await self.flush_thread_queues( - name=",".join([file["name"] for file in files]) - ) - + interaction = ",".join([file["name"] for file in files]) if get_data_layer(): coros = [ File( @@ -274,6 +269,12 @@ async def send_ask_user( elif spec.type == "action": action_res = cast(AskActionResponse, user_res) final_res = action_res + interaction = action_res["value"] + + if not self.session.has_first_interaction and interaction: + self.session.has_first_interaction = True + await self.init_thread(interaction=interaction) + await self.clear_ask() return final_res except TimeoutError as e: diff --git a/backend/chainlit/session.py b/backend/chainlit/session.py index 3c30c2567e..e6f8757299 100644 --- a/backend/chainlit/session.py +++ b/backend/chainlit/session.py @@ -56,7 +56,7 @@ def __init__( self.user = user self.token = token self.root_message = root_message - self.has_user_message = False + self.has_first_interaction = False self.user_env = user_env or {} self.chat_profile = chat_profile self.active_steps = [] diff --git a/backend/chainlit/socket.py b/backend/chainlit/socket.py index 90e77aca8e..54e9b0823c 100644 --- a/backend/chainlit/socket.py +++ b/backend/chainlit/socket.py @@ -138,7 +138,7 @@ async def connection_successful(sid): if context.session.thread_id_to_resume and config.code.on_chat_resume: thread = await resume_thread(context.session) if thread: - context.session.has_user_message = True + context.session.has_first_interaction = True await context.emitter.clear_ask() await context.emitter.resume_thread(thread) await config.code.on_chat_resume(thread) @@ -173,7 +173,7 @@ async def disconnect(sid): if config.code.on_chat_end and session: await config.code.on_chat_end() - if session and session.thread_id and session.has_user_message: + if session and session.thread_id and session.has_first_interaction: await persist_user_session(session.thread_id, session.to_persistable()) async def disconnect_on_timeout(sid): diff --git a/cypress/e2e/chat_profiles/.chainlit/config.toml b/cypress/e2e/chat_profiles/.chainlit/config.toml index 0ef3819e3e..f6ae6374f1 100644 --- a/cypress/e2e/chat_profiles/.chainlit/config.toml +++ b/cypress/e2e/chat_profiles/.chainlit/config.toml @@ -18,10 +18,28 @@ cache = false # Show the prompt playground prompt_playground = true +# Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript) +unsafe_allow_html = false + +# Process and display mathematical expressions. This can clash with "$" characters in messages. +latex = false + +# Authorize users to upload files with messages +multi_modal = true + +# Allows user to use speech to text +[features.speech_to_text] + enabled = false + # See all languages here https://github.com/JamesBrill/react-speech-recognition/blob/HEAD/docs/API.md#language-string + # language = "en-US" + [UI] # Name of the app and chatbot. name = "Chatbot" +# Show the readme while the thread is empty. +show_readme_as_default = false + # Description of the app and chatbot. This is used for HTML tags. # description = "" @@ -41,9 +59,6 @@ hide_cot = false # The CSS file can be served from the public directory or via an external link. # custom_css = "/public/test.css" -# If the app is served behind a reverse proxy (like cloud run) we need to know the base url for oauth -# base_url = "https://mydomain.com" - # Override default MUI light theme. (Check theme.ts) [UI.theme.light] #background = "#FAFAFA" @@ -66,4 +81,4 @@ hide_cot = false [meta] -generated_by = "0.7.1" +generated_by = "1.0.0rc2" diff --git a/cypress/e2e/chat_profiles/spec.cy.ts b/cypress/e2e/chat_profiles/spec.cy.ts index 16156a6478..f5df8f7dab 100644 --- a/cypress/e2e/chat_profiles/spec.cy.ts +++ b/cypress/e2e/chat_profiles/spec.cy.ts @@ -1,4 +1,4 @@ -import { runTestServer } from '../../support/testUtils'; +import { runTestServer, submitMessage } from '../../support/testUtils'; describe('Chat profiles', () => { before(() => { @@ -28,7 +28,6 @@ describe('Chat profiles', () => { // Change chat profile cy.get('[data-test="chat-profile:GPT-4"]').click(); - cy.get('#confirm').click(); cy.get('.step') .should('have.length', 1) @@ -48,5 +47,18 @@ describe('Chat profiles', () => { 'contain', 'starting chat with admin using the GPT-4 chat profile' ); + + submitMessage('hello'); + cy.get('.step').should('have.length', 2).eq(1).should('contain', 'hello'); + cy.get('[data-test="chat-profile:GPT-5"]').click(); + cy.get('#confirm').click(); + + cy.get('.step') + .should('have.length', 1) + .eq(0) + .should( + 'contain', + 'starting chat with admin using the GPT-5 chat profile' + ); }); }); diff --git a/frontend/src/components/molecules/chatProfiles.tsx b/frontend/src/components/molecules/chatProfiles.tsx index 47b6a18922..7ee934aac3 100644 --- a/frontend/src/components/molecules/chatProfiles.tsx +++ b/frontend/src/components/molecules/chatProfiles.tsx @@ -4,7 +4,11 @@ import { useRecoilValue } from 'recoil'; import { Box, Popover, Tab, Tabs } from '@mui/material'; -import { useChatInteract, useChatSession } from '@chainlit/react-client'; +import { + useChatInteract, + useChatMessages, + useChatSession +} from '@chainlit/react-client'; import { InputStateHandler, Markdown, @@ -19,6 +23,7 @@ import NewChatDialog from './newChatDialog'; export default function ChatProfiles() { const pSettings = useRecoilValue(projectSettingsState); const { chatProfile, setChatProfile } = useChatSession(); + const { firstInteraction } = useChatMessages(); const [anchorEl, setAnchorEl] = useState(null); const [chatProfileDescription, setChatProfileDescription] = useState(''); const { clear } = useChatInteract(); @@ -31,12 +36,13 @@ export default function ChatProfiles() { setNewChatProfile(null); }; - const handleConfirm = () => { - if (!newChatProfile) { + const handleConfirm = (newChatProfileWithoutConfirm?: string) => { + const chatProfile = newChatProfileWithoutConfirm || newChatProfile; + if (!chatProfile) { // Should never happen throw new Error('Retry clicking on a profile before starting a new chat'); } - setChatProfile(newChatProfile); + setChatProfile(chatProfile); setNewChatProfile(null); clear(); handleClose(); @@ -70,7 +76,11 @@ export default function ChatProfiles() { value={chatProfile || ''} onChange={(event: React.SyntheticEvent, newValue: string) => { setNewChatProfile(newValue); - setOpenDialog(true); + if (firstInteraction) { + setOpenDialog(true); + } else { + handleConfirm(newValue); + } }} variant="scrollable" sx={{ @@ -178,7 +188,7 @@ export default function ChatProfiles() { handleConfirm()} /> ); diff --git a/libs/react-client/src/state.ts b/libs/react-client/src/state.ts index a297f4d3be..f5a3640933 100644 --- a/libs/react-client/src/state.ts +++ b/libs/react-client/src/state.ts @@ -112,8 +112,8 @@ export const tasklistState = atom({ default: [] }); -export const firstUserMessageState = atom({ - key: 'FirstUserMessage', +export const firstUserInteraction = atom({ + key: 'FirstUserInteraction', default: undefined }); diff --git a/libs/react-client/src/useChatInteract.ts b/libs/react-client/src/useChatInteract.ts index 483ffa1697..900994cde7 100644 --- a/libs/react-client/src/useChatInteract.ts +++ b/libs/react-client/src/useChatInteract.ts @@ -8,7 +8,7 @@ import { chatSettingsInputsState, chatSettingsValueState, elementState, - firstUserMessageState, + firstUserInteraction, loadingState, messagesState, sessionIdState, @@ -32,7 +32,7 @@ const useChatInteract = () => { const resetSessionId = useResetRecoilState(sessionIdState); const resetChatSettingsValue = useResetRecoilState(chatSettingsValueState); - const setFirstUserMessage = useSetRecoilState(firstUserMessageState); + const setFirstUserInteraction = useSetRecoilState(firstUserInteraction); const setLoading = useSetRecoilState(loadingState); const setMessages = useSetRecoilState(messagesState); const setElements = useSetRecoilState(elementState); @@ -47,7 +47,7 @@ const useChatInteract = () => { session?.socket.disconnect(); setIdToResume(undefined); resetSessionId(); - setFirstUserMessage(undefined); + setFirstUserInteraction(undefined); setMessages([]); setElements([]); setAvatars([]); diff --git a/libs/react-client/src/useChatMessages.ts b/libs/react-client/src/useChatMessages.ts index e1621c9fda..5c3312309a 100644 --- a/libs/react-client/src/useChatMessages.ts +++ b/libs/react-client/src/useChatMessages.ts @@ -1,14 +1,14 @@ import { useRecoilValue } from 'recoil'; -import { firstUserMessageState, messagesState } from './state'; +import { firstUserInteraction, messagesState } from './state'; const useChatMessages = () => { const messages = useRecoilValue(messagesState); - const firstUserMessage = useRecoilValue(firstUserMessageState); + const firstInteraction = useRecoilValue(firstUserInteraction); return { messages, - firstUserMessage + firstInteraction }; }; diff --git a/libs/react-client/src/useChatSession.ts b/libs/react-client/src/useChatSession.ts index 65f1cf47c9..862c3ca43a 100644 --- a/libs/react-client/src/useChatSession.ts +++ b/libs/react-client/src/useChatSession.ts @@ -15,7 +15,7 @@ import { chatSettingsInputsState, chatSettingsValueState, elementState, - firstUserMessageState, + firstUserInteraction, loadingState, messagesState, sessionIdState, @@ -49,7 +49,7 @@ const useChatSession = () => { const [session, setSession] = useRecoilState(sessionState); const resetChatSettingsValue = useResetRecoilState(chatSettingsValueState); - const setFirstUserMessage = useSetRecoilState(firstUserMessageState); + const setFirstUserInteraction = useSetRecoilState(firstUserInteraction); const setLoading = useSetRecoilState(loadingState); const setMessages = useSetRecoilState(messagesState); const setAskUser = useSetRecoilState(askUserState); @@ -139,8 +139,8 @@ const useChatSession = () => { setMessages((oldMessages) => addMessage(oldMessages, message)); }); - socket.on('init_thread', (message: IStep) => { - setFirstUserMessage(message); + socket.on('first_interaction', (interaction: string) => { + setFirstUserInteraction(interaction); }); socket.on('update_message', (message: IStep) => {