From e13562e19a0724cc2acf58b8b7a918710ed37b2a Mon Sep 17 00:00:00 2001 From: Zelys Date: Mon, 11 May 2026 17:43:38 -0500 Subject: [PATCH 1/2] fix: escape double quotes and backslashes in AI directive string arguments `@conversation` and `@generation` directives interpolate user-supplied strings directly into GraphQL SDL without escaping special characters. Any systemPrompt or tool description containing a double quote or backslash produces invalid SDL, breaking schema compilation. - ConversationSchemaProcessor: add escapeGraphQLString helper (backslash first, then double quote, then newline) applied to systemPrompt and tool description - SchemaProcessor: route @generation systemPrompt through the existing escapeGraphQlString (JSON.stringify) helper already used for @validate and @sql Fixes aws-amplify/amplify-backend#2995 --- .../fix-escape-double-quotes-ai-prompts.md | 7 ++ .../ai/ConversationSchemaProcessor.test.ts | 95 +++++++++++++++++++ packages/data-schema/src/SchemaProcessor.ts | 12 +-- .../src/ai/ConversationSchemaProcessor.ts | 19 ++-- 4 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 .changeset/fix-escape-double-quotes-ai-prompts.md create mode 100644 packages/data-schema/__tests__/ai/ConversationSchemaProcessor.test.ts diff --git a/.changeset/fix-escape-double-quotes-ai-prompts.md b/.changeset/fix-escape-double-quotes-ai-prompts.md new file mode 100644 index 000000000..2073372b5 --- /dev/null +++ b/.changeset/fix-escape-double-quotes-ai-prompts.md @@ -0,0 +1,7 @@ +--- +"@aws-amplify/data-schema": patch +--- + +fix: escape double quotes and backslashes in AI directive string arguments + +`@conversation` and `@generation` directives interpolate user-supplied strings (`systemPrompt`, tool `description`) directly into GraphQL SDL without escaping special characters. Any prompt containing a double quote or backslash produces invalid SDL, breaking schema compilation. This fix escapes all GraphQL special characters before interpolation. diff --git a/packages/data-schema/__tests__/ai/ConversationSchemaProcessor.test.ts b/packages/data-schema/__tests__/ai/ConversationSchemaProcessor.test.ts new file mode 100644 index 000000000..18f8d783d --- /dev/null +++ b/packages/data-schema/__tests__/ai/ConversationSchemaProcessor.test.ts @@ -0,0 +1,95 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { a } from '../../src/index'; +import { defineFunctionStub } from '../utils'; + +describe('GraphQL string escaping in AI schema directives', () => { + describe('@generation', () => { + test('escapes double quotes in systemPrompt', () => { + const schema = a.schema({ + Result: a.customType({ value: a.string() }), + makeResult: a + .generation({ + aiModel: a.ai.model('Claude 3 Haiku'), + systemPrompt: 'Always say "yes" or "no".', + }) + .returns(a.ref('Result')), + }); + + const { schema: graphql } = schema.transform(); + + expect(graphql).toContain('systemPrompt: "Always say \\"yes\\" or \\"no\\"."'); + }); + }); + + describe('@conversation', () => { + test('escapes double quotes in systemPrompt', () => { + const schema = a.schema({ + ChatBot: a.conversation({ + aiModel: a.ai.model('Claude 3 Haiku'), + systemPrompt: 'Say "hello" and "goodbye" to users.', + }).authorization((allow) => allow.owner()), + }); + + const { schema: graphql } = schema.transform(); + + expect(graphql).toContain('systemPrompt: "Say \\"hello\\" and \\"goodbye\\" to users."'); + }); + + test('escapes double quotes in tool description', () => { + const handler = defineFunctionStub({}); + const schema = a.schema({ + Profile: a.customType({ value: a.integer() }), + infoQuery: a + .query() + .returns(a.ref('Profile')) + .authorization((allow) => allow.publicApiKey()) + .handler(a.handler.function(handler)), + + ChatBot: a.conversation({ + aiModel: a.ai.model('Claude 3 Haiku'), + systemPrompt: 'You are helpful.', + tools: [ + a.ai.dataTool({ + query: a.ref('infoQuery'), + name: 'infoQuery', + description: 'Fetches "live" profile data.', + }), + ], + }).authorization((allow) => allow.owner()), + }); + + const { schema: graphql } = schema.transform(); + + expect(graphql).toContain('description: "Fetches \\"live\\" profile data."'); + }); + + test('escapes backslashes in systemPrompt', () => { + const schema = a.schema({ + ChatBot: a.conversation({ + aiModel: a.ai.model('Claude 3 Haiku'), + systemPrompt: 'Use path C:\\\\docs for all outputs.', + }).authorization((allow) => allow.owner()), + }); + + const { schema: graphql } = schema.transform(); + + expect(graphql).toContain('systemPrompt: "Use path C:\\\\\\\\docs for all outputs."'); + }); + + test('preserves newline escaping in multiline systemPrompt', () => { + const schema = a.schema({ + ChatBot: a.conversation({ + aiModel: a.ai.model('Claude 3 Haiku'), + systemPrompt: `You are helpful. +Respond in haiku.`, + }).authorization((allow) => allow.owner()), + }); + + const { schema: graphql } = schema.transform(); + + expect(graphql).toContain('systemPrompt: "You are helpful.\\nRespond in haiku."'); + }); + }); +}); diff --git a/packages/data-schema/src/SchemaProcessor.ts b/packages/data-schema/src/SchemaProcessor.ts index bb2a31dc7..56636cf2d 100644 --- a/packages/data-schema/src/SchemaProcessor.ts +++ b/packages/data-schema/src/SchemaProcessor.ts @@ -539,16 +539,6 @@ function customOperationToGql( const { aiModel, systemPrompt, inferenceConfiguration } = typeDef.data.input; - // This is done to escape newlines in potentially multi-line system prompts - // e.g. - // generateStuff: a.generation({ - // aiModel: a.ai.model('Claude 3 Haiku'), - // systemPrompt: `Generate a haiku - // make it multiline`, - // }), - // - // It doesn't affect non multi-line string inputs for system prompts - const escapedSystemPrompt = systemPrompt.replace(/\r?\n/g, '\\n'); const inferenceConfigurationEntries = Object.entries( inferenceConfiguration ?? {}, ); @@ -558,7 +548,7 @@ function customOperationToGql( .map(([key, value]) => `${key}: ${value}`) .join(', ')} }` : ''; - gqlHandlerContent += `@generation(aiModel: "${aiModel.resourcePath}", systemPrompt: "${escapedSystemPrompt}"${inferenceConfigurationGql}) `; + gqlHandlerContent += `@generation(aiModel: "${aiModel.resourcePath}", systemPrompt: ${escapeGraphQlString(systemPrompt)}${inferenceConfigurationGql}) `; } const gqlField = `${callSignature}: ${returnTypeName} ${gqlHandlerContent}${authString}`; diff --git a/packages/data-schema/src/ai/ConversationSchemaProcessor.ts b/packages/data-schema/src/ai/ConversationSchemaProcessor.ts index 8d4e52a55..0144e17b2 100644 --- a/packages/data-schema/src/ai/ConversationSchemaProcessor.ts +++ b/packages/data-schema/src/ai/ConversationSchemaProcessor.ts @@ -8,6 +8,12 @@ import type { } from './ConversationType'; import type { InferenceConfiguration } from './ModelType'; +const escapeGraphQLString = (str: string): string => + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\r?\n/g, '\\n'); + export const createConversationField = ( typeDef: InternalConversationType, typeName: string, @@ -18,16 +24,7 @@ export const createConversationField = ( const args: Record = { aiModel: aiModel.resourcePath, - // This is done to escape newlines in potentially multi-line system prompts - // e.g. - // realtorChat: a.conversation({ - // aiModel: a.ai.model('Claude 3 Haiku'), - // systemPrompt: `You are a helpful real estate assistant - // Respond in the poetic form of haiku.`, - // }), - // - // It doesn't affect non multi-line string inputs for system prompts - systemPrompt: systemPrompt.replace(/\r?\n/g, '\\n'), + systemPrompt: escapeGraphQLString(systemPrompt), }; // Add each arg with quotes (aiModel and systemPrompt) @@ -126,7 +123,7 @@ const getConversationToolsString = (tools: DataToolDefinition[]) => ); } const toolDefinition = extractToolDefinition(tool); - return `{ name: "${name}", description: "${description}", ${toolDefinition} }`; + return `{ name: "${name}", description: "${escapeGraphQLString(description)}", ${toolDefinition} }`; }) .join(', '); From c0f9d53108dd06ad3d8e97d369ddacd577467ab4 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 12 May 2026 11:17:53 -0500 Subject: [PATCH 2/2] refactor(ai): use JSON.stringify for GraphQL string escaping in ConversationSchemaProcessor --- packages/data-schema/src/ai/ConversationSchemaProcessor.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/data-schema/src/ai/ConversationSchemaProcessor.ts b/packages/data-schema/src/ai/ConversationSchemaProcessor.ts index 0144e17b2..7b9f5fe36 100644 --- a/packages/data-schema/src/ai/ConversationSchemaProcessor.ts +++ b/packages/data-schema/src/ai/ConversationSchemaProcessor.ts @@ -9,10 +9,7 @@ import type { import type { InferenceConfiguration } from './ModelType'; const escapeGraphQLString = (str: string): string => - str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\r?\n/g, '\\n'); + JSON.stringify(str).slice(1, -1); export const createConversationField = ( typeDef: InternalConversationType,