diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index f2450066..a3cecf55 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -22,46 +22,46 @@ const checkContentCompatibility = ( [key: string]: unknown; }>, ): { isCompatible: boolean; message: string } => { - if ( - unstructuredContent.length !== 1 || - unstructuredContent[0].type !== "text" - ) { + // Look for at least one text content block that matches the structured content + const textBlocks = unstructuredContent.filter( + (block) => block.type === "text", + ); + + if (textBlocks.length === 0) { return { isCompatible: false, - message: "Unstructured content is not a single text block", + message: "No text blocks to match structured content", }; } - const textContent = unstructuredContent[0].text; - if (!textContent) { - return { - isCompatible: false, - message: "Text content is empty", - }; - } - - try { - const parsedContent = JSON.parse(textContent); - const isEqual = - JSON.stringify(parsedContent) === JSON.stringify(structuredContent); - - if (isEqual) { - return { - isCompatible: true, - message: "Unstructured content matches structured content", - }; - } else { - return { - isCompatible: false, - message: "Parsed JSON does not match structured content", - }; + // Check if any text block contains JSON that matches the structured content + for (const textBlock of textBlocks) { + const textContent = textBlock.text; + if (!textContent) { + continue; + } + + try { + const parsedContent = JSON.parse(textContent); + const isEqual = + JSON.stringify(parsedContent) === JSON.stringify(structuredContent); + + if (isEqual) { + return { + isCompatible: true, + message: `Structured content matches text block${textBlocks.length > 1 ? " (multiple blocks)" : ""}${unstructuredContent.length > textBlocks.length ? " + other content" : ""}`, + }; + } + } catch { + // Continue to next text block if this one doesn't parse as JSON + continue; } - } catch { - return { - isCompatible: false, - message: "Unstructured content is not valid JSON", - }; } + + return { + isCompatible: false, + message: "No text block matches structured content", + }; }; const ToolResults = ({ diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index fba1bba4..9b399af9 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -251,10 +251,12 @@ describe("ToolsTab", () => { }, }; - it("should display structured content when present", () => { - // Cache the tool's output schema so hasOutputSchema returns true + beforeEach(() => { + // Cache the tool's output schema before each test cacheToolOutputSchemas([toolWithOutputSchema]); + }); + it("should display structured content when present", () => { const structuredResult = { content: [], structuredContent: { @@ -263,6 +265,7 @@ describe("ToolsTab", () => { }; renderToolsTab({ + tools: [toolWithOutputSchema], selectedTool: toolWithOutputSchema, toolResult: structuredResult, }); @@ -274,8 +277,6 @@ describe("ToolsTab", () => { }); it("should show validation error for invalid structured content", () => { - cacheToolOutputSchemas([toolWithOutputSchema]); - const invalidResult = { content: [], structuredContent: { @@ -284,6 +285,7 @@ describe("ToolsTab", () => { }; renderToolsTab({ + tools: [toolWithOutputSchema], selectedTool: toolWithOutputSchema, toolResult: invalidResult, }); @@ -292,14 +294,13 @@ describe("ToolsTab", () => { }); it("should show error when tool with output schema doesn't return structured content", () => { - cacheToolOutputSchemas([toolWithOutputSchema]); - const resultWithoutStructured = { content: [{ type: "text", text: "some result" }], // No structuredContent }; renderToolsTab({ + tools: [toolWithOutputSchema], selectedTool: toolWithOutputSchema, toolResult: resultWithoutStructured, }); @@ -312,14 +313,13 @@ describe("ToolsTab", () => { }); it("should show unstructured content title when both structured and unstructured exist", () => { - cacheToolOutputSchemas([toolWithOutputSchema]); - const resultWithBoth = { content: [{ type: "text", text: '{"temperature": 25}' }], structuredContent: { temperature: 25 }, }; renderToolsTab({ + tools: [toolWithOutputSchema], selectedTool: toolWithOutputSchema, toolResult: resultWithBoth, }); @@ -344,26 +344,100 @@ describe("ToolsTab", () => { }); it("should show compatibility check when tool has output schema", () => { - cacheToolOutputSchemas([toolWithOutputSchema]); - const compatibleResult = { content: [{ type: "text", text: '{"temperature": 25}' }], structuredContent: { temperature: 25 }, }; renderToolsTab({ + tools: [toolWithOutputSchema], selectedTool: toolWithOutputSchema, toolResult: compatibleResult, }); // Should show compatibility result expect( - screen.getByText( - /matches structured content|not a single text block|not valid JSON|does not match/, - ), + screen.getByText(/structured content matches/i), ).toBeInTheDocument(); }); + it("should accept multiple content blocks with structured output", () => { + const multipleBlocksResult = { + content: [ + { type: "text", text: "Here is the weather data:" }, + { type: "text", text: '{"temperature": 25}' }, + { type: "text", text: "Have a nice day!" }, + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: multipleBlocksResult, + }); + + // Should show compatible result with multiple blocks + expect( + screen.getByText(/structured content matches.*multiple/i), + ).toBeInTheDocument(); + }); + + it("should accept mixed content types with structured output", () => { + const mixedContentResult = { + content: [ + { type: "text", text: "Weather report:" }, + { type: "text", text: '{"temperature": 25}' }, + { type: "image", data: "base64data", mimeType: "image/png" }, + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: mixedContentResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + + it("should reject when no text blocks match structured content", () => { + const noMatchResult = { + content: [ + { type: "text", text: "Some text" }, + { type: "text", text: '{"humidity": 60}' }, // Different structure + ], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: noMatchResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + + it("should reject when no text blocks are present", () => { + const noTextBlocksResult = { + content: [{ type: "image", data: "base64data", mimeType: "image/png" }], + structuredContent: { temperature: 25 }, + }; + + renderToolsTab({ + tools: [toolWithOutputSchema], + selectedTool: toolWithOutputSchema, + toolResult: noTextBlocksResult, + }); + + // Should render without crashing - the validation logic has been updated + expect(screen.getAllByText("weatherTool")).toHaveLength(2); + }); + it("should not show compatibility check when tool has no output schema", () => { const resultWithBoth = { content: [{ type: "text", text: '{"data": "value"}' }], @@ -378,7 +452,7 @@ describe("ToolsTab", () => { // Should not show any compatibility messages expect( screen.queryByText( - /matches structured content|not a single text block|not valid JSON|does not match/, + /structured content matches|no text blocks|no.*matches/i, ), ).not.toBeInTheDocument(); });