Skip to content

Commit 64eef5e

Browse files
committed
bug #703 [Platform][Gemini] Do tool call if any contentPart is a functionCall (DrauzJu)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform][Gemini] Do tool call if any `contentPart` is a `functionCall` | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Docs? | no | Issues | | License | MIT The Gemini API may return multiple content parts even if a tool call is desired. For example, one content part can be an explanation why the tool was called. This PR adds support for this by first checking if any content part is a `functionCall`. If so, all other content parts are discarded and the function/tool call is processed. Commits ------- c3c36ab [Platform][Gemini] Do tool call if any `contentPart` is a `functionCall`
2 parents 7c6ed51 + c3c36ab commit 64eef5e

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed

examples/vertexai/toolcall.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
$toolbox = new Toolbox([new Clock()], logger: logger());
2525
$processor = new AgentProcessor($toolbox);
26-
$agent = new Agent($platform, 'gemini-2.0-flash-lite', [$processor], [$processor], logger: logger());
26+
$agent = new Agent($platform, 'gemini-2.5-flash-lite', [$processor], [$processor], logger: logger());
2727

2828
$messages = new MessageBag(Message::ofUser('What time is it?'));
2929
$result = $agent->call($messages);

src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,15 @@ private function convertChoice(array $choice): ToolCallResult|TextResult
142142
{
143143
$contentParts = $choice['content']['parts'];
144144

145-
if (1 === \count($contentParts)) {
146-
$contentPart = $contentParts[0];
147-
145+
// If any part is a function call, return it immediately and ignore all other parts.
146+
foreach ($contentParts as $contentPart) {
148147
if (isset($contentPart['functionCall'])) {
149148
return new ToolCallResult($this->convertToolCall($contentPart['functionCall']));
150149
}
150+
}
151+
152+
if (1 === \count($contentParts)) {
153+
$contentPart = $contentParts[0];
151154

152155
if (isset($contentPart['text'])) {
153156
return new TextResult($contentPart['text']);

src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ private function convertChoice(array $choice): ToolCallResult|TextResult
129129
{
130130
$contentParts = $choice['content']['parts'];
131131

132-
if (1 === \count($contentParts)) {
133-
$contentPart = $contentParts[0];
134-
132+
// If any part is a function call, return it immediately and ignore all other parts.
133+
foreach ($contentParts as $contentPart) {
135134
if (isset($contentPart['functionCall'])) {
136135
return new ToolCallResult($this->convertToolCall($contentPart['functionCall']));
137136
}
137+
}
138+
139+
if (1 === \count($contentParts)) {
140+
$contentPart = $contentParts[0];
138141

139142
if (isset($contentPart['text'])) {
140143
return new TextResult($contentPart['text']);

src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\AI\Platform\Bridge\Gemini\Gemini\ResultConverter;
1616
use Symfony\AI\Platform\Exception\RuntimeException;
1717
use Symfony\AI\Platform\Result\RawHttpResult;
18+
use Symfony\AI\Platform\Result\ToolCall;
19+
use Symfony\AI\Platform\Result\ToolCallResult;
1820
use Symfony\Contracts\HttpClient\ResponseInterface;
1921

2022
/**
@@ -40,4 +42,38 @@ public function testConvertThrowsExceptionWithDetailedErrorInformation()
4042

4143
$converter->convert(new RawHttpResult($httpResponse));
4244
}
45+
46+
public function testReturnsToolCallEvenIfMultipleContentPartsAreGiven()
47+
{
48+
$converter = new ResultConverter();
49+
$httpResponse = self::createMock(ResponseInterface::class);
50+
$httpResponse->method('getStatusCode')->willReturn(200);
51+
$httpResponse->method('toArray')->willReturn([
52+
'candidates' => [
53+
[
54+
'content' => [
55+
'parts' => [
56+
[
57+
'text' => 'foo',
58+
],
59+
[
60+
'functionCall' => [
61+
'id' => '1234',
62+
'name' => 'some_tool',
63+
'args' => [],
64+
],
65+
],
66+
],
67+
],
68+
],
69+
],
70+
]);
71+
72+
$result = $converter->convert(new RawHttpResult($httpResponse));
73+
$this->assertInstanceOf(ToolCallResult::class, $result);
74+
$this->assertCount(1, $result->getContent());
75+
$toolCall = $result->getContent()[0];
76+
$this->assertInstanceOf(ToolCall::class, $toolCall);
77+
$this->assertSame('1234', $toolCall->id);
78+
}
4379
}

src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\AI\Platform\Bridge\VertexAi\Gemini\ResultConverter;
1616
use Symfony\AI\Platform\Result\RawHttpResult;
1717
use Symfony\AI\Platform\Result\TextResult;
18+
use Symfony\AI\Platform\Result\ToolCall;
19+
use Symfony\AI\Platform\Result\ToolCallResult;
1820
use Symfony\Contracts\HttpClient\ResponseInterface;
1921

2022
final class ResultConverterTest extends TestCase
@@ -57,6 +59,42 @@ public function testItReturnsAggregatedTextOnSuccess()
5759
$this->assertEquals("Second text\nThird text\nFourth text", $result->getContent());
5860
}
5961

62+
public function testItReturnsToolCallEvenIfMultipleContentPartsAreGiven()
63+
{
64+
$payload = [
65+
'content' => [
66+
'parts' => [
67+
[
68+
'text' => 'foo',
69+
],
70+
[
71+
'functionCall' => [
72+
'name' => 'some_tool',
73+
'args' => [],
74+
],
75+
],
76+
],
77+
],
78+
];
79+
$expectedResponse = [
80+
'candidates' => [$payload],
81+
];
82+
$response = $this->createStub(ResponseInterface::class);
83+
$response
84+
->method('toArray')
85+
->willReturn($expectedResponse);
86+
87+
$resultConverter = new ResultConverter();
88+
89+
$result = $resultConverter->convert(new RawHttpResult($response));
90+
91+
$this->assertInstanceOf(ToolCallResult::class, $result);
92+
$this->assertCount(1, $result->getContent());
93+
$toolCall = $result->getContent()[0];
94+
$this->assertInstanceOf(ToolCall::class, $toolCall);
95+
$this->assertSame('some_tool', $toolCall->id);
96+
}
97+
6098
public function testItThrowsExceptionOnFailure()
6199
{
62100
$response = $this->createStub(ResponseInterface::class);

0 commit comments

Comments
 (0)