Skip to content

Commit c3c36ab

Browse files
DrauzJuOskarStark
authored andcommitted
[Platform][Gemini] Do tool call if any contentPart is a functionCall
1 parent b50bce3 commit c3c36ab

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)