Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-games-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/ui-react-ai': minor
---

Add Amazon Bedrock Guardrails support to AIConversation component
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ConversationDisplayTextProvider,
ConversationInputContextProvider,
FallbackComponentProvider,
GuardrailsProvider,
LoadingContextProvider,
MessageRendererProvider,
MessagesProvider,
Expand All @@ -35,6 +36,7 @@ export const AIConversationProvider = ({
children,
controls,
displayText,
guardrails,
handleSendMessage,
isLoading,
maxAttachmentSize,
Expand Down Expand Up @@ -80,9 +82,13 @@ export const AIConversationProvider = ({
{/* because the intent is users should update the context */}
{/* without it affecting the already rendered messages */}
<AIContextProvider aiContext={aiContext}>
<LoadingContextProvider isLoading={isLoading}>
{children}
</LoadingContextProvider>
<GuardrailsProvider guardrails={guardrails}>
<LoadingContextProvider
isLoading={isLoading}
>
{children}
</LoadingContextProvider>
</GuardrailsProvider>
</AIContextProvider>
</MessagesProvider>
</MessageVariantProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import type { GuardrailConfiguration } from '../../../types';

/**
* Context that holds the optional Amazon Bedrock Guardrail configuration
* for the current conversation. When set, the guardrail will be applied
* to every message sent in the conversation.
*/
export const GuardrailsContext = React.createContext<
GuardrailConfiguration | undefined
>(undefined);

export const GuardrailsProvider = ({
children,
guardrails,
}: {
children?: React.ReactNode;
guardrails?: GuardrailConfiguration;
}): React.JSX.Element => {
return (
<GuardrailsContext.Provider value={guardrails}>
{children}
</GuardrailsContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render, screen } from '@testing-library/react';
import { GuardrailsContext, GuardrailsProvider } from '../GuardrailsContext';
import type { GuardrailConfiguration } from '../../../../types';

const TestConsumer = () => {
const guardrails = React.useContext(GuardrailsContext);
if (!guardrails) return <div data-testid="no-guardrails">none</div>;
return (
<div data-testid="guardrails">
<span data-testid="id">{guardrails.guardrailIdentifier}</span>
<span data-testid="version">{guardrails.guardrailVersion}</span>
{guardrails.trace && <span data-testid="trace">{guardrails.trace}</span>}
</div>
);
};

describe('GuardrailsContext', () => {
it('provides undefined by default', () => {
render(<TestConsumer />);
expect(screen.getByTestId('no-guardrails')).toBeInTheDocument();
});

it('provides guardrail configuration to children', () => {
const config: GuardrailConfiguration = {
guardrailIdentifier: 'gr-abc123',
guardrailVersion: '1',
};
render(
<GuardrailsProvider guardrails={config}>
<TestConsumer />
</GuardrailsProvider>
);
expect(screen.getByTestId('id')).toHaveTextContent('gr-abc123');
expect(screen.getByTestId('version')).toHaveTextContent('1');
expect(screen.queryByTestId('trace')).not.toBeInTheDocument();
});

it('provides trace mode when specified', () => {
const config: GuardrailConfiguration = {
guardrailIdentifier: 'gr-def456',
guardrailVersion: 'DRAFT',
trace: 'enabled',
};
render(
<GuardrailsProvider guardrails={config}>
<TestConsumer />
</GuardrailsProvider>
);
expect(screen.getByTestId('id')).toHaveTextContent('gr-def456');
expect(screen.getByTestId('version')).toHaveTextContent('DRAFT');
expect(screen.getByTestId('trace')).toHaveTextContent('enabled');
});

it('renders nothing for guardrails when guardrails prop is undefined', () => {
render(
<GuardrailsProvider guardrails={undefined}>
<TestConsumer />
</GuardrailsProvider>
);
expect(screen.getByTestId('no-guardrails')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { AIContextContext, AIContextProvider } from './AIContextContext';
export { GuardrailsContext, GuardrailsProvider } from './GuardrailsContext';
export { ActionsContext, ActionsProvider } from './ActionsContext';
export { AvatarsContext, AvatarsProvider } from './AvatarsContext';
export type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
allowAttachments,
messageRenderer,
FallbackResponseComponent,
guardrails,
} = input;

function AIConversation(props: AIConversationProps): React.JSX.Element {
Expand All @@ -40,6 +41,7 @@ export function createAIConversation(input: AIConversationInput = {}): {
isLoading,
messageRenderer,
FallbackResponseComponent,
guardrails,
};
return (
<AIConversationProvider {...providerProps}>
Expand Down
9 changes: 9 additions & 0 deletions packages/react-ai/src/components/AIConversation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { DisplayTextTemplate } from '@aws-amplify/ui';
import type { AIConversationDisplayText } from './displayText';
import type {
ConversationMessage,
GuardrailConfiguration,
SendMessage,
ResponseComponents,
TextContentBlock,
Expand Down Expand Up @@ -40,6 +41,14 @@ export interface AIConversationInput {
maxAttachments?: number;
maxAttachmentSize?: number;
messageRenderer?: MessageRenderer;
/**
* Amazon Bedrock Guardrail configuration to apply to every message in
* this conversation. Guardrails enforce safety policies such as content
* filtering, denied topics, and sensitive information redaction.
*
* @see https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html
*/
guardrails?: GuardrailConfiguration;
}

export interface AIConversationProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ConversationInput } from '../../context';
import {
AIContextContext,
ConversationInputContext,
GuardrailsContext,
useConversationDisplayText,
} from '../../context';
import { AIConversationElements } from '../../context/elements';
Expand Down Expand Up @@ -179,6 +180,7 @@ export const FormControl: FormControl = () => {
const responseComponents = React.useContext(ResponseComponentsContext);
const isLoading = React.useContext(LoadingContext);
const aiContext = React.useContext(AIContextContext);
const guardrails = React.useContext(GuardrailsContext);
const ref = React.useRef<HTMLFormElement | null>(null);
const controls = React.useContext(ControlsContext);
const [composing, setComposing] = React.useState(false);
Expand Down Expand Up @@ -234,7 +236,12 @@ export const FormControl: FormControl = () => {
aiContext: isFunction(aiContext) ? aiContext() : undefined,
toolConfiguration:
convertResponseComponentsToToolConfiguration(responseComponents),
});
// NOTE: guardrailConfiguration is not yet in the upstream
// @aws-amplify/data-schema sendMessage type. This cast is safe
// because the Bedrock Converse API supports guardrailConfig natively.
// The cast will be removed once the upstream type is updated.
...(guardrails ? { guardrailConfiguration: guardrails } : {}),
} as Parameters<typeof handleSendMessage>[0]);
}

// Clear the attachment errors when submitting
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import React from 'react';
import { render, screen, act } from '@testing-library/react';
import { render, screen, act, cleanup } from '@testing-library/react';
import { MessagesProvider } from '../../../context/MessagesContext';
import { FormControl } from '../FormControl';
import { ConversationInputContextProvider } from '../../../context/ConversationInputContext';
import userEvent from '@testing-library/user-event';
import { SendMessageContextProvider } from '../../../context/SendMessageContext';
import { AttachmentProvider } from '../../../context/AttachmentContext';
import { GuardrailsProvider } from '../../../context/GuardrailsContext';
import type { SendMesageParameters } from '../../../../../types';

describe('FieldControl', () => {
afterEach(cleanup);

it('renders a FieldControl component with the correct elements', () => {
const result = render(
<AttachmentProvider allowAttachments>
Expand Down Expand Up @@ -110,6 +114,57 @@ describe('FieldControl', () => {
expect(sendMessage).toHaveBeenCalledTimes(1);
});

it('sends message WITHOUT guardrailConfiguration when no guardrails are configured', async () => {
const sendMessage = jest.fn();
render(
<SendMessageContextProvider handleSendMessage={sendMessage}>
<ConversationInputContextProvider>
<FormControl />
</ConversationInputContextProvider>
</SendMessageContextProvider>
);
const textInput = screen.getByTestId('text-input');
await act(async () => {
await userEvent.type(textInput, 'test');
});
screen.getByTestId('send-button').click();
expect(sendMessage).toHaveBeenCalledTimes(1);
// Typed access via jest mock — avoids @typescript-eslint/no-unsafe-member-access
const [callArg] = jest.mocked(sendMessage).mock.calls[0] as [
SendMesageParameters,
];
expect(callArg).not.toHaveProperty('guardrailConfiguration');
});

it('sends message WITH guardrailConfiguration when GuardrailsProvider is set', async () => {
const sendMessage = jest.fn();
const guardrailConfig = {
guardrailIdentifier: 'gr-test123',
guardrailVersion: '2',
trace: 'enabled' as const,
};
render(
<GuardrailsProvider guardrails={guardrailConfig}>
<SendMessageContextProvider handleSendMessage={sendMessage}>
<ConversationInputContextProvider>
<FormControl />
</ConversationInputContextProvider>
</SendMessageContextProvider>
</GuardrailsProvider>
);
const textInput = screen.getByTestId('text-input');
await act(async () => {
await userEvent.type(textInput, 'hello guardrails');
});
screen.getByTestId('send-button').click();
expect(sendMessage).toHaveBeenCalledTimes(1);
// Typed access via jest mock — avoids @typescript-eslint/no-unsafe-member-access
const [callArg] = jest.mocked(sendMessage).mock.calls[0] as [
SendMesageParameters,
];
expect(callArg.guardrailConfiguration).toEqual(guardrailConfig);
});

it.todo('disables the send button when waiting for an AI message');
it.todo('attaches a file to the message when the attach button is clicked');
});
1 change: 1 addition & 0 deletions packages/react-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { createAIHooks } from './hooks';
export type {
ConversationMessage,
ConversationMessageContent,
GuardrailConfiguration,
SendMessage,
SendMesageParameters,
} from './types';
21 changes: 21 additions & 0 deletions packages/react-ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,31 @@ export type ToolConfiguration = NonNullable<
>['toolConfiguration']
>;

/**
* Configuration for Amazon Bedrock Guardrails.
* Guardrails enforce safety policies (content filters, denied topics,
* sensitive information redaction, etc.) on AI responses.
*
* @see https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html
*/
export interface GuardrailConfiguration {
/** The unique identifier of the guardrail. */
guardrailIdentifier: string;
/** The version of the guardrail. Use "DRAFT" for draft version. */
guardrailVersion: string;
/**
* Whether to enable trace mode for the guardrail.
* When enabled, the response will include details about which guardrail
* policies were triggered.
*/
trace?: 'enabled' | 'disabled';
}

export interface SendMesageParameters {
content: SendMessageContent;
aiContext?: SendMessageContext;
toolConfiguration?: ToolConfiguration;
guardrailConfiguration?: GuardrailConfiguration;
}

export type SendMessage = (input: SendMesageParameters) => void;
Expand Down