Feat: Agent chat multi thread support (#13216)
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: MD Readul Islam <99027968+readul-islam@users.noreply.github.com> Co-authored-by: readul-islam <developer.readul@gamil.com> Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com> Co-authored-by: Guillim <guillim@users.noreply.github.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Jean-Baptiste Ronssin <65334819+jbronssin@users.noreply.github.com> Co-authored-by: kahkashan shaik <93042682+kahkashanshaik@users.noreply.github.com> Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu>
This commit is contained in:
+1
@@ -9,6 +9,7 @@ export const GET_AGENT_CHAT_THREADS = gql`
|
||||
) {
|
||||
id
|
||||
agentId
|
||||
title
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
+44
-3
@@ -1,18 +1,28 @@
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { keyframes, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
|
||||
import {
|
||||
Avatar,
|
||||
IconDotsVertical,
|
||||
IconHistory,
|
||||
IconMessageCirclePlus,
|
||||
IconSparkles,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { AgentChatFilePreview } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview';
|
||||
import { AgentChatFileUpload } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload';
|
||||
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { AgentChatMessage } from '~/generated/graphql';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
import { useAgentChat } from '../hooks/useAgentChat';
|
||||
import { AgentChatMessage } from '../hooks/useAgentChatMessages';
|
||||
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
|
||||
import { AgentChatSelectedFilesPreview } from './AgentChatSelectedFilesPreview';
|
||||
|
||||
@@ -188,7 +198,13 @@ const StyledFilesContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const AIChatTab = ({ agentId }: { agentId: string }) => {
|
||||
export const AIChatTab = ({
|
||||
agentId,
|
||||
isWorkflowAgentNodeChat,
|
||||
}: {
|
||||
agentId: string;
|
||||
isWorkflowAgentNodeChat?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
@@ -201,6 +217,9 @@ export const AIChatTab = ({ agentId }: { agentId: string }) => {
|
||||
scrollWrapperId,
|
||||
} = useAgentChat(agentId);
|
||||
|
||||
const { createAgentChatThread } = useCreateNewAIChatThread({ agentId });
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
|
||||
const getAssistantMessageContent = (message: AgentChatMessage) => {
|
||||
if (message.content !== '') {
|
||||
return message.content;
|
||||
@@ -308,6 +327,28 @@ export const AIChatTab = ({ agentId }: { agentId: string }) => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<StyledButtonsContainer>
|
||||
{!isWorkflowAgentNodeChat && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconHistory}
|
||||
onClick={() =>
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewPreviousAIChats,
|
||||
pageTitle: t`View Previous AI Chats`,
|
||||
pageIcon: IconHistory,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconMessageCirclePlus}
|
||||
onClick={() => createAgentChatThread()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<AgentChatFileUpload agentId={agentId} />
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export const WorkflowEditActionAiAgent = ({
|
||||
componentInstanceId={WORKFLOW_AI_AGENT_TAB_LIST_COMPONENT_ID}
|
||||
/>
|
||||
{activeTabId === WorkflowAiAgentTabId.CHAT ? (
|
||||
<AIChatTab agentId={agentId} />
|
||||
<AIChatTab agentId={agentId} isWorkflowAgentNodeChat />
|
||||
) : (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
|
||||
+34
-14
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@@ -12,17 +13,21 @@ import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions
|
||||
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
|
||||
import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
useGetAgentChatMessagesQuery,
|
||||
useGetAgentChatThreadsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { AgentChatMessage } from '~/generated/graphql';
|
||||
import { agentChatInputState } from '../states/agentChatInputState';
|
||||
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
|
||||
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
|
||||
import { parseAgentStreamingChunk } from '../utils/parseAgentStreamingChunk';
|
||||
import { AgentChatMessage, useAgentChatMessages } from './useAgentChatMessages';
|
||||
import { useAgentChatThreads } from './useAgentChatThreads';
|
||||
|
||||
interface OptimisticMessage extends AgentChatMessage {
|
||||
type OptimisticMessage = AgentChatMessage & {
|
||||
isPending: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const useAgentChat = (agentId: string) => {
|
||||
const apolloClient = useApolloClient();
|
||||
@@ -33,6 +38,10 @@ export const useAgentChat = (agentId: string) => {
|
||||
agentId,
|
||||
);
|
||||
|
||||
const [currentThreadId, setCurrentThreadId] = useRecoilComponentStateV2(
|
||||
currentAIChatThreadComponentState,
|
||||
agentId,
|
||||
);
|
||||
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
|
||||
useRecoilComponentStateV2(agentChatUploadedFilesComponentState, agentId);
|
||||
|
||||
@@ -61,26 +70,37 @@ export const useAgentChat = (agentId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const { data: { threads = [] } = {}, loading: threadsLoading } =
|
||||
useAgentChatThreads(agentId);
|
||||
const currentThreadId = threads[0]?.id;
|
||||
const { loading: threadsLoading } = useGetAgentChatThreadsQuery({
|
||||
variables: { agentId },
|
||||
skip: isDefined(currentThreadId),
|
||||
onCompleted: (data) => {
|
||||
if (data.agentChatThreads.length > 0) {
|
||||
setCurrentThreadId(data.agentChatThreads[0].id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { loading: messagesLoading, refetch: refetchMessages } =
|
||||
useAgentChatMessages(currentThreadId, ({ messages }) => {
|
||||
setAgentChatMessages(messages);
|
||||
scrollToBottom();
|
||||
useGetAgentChatMessagesQuery({
|
||||
variables: { threadId: currentThreadId as string },
|
||||
skip: !isDefined(currentThreadId),
|
||||
onCompleted: ({ agentChatMessages }) => {
|
||||
setAgentChatMessages(agentChatMessages);
|
||||
scrollToBottom();
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
messagesLoading ||
|
||||
threadsLoading ||
|
||||
!currentThreadId ||
|
||||
isStreaming ||
|
||||
agentChatSelectedFiles.length > 0;
|
||||
|
||||
const createOptimisticMessages = (content: string): AgentChatMessage[] => {
|
||||
const optimisticUserMessage: OptimisticMessage = {
|
||||
id: v4(),
|
||||
threadId: currentThreadId,
|
||||
threadId: currentThreadId as string,
|
||||
role: AgentChatMessageRole.USER,
|
||||
content,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -90,7 +110,7 @@ export const useAgentChat = (agentId: string) => {
|
||||
|
||||
const optimisticAiMessage: OptimisticMessage = {
|
||||
id: v4(),
|
||||
threadId: currentThreadId,
|
||||
threadId: currentThreadId as string,
|
||||
role: AgentChatMessageRole.ASSISTANT,
|
||||
content: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -163,7 +183,7 @@ export const useAgentChat = (agentId: string) => {
|
||||
|
||||
const { data } = await refetchMessages();
|
||||
|
||||
setAgentChatMessages(data?.messages);
|
||||
setAgentChatMessages(data?.agentChatMessages);
|
||||
setAgentStreamingMessage({
|
||||
toolCall: '',
|
||||
streamingText: '',
|
||||
@@ -172,7 +192,7 @@ export const useAgentChat = (agentId: string) => {
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!agentChatInput.trim() || isLoading) {
|
||||
if (agentChatInput.trim() === '' || isLoading === true) {
|
||||
return;
|
||||
}
|
||||
const content = agentChatInput.trim();
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { File } from '~/generated-metadata/graphql';
|
||||
import { GET_AGENT_CHAT_MESSAGES } from '../api/agent-chat-apollo.api';
|
||||
|
||||
export type AgentChatMessage = {
|
||||
id: string;
|
||||
threadId: string;
|
||||
role: AgentChatMessageRole;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
files: File[];
|
||||
};
|
||||
|
||||
export const useAgentChatMessages = (
|
||||
threadId: string,
|
||||
onCompleted?: (data: { messages: AgentChatMessage[] }) => void,
|
||||
) => {
|
||||
return useQuery<{ messages: AgentChatMessage[] }>(GET_AGENT_CHAT_MESSAGES, {
|
||||
variables: { threadId },
|
||||
skip: !isDefined(threadId),
|
||||
onCompleted,
|
||||
});
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { GET_AGENT_CHAT_THREADS } from '../api/agent-chat-apollo.api';
|
||||
|
||||
export interface AgentChatThread {
|
||||
id: string;
|
||||
agentId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const useAgentChatThreads = (agentId: string) => {
|
||||
return useQuery<{ threads: AgentChatThread[] }>(GET_AGENT_CHAT_THREADS, {
|
||||
variables: { agentId },
|
||||
skip: !isDefined(agentId),
|
||||
});
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { AgentChatMessage } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useAgentChatMessages';
|
||||
import { AgentChatMessage } from '~/generated-metadata/graphql';
|
||||
|
||||
export const AgentChatMessagesComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
|
||||
Reference in New Issue
Block a user