diff --git a/packages/backend/server/src/data/migrations/utils/prompts.ts b/packages/backend/server/src/data/migrations/utils/prompts.ts index 0f3639af83..75f0365883 100644 --- a/packages/backend/server/src/data/migrations/utils/prompts.ts +++ b/packages/backend/server/src/data/migrations/utils/prompts.ts @@ -73,6 +73,18 @@ export const prompts: Prompt[] = [ }, ], }, + { + name: 'Explain this', + action: 'Explain this', + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'assistant', + content: + 'Explain the following content in a clear and concise manner, ensuring that the information is easy to understand and provides a comprehensive overview of the topic:\n\n{{content}}', + }, + ], + }, { name: 'Explain this image', action: 'Explain this image', @@ -167,6 +179,18 @@ export const prompts: Prompt[] = [ }, ], }, + { + name: 'Write outline', + action: 'Write outline', + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'assistant', + content: + 'Write an outline based on the following content, organizing the main points, subtopics, and structure:\n\n{{content}}', + }, + ], + }, { name: 'Change tone to', action: 'Change tone', @@ -192,6 +216,18 @@ export const prompts: Prompt[] = [ }, ], }, + { + name: 'Brainstorm mindmap', + action: 'Brainstorm mindmap', + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'assistant', + content: + 'Use the nested unordered list syntax without other extra text style in Markdown to create a structure similar to a mind map without any unnecessary plain text description. Analyze the following questions or topics: \n\n{{content}}', + }, + ], + }, { name: 'Improve writing for it', action: 'Improve writing for it', @@ -329,4 +365,26 @@ export const prompts: Prompt[] = [ }, ], }, + { + name: 'Make it longer', + action: 'Make it longer', + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'assistant', + content: 'Make the following content longer:\n\n{{content}}', + }, + ], + }, + { + name: 'Make it shorter', + action: 'Make it shorter', + model: 'gpt-4-turbo-preview', + messages: [ + { + role: 'assistant', + content: 'Make the following content shorter:\n\n{{content}}', + }, + ], + }, ]; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/copilot-client.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/copilot-client.ts new file mode 100644 index 0000000000..6ecdf8903e --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/copilot-client.ts @@ -0,0 +1,140 @@ +import { + createCopilotMessageMutation, + createCopilotSessionMutation, + fetcher, + getBaseUrl, + getCopilotHistoriesQuery, + getCopilotSessionsQuery, + type GraphQLQuery, + type RequestOptions, +} from '@affine/graphql'; + +type OptionsField = + RequestOptions['variables'] extends { options: infer U } ? U : never; + +export class CopilotClient { + readonly backendUrl = getBaseUrl(); + + async createSession( + options: OptionsField + ) { + const res = await fetcher({ + query: createCopilotSessionMutation, + variables: { + options, + }, + }); + return res.createCopilotSession; + } + + async createMessage( + options: OptionsField + ) { + const res = await fetcher({ + query: createCopilotMessageMutation, + variables: { + options, + }, + }); + return res.createCopilotMessage; + } + + async getSessions(workspaceId: string) { + const res = await fetcher({ + query: getCopilotSessionsQuery, + variables: { + workspaceId, + }, + }); + return res.currentUser?.copilot; + } + + async getHistories( + workspaceId: string, + docId?: string, + options?: OptionsField + ) { + const res = await fetcher({ + query: getCopilotHistoriesQuery, + variables: { + workspaceId, + docId, + options, + }, + }); + + return res.currentUser?.copilot?.histories; + } + + async textToText(message: string, sessionId: string) { + const res = await fetch( + `${this.backendUrl}/api/copilot/chat/${sessionId}?message=${encodeURIComponent(message)}` + ); + if (!res.ok) return; + return res.text(); + } + + textToTextStream(message: string, sessionId: string) { + return new EventSource( + `${this.backendUrl}/api/copilot/chat/${sessionId}/stream?message=${encodeURIComponent(message)}` + ); + } + + chatText({ + sessionId, + messageId, + message, + }: { + sessionId: string; + messageId?: string; + message?: string; + }) { + if (messageId && message) { + throw new Error('Only one of messageId or message can be provided'); + } else if (!messageId && !message) { + throw new Error('Either messageId or message must be provided'); + } + const url = new URL(`${this.backendUrl}/api/copilot/chat/${sessionId}`); + if (messageId) { + url.searchParams.set('messageId', messageId); + } + if (message) { + url.searchParams.set('message', message); + } + return fetch(url.toString()); + } + + // Text or image to text + chatTextStream({ + sessionId, + messageId, + message, + }: { + sessionId: string; + messageId?: string; + message?: string; + }) { + if (messageId && message) { + throw new Error('Only one of messageId or message can be provided'); + } else if (!messageId && !message) { + throw new Error('Either messageId or message must be provided'); + } + const url = new URL( + `${this.backendUrl}/api/copilot/chat/${sessionId}/stream` + ); + if (messageId) { + url.searchParams.set('messageId', messageId); + } + if (message) { + url.searchParams.set('message', message); + } + return new EventSource(url.toString()); + } + + // Text or image to images + imagesStream(messageId: string, sessionId: string) { + return new EventSource( + `${this.backendUrl}/api/copilot/chat/${sessionId}/images?messageId=${messageId}` + ); + } +} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/prompt.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/prompt.ts new file mode 100644 index 0000000000..cfe2da6e80 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/prompt.ts @@ -0,0 +1,35 @@ +// manually synced with packages/backend/server/src/data/migrations/utils/prompts.ts +// todo: automate this +export const promptKeys = [ + 'debug:chat:gpt4', + 'debug:action:gpt4', + 'debug:action:vision4', + 'debug:action:dalle3', + 'debug:action:fal-sd15', + 'Summary', + 'Summary the webpage', + 'Explain this', + 'Explain this image', + 'Explain this code', + 'Translate to', + 'Write an article about this', + 'Write a twitter about this', + 'Write a poem about this', + 'Write a blog post about this', + 'Write outline', + 'Change tone to', + 'Brainstorm ideas about this', + 'Brainstorm mindmap', + 'Improve writing for it', + 'Improve grammar for it', + 'Fix spelling for it', + 'Find action items from it', + 'Check code error', + 'Create a presentation', + 'Create headings', + 'Make it real', + 'Make it longer', + 'Make it shorter', +] as const; + +export type PromptKey = (typeof promptKeys)[number]; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.ts index 5131ef6c86..9cf20b68f1 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/provider.ts @@ -1,284 +1,193 @@ -import { assertExists } from '@blocksuite/global/utils'; import { AIProvider } from '@blocksuite/presets'; -import { imageToTextStream, textToTextStream } from './request'; +import { textToText } from './request'; export function setupAIProvider() { AIProvider.provideAction('chat', options => { - assertExists(options.stream); - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt: options.input, + return textToText({ + ...options, + content: options.input, + promptName: 'debug:chat:gpt4', }); }); AIProvider.provideAction('summary', options => { - assertExists(options.stream); - const prompt = ` - Summarize the key points from the following content in a clear and concise manner, - suitable for a reader who is seeking a quick understanding of the original content. - Ensure to capture the main ideas and any significant details without unnecessary elaboration: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Summary', }); }); AIProvider.provideAction('translate', options => { - assertExists(options.stream); - const prompt = `Please translate the following content into ${options.lang} and return it to us, adhering to the original format of the content: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + promptName: 'Translate to', + content: options.input, + params: { + language: options.lang, + }, }); }); AIProvider.provideAction('changeTone', options => { - assertExists(options.stream); - const prompt = `Change the tone of the following content to ${options.tone}: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Change tone to', }); }); AIProvider.provideAction('improveWriting', options => { - assertExists(options.stream); - const prompt = `Improve the writing of the following content: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Improve writing for it', }); }); AIProvider.provideAction('improveGrammar', options => { - assertExists(options.stream); - const prompt = `Improve the grammar of the following content: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Improve grammar for it', }); }); AIProvider.provideAction('fixSpelling', options => { - assertExists(options.stream); - const prompt = `Fix the spelling of the following content: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Fix spelling for it', }); }); AIProvider.provideAction('createHeadings', options => { - assertExists(options.stream); - const prompt = `Create headings for the following content: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Create headings', }); }); AIProvider.provideAction('makeLonger', options => { - assertExists(options.stream); - const prompt = `Make the following content longer: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Make it longer', }); }); AIProvider.provideAction('makeShorter', options => { - assertExists(options.stream); - const prompt = `Make the following content shorter: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Make it shorter', }); }); AIProvider.provideAction('checkCodeErrors', options => { - assertExists(options.stream); - const prompt = `Check the code errors in the following content and provide the corrected version: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Check code error', }); }); AIProvider.provideAction('explainCode', options => { - assertExists(options.stream); - const prompt = `Explain the code in the following content, focusing on the logic, functions, and expected outcomes: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Explain this code', }); }); AIProvider.provideAction('writeArticle', options => { - assertExists(options.stream); - const prompt = `Write an article based on the following content, focusing on the main ideas, structure, and flow: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Write an article about this', }); }); AIProvider.provideAction('writeTwitterPost', options => { - assertExists(options.stream); - const prompt = `Write a Twitter post based on the following content, keeping it concise and engaging: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Write a twitter about this', }); }); AIProvider.provideAction('writePoem', options => { - assertExists(options.stream); - const prompt = `Write a poem based on the following content, focusing on the emotions, imagery, and rhythm: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Write a poem about this', }); }); AIProvider.provideAction('writeOutline', options => { - assertExists(options.stream); - const prompt = `Write an outline from the following content in Markdown: ${options.input}`; - - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Write outline', }); }); AIProvider.provideAction('writeBlogPost', options => { - assertExists(options.stream); - const prompt = `Write a blog post based on the following content, focusing on the insights, analysis, and personal perspective: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Write a blog post about this', }); }); AIProvider.provideAction('brainstorm', options => { - assertExists(options.stream); - const prompt = `Brainstorm ideas based on the following content, exploring different angles, perspectives, and approaches: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Brainstorm ideas about this', }); }); AIProvider.provideAction('findActions', options => { - assertExists(options.stream); - const prompt = `Find actions related to the following content and return content in markdown: ${options.input}`; - - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, - }); - }); - - AIProvider.provideAction('writeOutline', options => { - assertExists(options.stream); - const prompt = `Write an outline based on the following content, organizing the main points, subtopics, and structure: - - ${options.input} - `; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Find action items from it', }); }); AIProvider.provideAction('brainstormMindmap', options => { - assertExists(options.stream); - const prompt = `Use the nested unordered list syntax without other extra text style in Markdown to create a structure similar to a mind map without any unnecessary plain text description. Analyze the following questions or topics: ${options.input}`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Brainstorm mindmap', }); }); AIProvider.provideAction('explain', options => { - assertExists(options.stream); - const prompt = `Explain the following content in Markdown: ${options.input}`; - - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, + return textToText({ + ...options, + content: options.input, + promptName: 'Explain this', }); }); AIProvider.provideAction('explainImage', options => { - assertExists(options.stream); - const prompt = `Describe the scene captured in this image, focusing on the details, colors, emotions, and any interactions between subjects or objects present.`; - return textToTextStream({ - docId: options.docId, - workspaceId: options.workspaceId, - prompt, - attachments: options.attachments, + return textToText({ + ...options, + content: options.input, + promptName: 'Explain this image', }); }); AIProvider.provideAction('makeItReal', options => { - assertExists(options.stream); - const promptName = 'Make it real'; - return imageToTextStream({ - promptName, - docId: options.docId, - workspaceId: options.workspaceId, + return textToText({ + ...options, + promptName: 'Make it real', + // @ts-expect-error todo: fix this after blocksuite bump params: options.params, - attachments: options.attachments, content: options.content || 'Here are the latest wireframes. Could you make a new website based on these wireframes and notes and send back just the html file?', diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/request.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/request.ts index ea11c88d33..eedf20557b 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/request.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/request.ts @@ -1,107 +1,135 @@ -import { getBaseUrl } from '@affine/graphql'; -import { CopilotClient, toTextStream } from '@blocksuite/presets'; +import { toTextStream } from '@blocksuite/presets'; -const TIMEOUT = 500000; +import { CopilotClient } from './copilot-client'; +import type { PromptKey } from './prompt'; -export function textToTextStream({ - docId, - workspaceId, - prompt, - attachments, - params, -}: { +const TIMEOUT = 50000; + +const client = new CopilotClient(); + +function readBlobAsURL(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => { + if (typeof e.target?.result === 'string') { + resolve(e.target.result); + } else { + reject(); + } + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +export type TextToTextOptions = { docId: string; workspaceId: string; - prompt: string; - attachments?: string[]; - params?: string; -}): BlockSuitePresets.TextStream { - const client = new CopilotClient(getBaseUrl()); - return { - [Symbol.asyncIterator]: async function* () { - const hasAttachments = attachments && attachments.length > 0; - const session = await client.createSession({ - workspaceId, - docId, - promptName: hasAttachments ? 'debug:action:vision4' : 'Summary', - }); - if (hasAttachments) { - const messageId = await client.createMessage({ - sessionId: session, - content: prompt, + promptName: PromptKey; + content?: string; + attachments?: (string | Blob)[]; + params?: Record; + timeout?: number; + stream?: boolean; +}; + +async function createSessionMessage({ + docId, + workspaceId, + promptName, + content, + attachments, + params, +}: TextToTextOptions) { + const hasAttachments = attachments && attachments.length > 0; + const session = await client.createSession({ + workspaceId, + docId, + promptName, + }); + if (hasAttachments) { + const normalizedAttachments = await Promise.all( + attachments.map(async attachment => { + if (typeof attachment === 'string') { + return attachment; + } + const url = await readBlobAsURL(attachment); + return url; + }) + ); + const messageId = await client.createMessage({ + sessionId: session, + content, + attachments: normalizedAttachments, + params, + }); + return { + messageId, + session, + }; + } else if (content) { + return { + message: content, + session, + }; + } else { + throw new Error('No content or attachments provided'); + } +} + +export function textToText({ + docId, + workspaceId, + promptName, + content, + attachments, + params, + stream, + timeout = TIMEOUT, +}: TextToTextOptions) { + if (stream) { + return { + [Symbol.asyncIterator]: async function* () { + const message = await createSessionMessage({ + docId, + workspaceId, + promptName, + content, attachments, params, }); - const eventSource = client.textStream(messageId, session); - yield* toTextStream(eventSource, { timeout: TIMEOUT }); - } else { - const eventSource = client.textToTextStream(prompt, session); - yield* toTextStream(eventSource, { timeout: TIMEOUT }); - } - }, - }; -} -// Image to text(html) -export function imageToTextStream({ - docId, - workspaceId, - promptName, - ...options -}: { - docId: string; - workspaceId: string; - promptName: string; - params?: string; - content: string; - attachments?: string[]; -}) { - const client = new CopilotClient(getBaseUrl()); - return { - [Symbol.asyncIterator]: async function* () { - const sessionId = await client.createSession({ - workspaceId, + const eventSource = client.chatTextStream({ + sessionId: message.session, + messageId: message.messageId, + message: message.message, + }); + yield* toTextStream(eventSource, { timeout: timeout }); + }, + }; + } else { + return Promise.race([ + timeout + ? new Promise((_res, rej) => { + setTimeout(() => { + rej(new Error('Timeout')); + }, timeout); + }) + : null, + createSessionMessage({ docId, - promptName, - }); - const messageId = await client.createMessage({ - sessionId, - ...options, - }); - const eventSource = client.textStream(messageId, sessionId); - yield* toTextStream(eventSource, { timeout: TIMEOUT }); - }, - }; -} - -// Image to images -export function imageToImagesStream({ - docId, - workspaceId, - promptName, - ...options -}: { - docId: string; - workspaceId: string; - promptName: string; - content: string; - params?: string; - attachments?: string[]; -}) { - const client = new CopilotClient(getBaseUrl()); - return { - [Symbol.asyncIterator]: async function* () { - const sessionId = await client.createSession({ workspaceId, - docId, promptName, - }); - const messageId = await client.createMessage({ - sessionId, - ...options, - }); - const eventSource = client.imagesStream(messageId, sessionId); - yield* toTextStream(eventSource, { timeout: TIMEOUT }); - }, - }; + content, + attachments, + params, + }).then(message => { + return client.chatText({ + sessionId: message.session, + messageId: message.messageId, + message: message.message, + }); + }), + ]); + } } diff --git a/packages/frontend/graphql/codegen.yml b/packages/frontend/graphql/codegen.yml index 6faebf03a4..6870b8f23f 100644 --- a/packages/frontend/graphql/codegen.yml +++ b/packages/frontend/graphql/codegen.yml @@ -16,7 +16,7 @@ config: Decimal: number UUID: string ID: string - JSON: string + JSON: Record Upload: File SafeInt: number overwrite: true diff --git a/packages/frontend/graphql/src/graphql/create-copilot-message.gql b/packages/frontend/graphql/src/graphql/create-copilot-message.gql new file mode 100644 index 0000000000..d71f276d3d --- /dev/null +++ b/packages/frontend/graphql/src/graphql/create-copilot-message.gql @@ -0,0 +1,3 @@ +mutation createCopilotMessage($options: CreateChatMessageInput!) { + createCopilotMessage(options: $options) +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 9bcd67b69a..abcf33bc3b 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -144,6 +144,17 @@ mutation createCheckoutSession($input: CreateCheckoutSessionInput!) { }`, }; +export const createCopilotMessageMutation = { + id: 'createCopilotMessageMutation' as const, + operationName: 'createCopilotMessage', + definitionName: 'createCopilotMessage', + containsFile: false, + query: ` +mutation createCopilotMessage($options: CreateChatMessageInput!) { + createCopilotMessage(options: $options) +}`, +}; + export const createCopilotSessionMutation = { id: 'createCopilotSessionMutation' as const, operationName: 'createCopilotSession', diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index e1e2ab9bbf..cb9e8ed528 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -29,7 +29,7 @@ export interface Scalars { /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ DateTime: { input: string; output: string }; /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ - JSON: { input: string; output: string }; + JSON: { input: Record; output: Record }; /** The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. */ SafeInt: { input: number; output: number }; /** The `Upload` scalar type represents a file upload. */ @@ -240,6 +240,15 @@ export type CreateCheckoutSessionMutation = { createCheckoutSession: string; }; +export type CreateCopilotMessageMutationVariables = Exact<{ + options: CreateChatMessageInput; +}>; + +export type CreateCopilotMessageMutation = { + __typename?: 'Mutation'; + createCopilotMessage: string; +}; + export type CreateCopilotSessionMutationVariables = Exact<{ options: CreateChatSessionInput; }>; @@ -1214,6 +1223,11 @@ export type Mutations = variables: CreateCheckoutSessionMutationVariables; response: CreateCheckoutSessionMutation; } + | { + name: 'createCopilotMessageMutation'; + variables: CreateCopilotMessageMutationVariables; + response: CreateCopilotMessageMutation; + } | { name: 'createCopilotSessionMutation'; variables: CreateCopilotSessionMutationVariables;