diff --git a/packages/backend/server/src/__tests__/copilot.e2e.ts b/packages/backend/server/src/__tests__/copilot.e2e.ts index 50d5741b93..752bd0ac85 100644 --- a/packages/backend/server/src/__tests__/copilot.e2e.ts +++ b/packages/backend/server/src/__tests__/copilot.e2e.ts @@ -461,6 +461,29 @@ test('should create message correctly', async t => { sessionId, undefined, undefined, + new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' }) + ); + t.truthy(messageId, 'should be able to create message with blob'); + } + + // with attachments + { + const { id } = await createWorkspace(app); + const sessionId = await createCopilotSession( + app, + id, + randomUUID(), + textPromptName + ); + const smallestPng = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'; + const pngData = await fetch(smallestPng).then(res => res.arrayBuffer()); + const messageId = await createCopilotMessage( + app, + sessionId, + undefined, + undefined, + undefined, [new File([new Uint8Array(pngData)], '1.png', { type: 'image/png' })] ); t.truthy(messageId, 'should be able to create message with blobs'); diff --git a/packages/backend/server/src/__tests__/utils/copilot.ts b/packages/backend/server/src/__tests__/utils/copilot.ts index 8b5171c415..f687253d1a 100644 --- a/packages/backend/server/src/__tests__/utils/copilot.ts +++ b/packages/backend/server/src/__tests__/utils/copilot.ts @@ -554,52 +554,73 @@ export async function createCopilotMessage( sessionId: string, content?: string, attachments?: string[], + blob?: File, blobs?: File[], params?: Record ): Promise { - let resp = app - .POST('/graphql') - .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) - .field( - 'operations', - JSON.stringify({ - query: ` + const gql = { + query: ` mutation createCopilotMessage($options: CreateChatMessageInput!) { createCopilotMessage(options: $options) } `, - variables: { - options: { sessionId, content, attachments, blobs: [], params }, - }, - }) - ) - .field( - 'map', - JSON.stringify( - Array.from({ length: blobs?.length ?? 0 }).reduce( - (acc, _, idx) => { - acc[idx.toString()] = [`variables.options.blobs.${idx}`]; - return acc; - }, - {} - ) - ) - ); - if (blobs && blobs.length) { - for (const [idx, file] of blobs.entries()) { - resp = resp.attach( - idx.toString(), - Buffer.from(await file.arrayBuffer()), - { - filename: file.name || `file${idx}`, - contentType: file.type || 'application/octet-stream', - } + variables: { + options: { + sessionId, + content, + attachments, + blob: null, + blobs: [], + params, + }, + }, + }; + + let resp = app + .POST('/graphql') + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }); + if (blob || blobs) { + resp = resp.field('operations', JSON.stringify(gql)); + + if (blob) { + resp = resp.field( + 'map', + JSON.stringify({ '0': ['variables.options.blob'] }) ); + resp = resp.attach('0', Buffer.from(await blob.arrayBuffer()), { + filename: blob.name || 'file', + contentType: blob.type || 'application/octet-stream', + }); + } else if (blobs && blobs.length) { + resp = resp.field( + 'map', + JSON.stringify( + Array.from({ length: blobs?.length ?? 0 }).reduce( + (acc, _, idx) => { + acc[idx.toString()] = [`variables.options.blobs.${idx}`]; + return acc; + }, + {} + ) + ) + ); + for (const [idx, file] of blobs.entries()) { + resp = resp.attach( + idx.toString(), + Buffer.from(await file.arrayBuffer()), + { + filename: file.name || `file${idx}`, + contentType: file.type || 'application/octet-stream', + } + ); + } } + } else { + resp = resp.send(gql); } const res = await resp.expect(200); - + console.log('createCopilotMessage', res.body); return res.body.data.createCopilotMessage; } diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index a05cc7c30f..d97e4f1a19 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -143,6 +143,9 @@ class CreateChatMessageInput implements Omit { @Field(() => [String], { nullable: true, deprecationReason: 'use blobs' }) attachments!: string[] | undefined; + @Field(() => GraphQLUpload, { nullable: true }) + blob!: Promise | undefined; + @Field(() => [GraphQLUpload], { nullable: true }) blobs!: Promise[] | undefined; @@ -703,10 +706,13 @@ export class CopilotResolver { } const attachments: PromptMessage['attachments'] = options.attachments || []; - if (options.blobs) { + if (options.blob || options.blobs) { const { workspaceId } = session.config; - const blobs = await Promise.all(options.blobs); + const blobs = await Promise.all( + options.blob ? [options.blob] : options.blobs || [] + ); + delete options.blob; delete options.blobs; for (const blob of blobs) { diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index edf792e578..c1a0b1de26 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -457,6 +457,7 @@ type CopilotWorkspaceIgnoredDocTypeEdge { input CreateChatMessageInput { attachments: [String!] + blob: Upload blobs: [Upload!] content: String params: JSON diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 0e7d456427..283992f332 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -569,6 +569,7 @@ export interface CopilotWorkspaceIgnoredDocTypeEdge { export interface CreateChatMessageInput { attachments?: InputMaybe>; + blob?: InputMaybe; blobs?: InputMaybe>; content?: InputMaybe; params?: InputMaybe;