From e5c24045ecf8ce17749fa65124b39433eff71e89 Mon Sep 17 00:00:00 2001 From: Yogesh Vaishnav Date: Tue, 27 Jan 2026 00:16:04 +0530 Subject: [PATCH 1/2] fix: change return method from toArray() to all() in ResponseBuilder --- src/Structured/ResponseBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Structured/ResponseBuilder.php b/src/Structured/ResponseBuilder.php index f36423ac..0c293dbd 100644 --- a/src/Structured/ResponseBuilder.php +++ b/src/Structured/ResponseBuilder.php @@ -100,7 +100,7 @@ protected function aggregateToolResults(): array return $this->steps ->flatMap(fn (Step $step): array => $step->toolResults) ->values() - ->toArray(); + ->all(); } protected function calculateTotalUsage(): Usage From 12d1917deb686884c40e4bd62d651b7967aa23b1 Mon Sep 17 00:00:00 2001 From: Yogesh Vaishnav Date: Tue, 27 Jan 2026 00:29:39 +0530 Subject: [PATCH 2/2] tests: write unit tests for tool calls and tool results aggregation --- tests/Structured/ResponseBuilderTest.php | 138 +++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/tests/Structured/ResponseBuilderTest.php b/tests/Structured/ResponseBuilderTest.php index 4ed02529..1d6ae112 100644 --- a/tests/Structured/ResponseBuilderTest.php +++ b/tests/Structured/ResponseBuilderTest.php @@ -7,6 +7,8 @@ use Prism\Prism\Structured\ResponseBuilder; use Prism\Prism\Structured\Step; use Prism\Prism\ValueObjects\Meta; +use Prism\Prism\ValueObjects\ToolCall; +use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; test('throws a PrismStructuredDecodingException if the response is not valid json', function (): void { @@ -89,3 +91,139 @@ expect($response->structured)->toBe(['value' => 42]); }); + +test('StructuredResponseBuilder aggregates tool calls from multiple steps as ToolCall objects', function (): void { + $builder = new ResponseBuilder; + + $toolCall1 = new ToolCall( + id: 'call_1', + name: 'get_weather', + arguments: '{"location":"NYC"}' + ); + + $toolCall2 = new ToolCall( + id: 'call_2', + name: 'get_temperature', + arguments: ['location' => 'LA'] + ); + + $toolCall3 = new ToolCall( + id: 'call_3', + name: 'get_humidity', + arguments: '{"location":"SF"}' + ); + + $builder->addStep(new Step( + text: '', + finishReason: FinishReason::ToolCalls, + usage: new Usage(promptTokens: 10, completionTokens: 5), + meta: new Meta('step1', 'test-model'), + messages: [], + systemPrompts: [], + toolCalls: [$toolCall1, $toolCall2], + )); + + $builder->addStep(new Step( + text: '{"result":"success"}', + finishReason: FinishReason::Stop, + usage: new Usage(promptTokens: 5, completionTokens: 3), + meta: new Meta('step2', 'test-model'), + messages: [], + systemPrompts: [], + toolCalls: [$toolCall3], + )); + + $response = $builder->toResponse(); + + expect($response->toolCalls)->toHaveCount(3) + ->and($response->toolCalls[0])->toBeInstanceOf(ToolCall::class) + ->and($response->toolCalls[0]->id)->toBe('call_1') + ->and($response->toolCalls[0]->name)->toBe('get_weather') + ->and($response->toolCalls[1])->toBeInstanceOf(ToolCall::class) + ->and($response->toolCalls[1]->id)->toBe('call_2') + ->and($response->toolCalls[1]->name)->toBe('get_temperature') + ->and($response->toolCalls[2])->toBeInstanceOf(ToolCall::class) + ->and($response->toolCalls[2]->id)->toBe('call_3') + ->and($response->toolCalls[2]->name)->toBe('get_humidity'); +}); + +test('StructuredResponseBuilder aggregates tool results from multiple steps as ToolResult objects', function (): void { + $builder = new ResponseBuilder; + + $toolResult1 = new ToolResult( + toolCallId: 'call_1', + toolName: 'get_weather', + args: ['location' => 'NYC'], + result: ['temperature' => 75, 'condition' => 'sunny'] + ); + + $toolResult2 = new ToolResult( + toolCallId: 'call_2', + toolName: 'get_temperature', + args: ['location' => 'LA'], + result: 80 + ); + + $toolResult3 = new ToolResult( + toolCallId: 'call_3', + toolName: 'get_humidity', + args: ['location' => 'SF'], + result: 'High' + ); + + $builder->addStep(new Step( + text: '', + finishReason: FinishReason::ToolCalls, + usage: new Usage(promptTokens: 10, completionTokens: 5), + meta: new Meta('step1', 'test-model'), + messages: [], + systemPrompts: [], + toolResults: [$toolResult1, $toolResult2], + )); + + $builder->addStep(new Step( + text: '{"result":"success"}', + finishReason: FinishReason::Stop, + usage: new Usage(promptTokens: 5, completionTokens: 3), + meta: new Meta('step2', 'test-model'), + messages: [], + systemPrompts: [], + toolResults: [$toolResult3], + )); + + $response = $builder->toResponse(); + + expect($response->toolResults)->toHaveCount(3) + ->and($response->toolResults[0])->toBeInstanceOf(ToolResult::class) + ->and($response->toolResults[0]->toolCallId)->toBe('call_1') + ->and($response->toolResults[0]->toolName)->toBe('get_weather') + ->and($response->toolResults[0]->result)->toBe(['temperature' => 75, 'condition' => 'sunny']) + ->and($response->toolResults[1])->toBeInstanceOf(ToolResult::class) + ->and($response->toolResults[1]->toolCallId)->toBe('call_2') + ->and($response->toolResults[1]->toolName)->toBe('get_temperature') + ->and($response->toolResults[1]->result)->toBe(80) + ->and($response->toolResults[2])->toBeInstanceOf(ToolResult::class) + ->and($response->toolResults[2]->toolCallId)->toBe('call_3') + ->and($response->toolResults[2]->toolName)->toBe('get_humidity') + ->and($response->toolResults[2]->result)->toBe('High'); +}); + +test('StructuredResponseBuilder returns empty arrays when no tool calls or results exist', function (): void { + $builder = new ResponseBuilder; + + $builder->addStep(new Step( + text: '{"value":42}', + finishReason: FinishReason::Stop, + usage: new Usage(promptTokens: 10, completionTokens: 5), + meta: new Meta('step1', 'test-model'), + messages: [], + systemPrompts: [], + )); + + $response = $builder->toResponse(); + + expect($response->toolCalls)->toBeArray() + ->and($response->toolCalls)->toBeEmpty() + ->and($response->toolResults)->toBeArray() + ->and($response->toolResults)->toBeEmpty(); +});