Skip to content

Commit b80bab0

Browse files
authored
fix(ai): fix renders for empty toolUse messages (#5799)
* fix(ai): fix renders for empty toolUse messages * Create ten-beds-jump.md * memoize response components value * update component tool prefix
1 parent f2fb980 commit b80bab0

File tree

6 files changed

+103
-7
lines changed

6 files changed

+103
-7
lines changed

.changeset/ten-beds-jump.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@aws-amplify/ui-react-ai": patch
3+
---
4+
5+
fix(ai): fix renders for empty toolUse messages

packages/react-ai/src/components/AIConversation/context/ResponseComponentsContext.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,36 @@ import { ToolConfiguration } from '../../../types';
44

55
type ResponseComponentsContextProps = ResponseComponents | undefined;
66

7+
export const RESPONSE_COMPONENT_PREFIX = 'AMPLIFY_UI_';
8+
79
export const ResponseComponentsContext =
810
React.createContext<ResponseComponentsContextProps>(undefined);
911

12+
const prependResponseComponents = (responseComponents?: ResponseComponents) => {
13+
if (!responseComponents) return responseComponents;
14+
return Object.keys(responseComponents).reduce(
15+
(prev, key) => (
16+
(prev[`${RESPONSE_COMPONENT_PREFIX}${key}`] = responseComponents[key]),
17+
prev
18+
),
19+
{} as ResponseComponents
20+
);
21+
};
22+
1023
export const ResponseComponentsProvider = ({
1124
children,
1225
responseComponents,
1326
}: {
1427
children?: React.ReactNode;
1528
responseComponents?: ResponseComponents;
1629
}): JSX.Element => {
30+
const _responseComponents = React.useMemo(
31+
() => prependResponseComponents(responseComponents),
32+
[responseComponents]
33+
);
34+
1735
return (
18-
<ResponseComponentsContext.Provider value={responseComponents}>
36+
<ResponseComponentsContext.Provider value={_responseComponents}>
1937
{children}
2038
</ResponseComponentsContext.Provider>
2139
);

packages/react-ai/src/components/AIConversation/context/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export {
2929
ControlsProvider,
3030
} from './ControlsContext';
3131
export { LoadingContextProvider } from './LoadingContext';
32-
export { ResponseComponentsProvider } from './ResponseComponentsContext';
32+
export {
33+
ResponseComponentsProvider,
34+
RESPONSE_COMPONENT_PREFIX,
35+
} from './ResponseComponentsContext';
3336
export { SendMessageContextProvider } from './SendMessageContext';
3437

3538
export * from './elements';

packages/react-ai/src/components/AIConversation/views/Controls/MessagesControl.tsx

+20-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { convertBufferToBase64 } from '../../utils';
1212
import { ActionsBarControl } from './ActionsBarControl';
1313
import { AvatarControl } from './AvatarControl';
1414
import { ConversationMessage } from '../../../../types';
15-
import { ResponseComponentsContext } from '../../context/ResponseComponentsContext';
15+
import {
16+
RESPONSE_COMPONENT_PREFIX,
17+
ResponseComponentsContext,
18+
} from '../../context/ResponseComponentsContext';
1619
import { ControlsContext } from '../../context/ControlsContext';
1720

1821
const { Image, Span, Text, View } = AIConversationElements;
@@ -83,7 +86,11 @@ export const MessageControl: MessageControl = ({ message }) => {
8386
} else if (content.toolUse) {
8487
// For now tool use is limited to custom response components
8588
const { name, input } = content.toolUse;
86-
if (!responseComponents || !name) {
89+
if (
90+
!responseComponents ||
91+
!name ||
92+
!name.startsWith(RESPONSE_COMPONENT_PREFIX)
93+
) {
8794
return;
8895
} else {
8996
const response = responseComponents[name];
@@ -206,9 +213,19 @@ export const MessagesControl: MessagesControl = ({ renderMessage }) => {
206213
return <controls.MessageList messages={messages!} />;
207214
}
208215

216+
const messagesWithRenderableContent =
217+
messages?.filter((message) =>
218+
message.content.some(
219+
(content) =>
220+
content.image ??
221+
content.text ??
222+
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX)
223+
)
224+
) ?? [];
225+
209226
return (
210227
<Layout>
211-
{messages?.map((message, index) => {
228+
{messagesWithRenderableContent?.map((message, index) => {
212229
return renderMessage ? (
213230
renderMessage(message)
214231
) : (

packages/react-ai/src/components/AIConversation/views/Controls/__tests__/MessagesControl.spec.tsx

+43-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,38 @@ const AIResponseComponentMessage: ConversationMessage = {
5454
content: [
5555
{
5656
toolUse: {
57-
name: 'annoyingComponent',
57+
name: 'AMPLIFY_UI_annoyingComponent',
58+
input: { text: 'ahoy matey' },
59+
toolUseId: 'tooluseID',
60+
},
61+
},
62+
],
63+
role: 'assistant',
64+
createdAt: new Date(2023, 4, 21, 15, 25).toDateString(),
65+
};
66+
const ToolUseMessage: ConversationMessage = {
67+
conversationId: 'foobar',
68+
id: '3',
69+
content: [
70+
{
71+
toolUse: {
72+
name: '"generateRecipe"',
73+
input: { text: 'ahoy matey' },
74+
toolUseId: 'tooluseID',
75+
},
76+
},
77+
],
78+
role: 'assistant',
79+
createdAt: new Date(2023, 4, 21, 15, 25).toDateString(),
80+
};
81+
const TextAndToolUseMessage: ConversationMessage = {
82+
conversationId: 'foobar',
83+
id: '3',
84+
content: [
85+
{ text: 'hey what up' },
86+
{
87+
toolUse: {
88+
name: '"generateRecipe"',
5889
input: { text: 'ahoy matey' },
5990
toolUseId: 'tooluseID',
6091
},
@@ -345,4 +376,15 @@ describe('MessageControl', () => {
345376
const message = screen.getByText('argh matey! ahoy matey');
346377
expect(message).toBeInTheDocument();
347378
});
379+
380+
it('renders text when sent with a tooluse content', () => {
381+
render(<MessageControl message={TextAndToolUseMessage} />);
382+
const message = screen.getByText('hey what up');
383+
expect(message).toBeInTheDocument();
384+
});
385+
386+
it('renders nothing when only a toolUse block is sent', () => {
387+
const { container } = render(<MessageControl message={ToolUseMessage} />);
388+
expect(container.firstChild).toBeEmptyDOMElement();
389+
});
348390
});

packages/react-ai/src/components/AIConversation/views/default/MessageList.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MessageControl } from '../Controls/MessagesControl';
44
import {
55
AvatarsContext,
66
MessageVariantContext,
7+
RESPONSE_COMPONENT_PREFIX,
78
RoleContext,
89
useConversationDisplayText,
910
} from '../../context';
@@ -99,9 +100,19 @@ export const MessageList: ControlsContextProps['MessageList'] = ({
99100
messages,
100101
}) => {
101102
const isLoading = React.useContext(LoadingContext);
103+
const messagesWithRenderableContent =
104+
messages?.filter((message) =>
105+
message.content.some(
106+
(content) =>
107+
content.image ??
108+
content.text ??
109+
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX)
110+
)
111+
) ?? [];
112+
102113
return (
103114
<View className={ComponentClassName.AIConversationMessageList}>
104-
{messages.map((message, i) => (
115+
{messagesWithRenderableContent.map((message, i) => (
105116
<Message key={`message-${i}`} message={message} />
106117
))}
107118
{isLoading ? <LoadingMessage /> : null}

0 commit comments

Comments
 (0)