@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.
- package/dist/mcp/tambo-mcp-provider.js +1 -1
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/dist/model/component-metadata.d.ts +1 -1
- package/dist/model/component-metadata.d.ts.map +1 -1
- package/dist/model/component-metadata.js.map +1 -1
- package/dist/providers/__tests__/tambo-thread-provider.test.js +374 -0
- package/dist/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
- package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-provider.js +6 -8
- package/dist/providers/tambo-thread-provider.js.map +1 -1
- package/dist/testing/tools.d.ts +1 -1
- package/esm/mcp/tambo-mcp-provider.js +1 -1
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/model/component-metadata.d.ts +1 -1
- package/esm/model/component-metadata.d.ts.map +1 -1
- package/esm/model/component-metadata.js.map +1 -1
- package/esm/providers/__tests__/tambo-thread-provider.test.js +374 -0
- package/esm/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
- package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-provider.js +6 -8
- package/esm/providers/tambo-thread-provider.js.map +1 -1
- package/esm/testing/tools.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|