@tambo-ai/react 0.58.0 → 0.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -844,5 +844,379 @@ describe("TamboThreadProvider", () => {
844
844
  expect(result.current.thread.id).toBe("existing-thread-123");
845
845
  });
846
846
  });
847
+ describe("transformToContent", () => {
848
+ it("should use custom transformToContent when provided (non-streaming)", async () => {
849
+ const mockTransformToContent = jest.fn().mockReturnValue([
850
+ { type: "text", text: "Custom transformed content" },
851
+ {
852
+ type: "image_url",
853
+ image_url: { url: "https://example.com/image.png" },
854
+ },
855
+ ]);
856
+ const customToolRegistry = [
857
+ {
858
+ name: "TestComponent",
859
+ component: () => React.createElement("div", null, "Test"),
860
+ description: "Test",
861
+ propsSchema: z.object({ test: z.string() }),
862
+ associatedTools: [
863
+ {
864
+ name: "custom-tool",
865
+ tool: jest.fn().mockResolvedValue({ data: "tool result" }),
866
+ description: "Tool with custom transform",
867
+ toolSchema: z
868
+ .function()
869
+ .args(z.string())
870
+ .returns(z.object({ data: z.string() })),
871
+ transformToContent: mockTransformToContent,
872
+ },
873
+ ],
874
+ },
875
+ ];
876
+ const wrapperWithCustomTool = ({ children, }) => (React.createElement(TamboRegistryProvider, { components: customToolRegistry },
877
+ React.createElement(TamboContextHelpersProvider, { contextHelpers: {
878
+ currentTimeContextHelper: () => null,
879
+ currentPageContextHelper: () => null,
880
+ } },
881
+ React.createElement(TamboThreadProvider, { streaming: false }, children))));
882
+ const mockToolCallResponse = {
883
+ responseMessageDto: {
884
+ id: "tool-call-1",
885
+ content: [{ type: "text", text: "Tool response" }],
886
+ role: "tool",
887
+ threadId: "test-thread-1",
888
+ toolCallRequest: {
889
+ toolName: "custom-tool",
890
+ parameters: [{ parameterName: "input", parameterValue: "test" }],
891
+ },
892
+ componentState: {},
893
+ createdAt: new Date().toISOString(),
894
+ },
895
+ generationStage: GenerationStage.COMPLETE,
896
+ mcpAccessToken: "test-mcp-access-token",
897
+ };
898
+ jest
899
+ .mocked(mockThreadsApi.advanceByID)
900
+ .mockResolvedValueOnce(mockToolCallResponse)
901
+ .mockResolvedValueOnce({
902
+ responseMessageDto: {
903
+ id: "final-response",
904
+ content: [{ type: "text", text: "Final response" }],
905
+ role: "assistant",
906
+ threadId: "test-thread-1",
907
+ componentState: {},
908
+ createdAt: new Date().toISOString(),
909
+ },
910
+ generationStage: GenerationStage.COMPLETE,
911
+ mcpAccessToken: "test-mcp-access-token",
912
+ });
913
+ const { result } = renderHook(() => useTamboThread(), {
914
+ wrapper: wrapperWithCustomTool,
915
+ });
916
+ await act(async () => {
917
+ await result.current.sendThreadMessage("Use custom tool", {
918
+ threadId: "test-thread-1",
919
+ streamResponse: false,
920
+ });
921
+ });
922
+ // Verify the tool was called
923
+ expect(customToolRegistry[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith("test");
924
+ // Verify transformToContent was called with the tool result
925
+ expect(mockTransformToContent).toHaveBeenCalledWith({
926
+ data: "tool result",
927
+ });
928
+ // Verify the second advance call included the transformed content
929
+ expect(mockThreadsApi.advanceByID).toHaveBeenCalledTimes(2);
930
+ expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
931
+ messageToAppend: expect.objectContaining({
932
+ content: [
933
+ { type: "text", text: "Custom transformed content" },
934
+ {
935
+ type: "image_url",
936
+ image_url: { url: "https://example.com/image.png" },
937
+ },
938
+ ],
939
+ role: "tool",
940
+ }),
941
+ }));
942
+ });
943
+ it("should use custom async transformToContent when provided (streaming)", async () => {
944
+ const mockTransformToContent = jest
945
+ .fn()
946
+ .mockResolvedValue([
947
+ { type: "text", text: "Async transformed content" },
948
+ ]);
949
+ const customToolRegistry = [
950
+ {
951
+ name: "TestComponent",
952
+ component: () => React.createElement("div", null, "Test"),
953
+ description: "Test",
954
+ propsSchema: z.object({ test: z.string() }),
955
+ associatedTools: [
956
+ {
957
+ name: "async-tool",
958
+ tool: jest.fn().mockResolvedValue({ data: "async tool result" }),
959
+ description: "Tool with async transform",
960
+ toolSchema: z
961
+ .function()
962
+ .args(z.string())
963
+ .returns(z.object({ data: z.string() })),
964
+ transformToContent: mockTransformToContent,
965
+ },
966
+ ],
967
+ },
968
+ ];
969
+ const wrapperWithAsyncTool = ({ children, }) => (React.createElement(TamboRegistryProvider, { components: customToolRegistry },
970
+ React.createElement(TamboContextHelpersProvider, { contextHelpers: {
971
+ currentTimeContextHelper: () => null,
972
+ currentPageContextHelper: () => null,
973
+ } },
974
+ React.createElement(TamboThreadProvider, { streaming: true }, children))));
975
+ const mockToolCallChunk = {
976
+ responseMessageDto: {
977
+ id: "tool-call-chunk",
978
+ content: [{ type: "text", text: "Tool call" }],
979
+ role: "tool",
980
+ threadId: "test-thread-1",
981
+ toolCallRequest: {
982
+ toolName: "async-tool",
983
+ parameters: [
984
+ { parameterName: "input", parameterValue: "async-test" },
985
+ ],
986
+ },
987
+ componentState: {},
988
+ createdAt: new Date().toISOString(),
989
+ },
990
+ generationStage: GenerationStage.COMPLETE,
991
+ mcpAccessToken: "test-mcp-access-token",
992
+ };
993
+ const mockFinalChunk = {
994
+ responseMessageDto: {
995
+ id: "final-chunk",
996
+ content: [{ type: "text", text: "Final streaming response" }],
997
+ role: "assistant",
998
+ threadId: "test-thread-1",
999
+ componentState: {},
1000
+ createdAt: new Date().toISOString(),
1001
+ },
1002
+ generationStage: GenerationStage.COMPLETE,
1003
+ mcpAccessToken: "test-mcp-access-token",
1004
+ };
1005
+ const mockAsyncIterator = {
1006
+ [Symbol.asyncIterator]: async function* () {
1007
+ yield mockToolCallChunk;
1008
+ yield mockFinalChunk;
1009
+ },
1010
+ };
1011
+ jest
1012
+ .mocked(advanceStream)
1013
+ .mockResolvedValueOnce(mockAsyncIterator)
1014
+ .mockResolvedValueOnce({
1015
+ [Symbol.asyncIterator]: async function* () {
1016
+ yield mockFinalChunk;
1017
+ },
1018
+ });
1019
+ const { result } = renderHook(() => useTamboThread(), {
1020
+ wrapper: wrapperWithAsyncTool,
1021
+ });
1022
+ await act(async () => {
1023
+ await result.current.sendThreadMessage("Use async tool", {
1024
+ threadId: "test-thread-1",
1025
+ streamResponse: true,
1026
+ });
1027
+ });
1028
+ // Verify the tool was called
1029
+ expect(customToolRegistry[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith("async-test");
1030
+ // Verify transformToContent was called
1031
+ expect(mockTransformToContent).toHaveBeenCalledWith({
1032
+ data: "async tool result",
1033
+ });
1034
+ // Verify advanceStream was called twice (initial request and tool response)
1035
+ expect(advanceStream).toHaveBeenCalledTimes(2);
1036
+ // Verify the second advanceStream call included the transformed content
1037
+ expect(advanceStream).toHaveBeenLastCalledWith(mockTamboAI, expect.objectContaining({
1038
+ messageToAppend: expect.objectContaining({
1039
+ content: [{ type: "text", text: "Async transformed content" }],
1040
+ role: "tool",
1041
+ }),
1042
+ }), "test-thread-1");
1043
+ });
1044
+ it("should fallback to stringified text when transformToContent is not provided", async () => {
1045
+ const toolWithoutTransform = [
1046
+ {
1047
+ name: "TestComponent",
1048
+ component: () => React.createElement("div", null, "Test"),
1049
+ description: "Test",
1050
+ propsSchema: z.object({ test: z.string() }),
1051
+ associatedTools: [
1052
+ {
1053
+ name: "no-transform-tool",
1054
+ tool: jest
1055
+ .fn()
1056
+ .mockResolvedValue({ complex: "data", nested: { value: 42 } }),
1057
+ description: "Tool without custom transform",
1058
+ toolSchema: z
1059
+ .function()
1060
+ .args(z.string())
1061
+ .returns(z.object({
1062
+ complex: z.string(),
1063
+ nested: z.object({ value: z.number() }),
1064
+ })),
1065
+ // No transformToContent provided
1066
+ },
1067
+ ],
1068
+ },
1069
+ ];
1070
+ const wrapperWithoutTransform = ({ children, }) => (React.createElement(TamboRegistryProvider, { components: toolWithoutTransform },
1071
+ React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1072
+ currentTimeContextHelper: () => null,
1073
+ currentPageContextHelper: () => null,
1074
+ } },
1075
+ React.createElement(TamboThreadProvider, { streaming: false }, children))));
1076
+ const mockToolCallResponse = {
1077
+ responseMessageDto: {
1078
+ id: "tool-call-1",
1079
+ content: [{ type: "text", text: "Tool call" }],
1080
+ role: "tool",
1081
+ threadId: "test-thread-1",
1082
+ toolCallRequest: {
1083
+ toolName: "no-transform-tool",
1084
+ parameters: [{ parameterName: "input", parameterValue: "test" }],
1085
+ },
1086
+ componentState: {},
1087
+ createdAt: new Date().toISOString(),
1088
+ },
1089
+ generationStage: GenerationStage.COMPLETE,
1090
+ mcpAccessToken: "test-mcp-access-token",
1091
+ };
1092
+ jest
1093
+ .mocked(mockThreadsApi.advanceByID)
1094
+ .mockResolvedValueOnce(mockToolCallResponse)
1095
+ .mockResolvedValueOnce({
1096
+ responseMessageDto: {
1097
+ id: "final-response",
1098
+ content: [{ type: "text", text: "Final response" }],
1099
+ role: "assistant",
1100
+ threadId: "test-thread-1",
1101
+ componentState: {},
1102
+ createdAt: new Date().toISOString(),
1103
+ },
1104
+ generationStage: GenerationStage.COMPLETE,
1105
+ mcpAccessToken: "test-mcp-access-token",
1106
+ });
1107
+ const { result } = renderHook(() => useTamboThread(), {
1108
+ wrapper: wrapperWithoutTransform,
1109
+ });
1110
+ await act(async () => {
1111
+ await result.current.sendThreadMessage("Use tool without transform", {
1112
+ threadId: "test-thread-1",
1113
+ streamResponse: false,
1114
+ });
1115
+ });
1116
+ // Verify the tool was called
1117
+ expect(toolWithoutTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith("test");
1118
+ // Verify the second advance call used stringified content
1119
+ expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
1120
+ messageToAppend: expect.objectContaining({
1121
+ content: [
1122
+ {
1123
+ type: "text",
1124
+ text: '{"complex":"data","nested":{"value":42}}',
1125
+ },
1126
+ ],
1127
+ role: "tool",
1128
+ }),
1129
+ }));
1130
+ });
1131
+ it("should always return text for error responses even with transformToContent", async () => {
1132
+ const mockTransformToContent = jest.fn().mockReturnValue([
1133
+ {
1134
+ type: "image_url",
1135
+ image_url: { url: "https://example.com/error.png" },
1136
+ },
1137
+ ]);
1138
+ const toolWithTransform = [
1139
+ {
1140
+ name: "TestComponent",
1141
+ component: () => React.createElement("div", null, "Test"),
1142
+ description: "Test",
1143
+ propsSchema: z.object({ test: z.string() }),
1144
+ associatedTools: [
1145
+ {
1146
+ name: "error-tool",
1147
+ tool: jest
1148
+ .fn()
1149
+ .mockRejectedValue(new Error("Tool execution failed")),
1150
+ description: "Tool that errors",
1151
+ toolSchema: z.function().args(z.string()).returns(z.string()),
1152
+ transformToContent: mockTransformToContent,
1153
+ },
1154
+ ],
1155
+ },
1156
+ ];
1157
+ const wrapperWithErrorTool = ({ children, }) => (React.createElement(TamboRegistryProvider, { components: toolWithTransform },
1158
+ React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1159
+ currentTimeContextHelper: () => null,
1160
+ currentPageContextHelper: () => null,
1161
+ } },
1162
+ React.createElement(TamboThreadProvider, { streaming: false }, children))));
1163
+ const mockToolCallResponse = {
1164
+ responseMessageDto: {
1165
+ id: "tool-call-1",
1166
+ content: [{ type: "text", text: "Tool call" }],
1167
+ role: "tool",
1168
+ threadId: "test-thread-1",
1169
+ toolCallRequest: {
1170
+ toolName: "error-tool",
1171
+ parameters: [{ parameterName: "input", parameterValue: "test" }],
1172
+ },
1173
+ componentState: {},
1174
+ createdAt: new Date().toISOString(),
1175
+ },
1176
+ generationStage: GenerationStage.COMPLETE,
1177
+ mcpAccessToken: "test-mcp-access-token",
1178
+ };
1179
+ jest
1180
+ .mocked(mockThreadsApi.advanceByID)
1181
+ .mockResolvedValueOnce(mockToolCallResponse)
1182
+ .mockResolvedValueOnce({
1183
+ responseMessageDto: {
1184
+ id: "final-response",
1185
+ content: [{ type: "text", text: "Final response" }],
1186
+ role: "assistant",
1187
+ threadId: "test-thread-1",
1188
+ componentState: {},
1189
+ createdAt: new Date().toISOString(),
1190
+ },
1191
+ generationStage: GenerationStage.COMPLETE,
1192
+ mcpAccessToken: "test-mcp-access-token",
1193
+ });
1194
+ const { result } = renderHook(() => useTamboThread(), {
1195
+ wrapper: wrapperWithErrorTool,
1196
+ });
1197
+ await act(async () => {
1198
+ await result.current.sendThreadMessage("Use error tool", {
1199
+ threadId: "test-thread-1",
1200
+ streamResponse: false,
1201
+ });
1202
+ });
1203
+ // Verify the tool was called
1204
+ expect(toolWithTransform[0]?.associatedTools?.[0]?.tool).toHaveBeenCalledWith("test");
1205
+ // Verify transformToContent was NOT called for error responses
1206
+ expect(mockTransformToContent).not.toHaveBeenCalled();
1207
+ // Verify the second advance call used text content with the error message
1208
+ expect(mockThreadsApi.advanceByID).toHaveBeenLastCalledWith("test-thread-1", expect.objectContaining({
1209
+ messageToAppend: expect.objectContaining({
1210
+ content: [
1211
+ expect.objectContaining({
1212
+ type: "text",
1213
+ // Error message should be in text format
1214
+ }),
1215
+ ],
1216
+ role: "tool",
1217
+ }),
1218
+ }));
1219
+ });
1220
+ });
847
1221
  });
848
1222
  //# sourceMappingURL=tambo-thread-provider.test.js.map