From c3c36abe21285e1ab7b547cb26a733a26c5a6a7b Mon Sep 17 00:00:00 2001 From: Julian Drauz Date: Tue, 30 Sep 2025 00:11:17 +0200 Subject: [PATCH] [Platform][Gemini] Do tool call if any `contentPart` is a `functionCall` --- examples/vertexai/toolcall.php | 2 +- .../Bridge/Gemini/Gemini/ResultConverter.php | 9 +++-- .../VertexAi/Gemini/ResultConverter.php | 9 +++-- .../Gemini/Gemini/ResultConverterTest.php | 36 ++++++++++++++++++ .../VertexAi/Gemini/ResultConverterTest.php | 38 +++++++++++++++++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/examples/vertexai/toolcall.php b/examples/vertexai/toolcall.php index 1473bf6af..3d6201afa 100644 --- a/examples/vertexai/toolcall.php +++ b/examples/vertexai/toolcall.php @@ -23,7 +23,7 @@ $toolbox = new Toolbox([new Clock()], logger: logger()); $processor = new AgentProcessor($toolbox); -$agent = new Agent($platform, 'gemini-2.0-flash-lite', [$processor], [$processor], logger: logger()); +$agent = new Agent($platform, 'gemini-2.5-flash-lite', [$processor], [$processor], logger: logger()); $messages = new MessageBag(Message::ofUser('What time is it?')); $result = $agent->call($messages); diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index adc4bf8c3..b71249e69 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -142,12 +142,15 @@ private function convertChoice(array $choice): ToolCallResult|TextResult { $contentParts = $choice['content']['parts']; - if (1 === \count($contentParts)) { - $contentPart = $contentParts[0]; - + // If any part is a function call, return it immediately and ignore all other parts. + foreach ($contentParts as $contentPart) { if (isset($contentPart['functionCall'])) { return new ToolCallResult($this->convertToolCall($contentPart['functionCall'])); } + } + + if (1 === \count($contentParts)) { + $contentPart = $contentParts[0]; if (isset($contentPart['text'])) { return new TextResult($contentPart['text']); diff --git a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php index b344c576d..161d70235 100644 --- a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php @@ -129,12 +129,15 @@ private function convertChoice(array $choice): ToolCallResult|TextResult { $contentParts = $choice['content']['parts']; - if (1 === \count($contentParts)) { - $contentPart = $contentParts[0]; - + // If any part is a function call, return it immediately and ignore all other parts. + foreach ($contentParts as $contentPart) { if (isset($contentPart['functionCall'])) { return new ToolCallResult($this->convertToolCall($contentPart['functionCall'])); } + } + + if (1 === \count($contentParts)) { + $contentPart = $contentParts[0]; if (isset($contentPart['text'])) { return new TextResult($contentPart['text']); diff --git a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php index 852c20f97..a2726a331 100644 --- a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php +++ b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php @@ -15,6 +15,8 @@ use Symfony\AI\Platform\Bridge\Gemini\Gemini\ResultConverter; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Result\RawHttpResult; +use Symfony\AI\Platform\Result\ToolCall; +use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\Contracts\HttpClient\ResponseInterface; /** @@ -40,4 +42,38 @@ public function testConvertThrowsExceptionWithDetailedErrorInformation() $converter->convert(new RawHttpResult($httpResponse)); } + + public function testReturnsToolCallEvenIfMultipleContentPartsAreGiven() + { + $converter = new ResultConverter(); + $httpResponse = self::createMock(ResponseInterface::class); + $httpResponse->method('getStatusCode')->willReturn(200); + $httpResponse->method('toArray')->willReturn([ + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ + 'text' => 'foo', + ], + [ + 'functionCall' => [ + 'id' => '1234', + 'name' => 'some_tool', + 'args' => [], + ], + ], + ], + ], + ], + ], + ]); + + $result = $converter->convert(new RawHttpResult($httpResponse)); + $this->assertInstanceOf(ToolCallResult::class, $result); + $this->assertCount(1, $result->getContent()); + $toolCall = $result->getContent()[0]; + $this->assertInstanceOf(ToolCall::class, $toolCall); + $this->assertSame('1234', $toolCall->id); + } } diff --git a/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php b/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php index ea655d501..b93aad8a2 100644 --- a/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php +++ b/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php @@ -15,6 +15,8 @@ use Symfony\AI\Platform\Bridge\VertexAi\Gemini\ResultConverter; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\TextResult; +use Symfony\AI\Platform\Result\ToolCall; +use Symfony\AI\Platform\Result\ToolCallResult; use Symfony\Contracts\HttpClient\ResponseInterface; final class ResultConverterTest extends TestCase @@ -57,6 +59,42 @@ public function testItReturnsAggregatedTextOnSuccess() $this->assertEquals("Second text\nThird text\nFourth text", $result->getContent()); } + public function testItReturnsToolCallEvenIfMultipleContentPartsAreGiven() + { + $payload = [ + 'content' => [ + 'parts' => [ + [ + 'text' => 'foo', + ], + [ + 'functionCall' => [ + 'name' => 'some_tool', + 'args' => [], + ], + ], + ], + ], + ]; + $expectedResponse = [ + 'candidates' => [$payload], + ]; + $response = $this->createStub(ResponseInterface::class); + $response + ->method('toArray') + ->willReturn($expectedResponse); + + $resultConverter = new ResultConverter(); + + $result = $resultConverter->convert(new RawHttpResult($response)); + + $this->assertInstanceOf(ToolCallResult::class, $result); + $this->assertCount(1, $result->getContent()); + $toolCall = $result->getContent()[0]; + $this->assertInstanceOf(ToolCall::class, $toolCall); + $this->assertSame('some_tool', $toolCall->id); + } + public function testItThrowsExceptionOnFailure() { $response = $this->createStub(ResponseInterface::class);