From 89579f522576466be2550843980d1e66ad5a743e Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Mon, 18 May 2026 17:50:02 +0200 Subject: [PATCH] fix(ai-chat) - upload files (#20681) closes https://github.com/twentyhq/twenty/issues/20437 bonus : persist file filename for UI display --- .../src/metadata/generated/schema.graphql | 7 +- .../src/metadata/generated/schema.ts | 4 +- .../src/metadata/generated/types.ts | 81 +++++++++++-------- .../src/generated-metadata/graphql.ts | 11 ++- .../ai/graphql/mutations/sendChatMessage.ts | 4 +- .../src/modules/ai/hooks/useAgentChat.ts | 23 +++--- .../ai/ai-chat/dtos/file-attachment.input.ts | 12 +++ .../ai-chat/resolvers/agent-chat.resolver.ts | 12 ++- .../services/agent-chat-streaming.service.ts | 58 ++++++++----- .../ai/ai-chat/services/agent-chat.service.ts | 35 +++++--- .../types/ai-chat-file-attachment.type.ts | 4 + 11 files changed, 160 insertions(+), 91 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type.ts diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 3c22724f72..465050fca3 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3263,7 +3263,7 @@ type Mutation { updateWebhook(input: UpdateWebhookInput!): Webhook! deleteWebhook(id: UUID!): Webhook! createChatThread: AgentChatThread! - sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult! + sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult! stopAgentChatStream(threadId: UUID!): Boolean! renameChatThread(id: UUID!, title: String!): AgentChatThread! archiveChatThread(id: UUID!): AgentChatThread! @@ -4228,6 +4228,11 @@ input UpdateWebhookInputUpdates { secret: String } +input FileAttachmentInput { + id: UUID! + filename: String! +} + input CreateSkillInput { id: UUID name: String! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index b4ba05a9dd..9780dc6adf 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -5872,7 +5872,7 @@ export interface MutationGenqlSelection{ updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} }) deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} }) createChatThread?: AgentChatThreadGenqlSelection - sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} }) + sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} }) stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} } renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} }) archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} }) @@ -6259,6 +6259,8 @@ update: UpdateWebhookInputUpdates} export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)} +export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']} + export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']} export interface UpdateSkillInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null)} diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 40277af795..d0a1ce3738 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -78,8 +78,8 @@ export default { 335, 342, 378, - 454, - 466 + 455, + 467 ], "types": { "BillingProductDTO": { @@ -7876,9 +7876,9 @@ export default { "modelId": [ 1 ], - "fileIds": [ - 3, - "[UUID!]" + "fileAttachments": [ + 440, + "[FileAttachmentInput!]" ] } ], @@ -7944,7 +7944,7 @@ export default { 290, { "input": [ - 440, + 441, "CreateSkillInput!" ] } @@ -7953,7 +7953,7 @@ export default { 290, { "input": [ - 441, + 442, "UpdateSkillInput!" ] } @@ -8011,7 +8011,7 @@ export default { 238, { "input": [ - 442, + 443, "GetAuthorizationUrlForSSOInput!" ] } @@ -8265,7 +8265,7 @@ export default { 196, { "input": [ - 443, + 444, "CreateApplicationRegistrationInput!" ] } @@ -8274,7 +8274,7 @@ export default { 7, { "input": [ - 444, + 445, "UpdateApplicationRegistrationInput!" ] } @@ -8301,7 +8301,7 @@ export default { 5, { "input": [ - 446, + 447, "CreateApplicationRegistrationVariableInput!" ] } @@ -8310,7 +8310,7 @@ export default { 5, { "input": [ - 447, + 448, "UpdateApplicationRegistrationVariableInput!" ] } @@ -8399,7 +8399,7 @@ export default { 6, { "input": [ - 449, + 450, "UpdateWorkspaceMemberSettingsInput!" ] } @@ -8433,7 +8433,7 @@ export default { 75, { "data": [ - 450, + 451, "ActivateWorkspaceInput!" ] } @@ -8442,7 +8442,7 @@ export default { 75, { "data": [ - 451, + 452, "UpdateWorkspaceInput!" ] } @@ -8469,7 +8469,7 @@ export default { 6, { "workspaceMigration": [ - 452, + 453, "WorkspaceMigrationInput!" ] } @@ -8487,7 +8487,7 @@ export default { 220, { "input": [ - 455, + 456, "SetupOIDCSsoInput!" ] } @@ -8496,7 +8496,7 @@ export default { 220, { "input": [ - 456, + 457, "SetupSAMLSsoInput!" ] } @@ -8505,7 +8505,7 @@ export default { 216, { "input": [ - 457, + 458, "DeleteSsoInput!" ] } @@ -8514,7 +8514,7 @@ export default { 217, { "input": [ - 458, + 459, "EditSsoInput!" ] } @@ -8545,7 +8545,7 @@ export default { 286, { "input": [ - 459, + 460, "SendEmailInput!" ] } @@ -8571,7 +8571,7 @@ export default { "String!" ], "connectionParameters": [ - 461, + 462, "EmailAccountConnectionParameters!" ], "id": [ @@ -8583,7 +8583,7 @@ export default { 168, { "input": [ - 463, + 464, "UpdateLabPublicFeatureFlagInput!" ] } @@ -8665,7 +8665,7 @@ export default { 77, { "input": [ - 464, + 465, "CreateOneAppTokenInput!" ] } @@ -8728,7 +8728,7 @@ export default { "String!" ], "fileFolder": [ - 466, + 467, "FileFolder!" ], "filePath": [ @@ -10746,6 +10746,17 @@ export default { 1 ] }, + "FileAttachmentInput": { + "id": [ + 3 + ], + "filename": [ + 1 + ], + "__typename": [ + 1 + ] + }, "CreateSkillInput": { "id": [ 3 @@ -10828,7 +10839,7 @@ export default { 1 ], "update": [ - 445 + 446 ], "__typename": [ 1 @@ -10876,7 +10887,7 @@ export default { 1 ], "update": [ - 448 + 449 ], "__typename": [ 1 @@ -10994,7 +11005,7 @@ export default { }, "WorkspaceMigrationInput": { "actions": [ - 453 + 454 ], "__typename": [ 1 @@ -11002,7 +11013,7 @@ export default { }, "WorkspaceMigrationDeleteActionInput": { "type": [ - 454 + 455 ], "metadataName": [ 318 @@ -11097,7 +11108,7 @@ export default { 1 ], "files": [ - 460 + 461 ], "__typename": [ 1 @@ -11116,13 +11127,13 @@ export default { }, "EmailAccountConnectionParameters": { "IMAP": [ - 462 + 463 ], "SMTP": [ - 462 + 463 ], "CALDAV": [ - 462 + 463 ], "__typename": [ 1 @@ -11161,7 +11172,7 @@ export default { }, "CreateOneAppTokenInput": { "appToken": [ - 465 + 466 ], "__typename": [ 1 @@ -11190,7 +11201,7 @@ export default { 230, { "input": [ - 468, + 469, "LogicFunctionLogsInput!" ] } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 4de0c4c797..a659d692d7 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1792,6 +1792,11 @@ export type File = { size: Scalars['Float']; }; +export type FileAttachmentInput = { + filename: Scalars['String']; + id: Scalars['UUID']; +}; + export enum FileFolder { AgentChat = 'AgentChat', AppTarball = 'AppTarball', @@ -3197,7 +3202,7 @@ export type MutationSaveImapSmtpCaldavAccountArgs = { export type MutationSendChatMessageArgs = { browsingContext?: InputMaybe; - fileIds?: InputMaybe>; + fileAttachments?: InputMaybe>; messageId: Scalars['UUID']; modelId?: InputMaybe; text: Scalars['String']; @@ -6148,7 +6153,7 @@ export type SendChatMessageMutationVariables = Exact<{ messageId: Scalars['UUID']; browsingContext?: InputMaybe; modelId?: InputMaybe; - fileIds?: InputMaybe | Scalars['UUID']>; + fileAttachments?: InputMaybe | FileAttachmentInput>; }>; @@ -7983,7 +7988,7 @@ export const EvaluateAgentTurnDocument = {"kind":"Document","definitions":[{"kin export const RemoveRoleFromAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveRoleFromAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeRoleFromAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}}]}]}}]} as unknown as DocumentNode; export const RenameChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenameChatThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"title"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renameChatThread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"title"},"value":{"kind":"Variable","name":{"kind":"Name","value":"title"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const RunEvaluationInputDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RunEvaluationInput"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runEvaluationInput"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"agentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"agentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"threadId"}},{"kind":"Field","name":{"kind":"Name","value":"agentId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"evaluations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; -export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"fileIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fileIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode; +export const SendChatMessageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SendChatMessage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fileAttachments"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FileAttachmentInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendChatMessage"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}},{"kind":"Argument","name":{"kind":"Name","value":"text"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}},{"kind":"Argument","name":{"kind":"Name","value":"messageId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageId"}}},{"kind":"Argument","name":{"kind":"Name","value":"browsingContext"},"value":{"kind":"Variable","name":{"kind":"Name","value":"browsingContext"}}},{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"fileAttachments"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fileAttachments"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageId"}},{"kind":"Field","name":{"kind":"Name","value":"queued"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}}]}}]}}]} as unknown as DocumentNode; export const StopAgentChatStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StopAgentChatStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stopAgentChatStream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"threadId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"threadId"}}}]}]}}]} as unknown as DocumentNode; export const UnarchiveChatThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnarchiveChatThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unarchiveChatThread"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const UpdateOneAgentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneAgent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateAgentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneAgent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AgentFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AgentFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Agent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"prompt"}},{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"responseFormat"}},{"kind":"Field","name":{"kind":"Name","value":"roleId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"modelConfiguration"}},{"kind":"Field","name":{"kind":"Name","value":"evaluationInputs"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/sendChatMessage.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/sendChatMessage.ts index cd6ca8d835..f4c21ddfc6 100644 --- a/packages/twenty-front/src/modules/ai/graphql/mutations/sendChatMessage.ts +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/sendChatMessage.ts @@ -7,7 +7,7 @@ export const SEND_CHAT_MESSAGE = gql` $messageId: UUID! $browsingContext: JSON $modelId: String - $fileIds: [UUID!] + $fileAttachments: [FileAttachmentInput!] ) { sendChatMessage( threadId: $threadId @@ -15,7 +15,7 @@ export const SEND_CHAT_MESSAGE = gql` messageId: $messageId browsingContext: $browsingContext modelId: $modelId - fileIds: $fileIds + fileAttachments: $fileAttachments ) { messageId queued diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts index 177efe2ca6..32cc1bccc7 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts @@ -30,7 +30,6 @@ import { useListenToBrowserEvent } from '@/browser-event/hooks/useListenToBrowse import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; export const useAgentChat = ( @@ -44,11 +43,9 @@ export const useAgentChat = ( const setCurrentAiChatThread = useSetAtomState(currentAiChatThreadState); const store = useStore(); - const agentChatSelectedFiles = useAtomStateValue(agentChatSelectedFilesState); - const [, setPendingThreadIdAfterFirstSend] = useState(null); - const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useAtomState( + const setAgentChatUploadedFiles = useSetAtomState( agentChatUploadedFilesState, ); @@ -74,12 +71,14 @@ export const useAgentChat = ( return; } - const isLoading = agentChatSelectedFiles.length > 0; + const agentChatSelectedFiles = store.get(agentChatSelectedFilesState.atom); - if (isLoading) { + if (agentChatSelectedFiles.length > 0) { return; } + const agentChatUploadedFiles = store.get(agentChatUploadedFilesState.atom); + const threadId = await ensureThreadIdForSend(); if (!isDefined(threadId)) { @@ -131,7 +130,11 @@ export const useAgentChat = ( store.set(messagesAtom, [...currentMessages, optimisticUserMessage]); store.set(errorAtom, null); - const fileIds = agentChatUploadedFiles.map((file) => file.fileId); + const fileAttachments = agentChatUploadedFiles.map((file) => ({ + id: file.fileId, + filename: file.filename, + })); + const uploadedFilesSnapshot = agentChatUploadedFiles; setAgentChatUploadedFiles([]); @@ -150,7 +153,8 @@ export const useAgentChat = ( messageId, browsingContext: browsingContext ?? null, modelId: modelIdForRequest ?? undefined, - fileIds: fileIds.length > 0 ? fileIds : undefined, + fileAttachments: + fileAttachments.length > 0 ? fileAttachments : undefined, }, }); @@ -186,6 +190,7 @@ export const useAgentChat = ( ? { [AGENT_CHAT_NEW_THREAD_DRAFT_KEY]: '' } : {}), })); + setAgentChatUploadedFiles(uploadedFilesSnapshot); const latestMessages = store.get(messagesAtom); @@ -214,11 +219,9 @@ export const useAgentChat = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [ store, - agentChatSelectedFiles, ensureThreadIdForSend, setAgentChatInput, getBrowsingContext, - agentChatUploadedFiles, setAgentChatUploadedFiles, setAgentChatDraftsByThreadId, modelIdForRequest, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input.ts new file mode 100644 index 0000000000..16b22246db --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input.ts @@ -0,0 +1,12 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@InputType() +export class FileAttachmentInput { + @Field(() => UUIDScalarType) + id: string; + + @Field() + filename: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/resolvers/agent-chat.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/resolvers/agent-chat.resolver.ts index 7601d6525d..0abe09cba5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/resolvers/agent-chat.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/resolvers/agent-chat.resolver.ts @@ -27,6 +27,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { AgentMessageDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message.dto'; import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type'; import { AgentChatThreadDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/agent-chat-thread.dto'; +import { FileAttachmentInput } from 'src/engine/metadata-modules/ai/ai-chat/dtos/file-attachment.input'; import { AiSystemPromptPreviewDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/ai-system-prompt-preview.dto'; import { ChatStreamCatchupChunksDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/chat-stream-catchup-chunks.dto'; import { SendChatMessageResultDTO } from 'src/engine/metadata-modules/ai/ai-chat/dtos/send-chat-message-result.dto'; @@ -113,8 +114,11 @@ export class AgentChatResolver { browsingContext: BrowsingContextType | null, @Args('modelId', { type: () => String, nullable: true }) modelId: string | undefined, - @Args('fileIds', { type: () => [UUIDScalarType], nullable: true }) - fileIds: string[] | null, + @Args('fileAttachments', { + type: () => [FileAttachmentInput], + nullable: true, + }) + fileAttachments: FileAttachmentInput[] | null, @AuthUserWorkspaceId() userWorkspaceId: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { @@ -157,7 +161,7 @@ export class AgentChatResolver { threadId, text, id: messageId, - fileIds: fileIds ?? undefined, + fileAttachments: fileAttachments ?? undefined, workspaceId: workspace.id, userWorkspaceId, }); @@ -179,7 +183,7 @@ export class AgentChatResolver { workspace, text, messageId, - fileIds: fileIds ?? undefined, + fileAttachments: fileAttachments ?? undefined, }); return { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts index ddf720d3c0..22b5a2b198 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts @@ -21,18 +21,19 @@ import { AgentMessageStatus, } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity'; import { mapDBPartsToUIMessageParts } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/mapDBPartsToUIMessageParts'; -import { - AiException, - AiExceptionCode, -} from 'src/engine/metadata-modules/ai/ai.exception'; import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type'; import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity'; import { STREAM_AGENT_CHAT_JOB_NAME } from 'src/engine/metadata-modules/ai/ai-chat/jobs/stream-agent-chat-job-name.constant'; import { type StreamAgentChatJobData } from 'src/engine/metadata-modules/ai/ai-chat/jobs/stream-agent-chat-job.types'; import { AgentChatEventPublisherService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-event-publisher.service'; import { AgentChatService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service'; +import { AiChatFileAttachment } from 'src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type'; +import { + AiException, + AiExceptionCode, +} from 'src/engine/metadata-modules/ai/ai.exception'; -export type StreamAgentChatOptions = { +type StreamAgentChatOptions = { threadId: string; userWorkspaceId: string; workspace: WorkspaceEntity; @@ -40,7 +41,7 @@ export type StreamAgentChatOptions = { browsingContext: BrowsingContextType | null; modelId?: string; messageId?: string; - fileIds?: string[]; + fileAttachments?: AiChatFileAttachment[]; }; @Injectable() @@ -67,7 +68,7 @@ export class AgentChatStreamingService { browsingContext, modelId, messageId, - fileIds, + fileAttachments, }: StreamAgentChatOptions): Promise<{ streamId: string; messageId: string }> { const thread = await this.threadRepository.findOne({ where: { @@ -83,7 +84,10 @@ export class AgentChatStreamingService { ); } - const fileParts = await this.buildFilePartsFromIds(fileIds, workspace.id); + const fileParts = await this.buildFilePartsFromAttachments( + fileAttachments, + workspace.id, + ); const userMessageParts: ExtendedUIMessagePart[] = [ { type: 'text' as const, text }, @@ -281,30 +285,40 @@ export class AgentChatStreamingService { ); } - private async buildFilePartsFromIds( - fileIds: string[] | undefined, + private async buildFilePartsFromAttachments( + fileAttachments: AiChatFileAttachment[] | undefined, workspaceId: string, ): Promise { - if (!fileIds || fileIds.length === 0) { + if (!fileAttachments || fileAttachments.length === 0) { return []; } - const files = await this.fileRepository.find({ + const fileIds = fileAttachments.map((attachment) => attachment.id); + + const validFiles = await this.fileRepository.find({ where: { id: In(fileIds), workspaceId, - path: Like(`%/${FileFolder.AgentChat}/%`), + path: Like(`${FileFolder.AgentChat}/%`), }, }); - return files.map( - (file): ExtendedFileUIPart => ({ - type: 'file' as const, - mediaType: file.mimeType, - filename: file.path.split('/').pop() ?? file.path, - url: '', - fileId: file.id, - }), - ); + const validFileIds = new Set(validFiles.map((file) => file.id)); + + return fileAttachments + .filter((attachment) => validFileIds.has(attachment.id)) + .map((attachment): ExtendedFileUIPart => { + const file = validFiles.find( + (validFile) => validFile.id === attachment.id, + ); + + return { + type: 'file' as const, + mediaType: file?.mimeType ?? 'application/octet-stream', + filename: attachment.filename, + url: '', + fileId: attachment.id, + }; + }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts index f0adbeac82..e40fbc25eb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts @@ -23,6 +23,7 @@ import { } from 'src/engine/metadata-modules/ai/ai.exception'; import { WorkspaceEventBroadcaster } from 'src/engine/subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service'; +import { AiChatFileAttachment } from 'src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type'; import { AgentTitleGenerationService } from './agent-title-generation.service'; const serializeThreadForBroadcast = ( @@ -252,14 +253,14 @@ export class AgentChatService { threadId, text, id, - fileIds, + fileAttachments, workspaceId, userWorkspaceId, }: { threadId: string; text: string; id?: string; - fileIds?: string[]; + fileAttachments?: AiChatFileAttachment[]; workspaceId: string; userWorkspaceId: string; }): Promise { @@ -277,13 +278,19 @@ export class AgentChatService { const savedMessageId = (id ?? insertResult.identifiers[0].id) as string; - const files = - fileIds && fileIds.length > 0 + const validFiles = + fileAttachments && fileAttachments.length > 0 ? await this.fileRepository.find({ - where: { id: In(fileIds), workspaceId }, + where: { + id: In(fileAttachments.map((attachment) => attachment.id)), + workspaceId, + }, + select: ['id'], }) : []; + const validFileIds = new Set(validFiles.map((file) => file.id)); + const parts = [ { messageId: savedMessageId, @@ -292,14 +299,16 @@ export class AgentChatService { textContent: text, workspaceId, }, - ...files.map((file, index) => ({ - messageId: savedMessageId, - orderIndex: index + 1, - type: 'file', - fileId: file.id, - fileFilename: file.path.split('/').pop() ?? null, - workspaceId, - })), + ...(fileAttachments ?? []) + .filter((attachment) => validFileIds.has(attachment.id)) + .map((attachment, index) => ({ + messageId: savedMessageId, + orderIndex: index + 1, + type: 'file', + fileId: attachment.id, + fileFilename: attachment.filename, + workspaceId, + })), ]; await this.messagePartRepository.insert(parts); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type.ts new file mode 100644 index 0000000000..3b0e121cd0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/ai-chat-file-attachment.type.ts @@ -0,0 +1,4 @@ +export type AiChatFileAttachment = { + id: string; + filename: string; +};