Skip to content

Commit 7393bce

Browse files
committed
bug #466 [Platform][VertexAI] Update ResultConverter (pentiminax)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform][VertexAI] Update `ResultConverter` | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Docs? | no | License | MIT Same as #421 but for VertexAI bridge This PR fixes a crash in the Gemini ResultConverter when handling responses that only contained executableCode and codeExecutionResult parts. Rather than duplicating the code, I created a trait that is used in both bridges. I also reused the same test fixtures. Commits ------- a46ddcf [Platform][VertexAI] Update `ResultConverter`
2 parents 0bd8d96 + a46ddcf commit 7393bce

File tree

5 files changed

+200
-9
lines changed

5 files changed

+200
-9
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"candidates": [
3+
{
4+
"content": {
5+
"parts": [
6+
{
7+
"text": "First text"
8+
},
9+
{
10+
"executableCode": {
11+
"language": "PYTHON",
12+
"code": "print('Hello, World!')"
13+
}
14+
},
15+
{
16+
"codeExecutionResult": {
17+
"outcome": "OUTCOME_DEADLINE_EXCEEDED",
18+
"output": "An error occurred during code execution."
19+
}
20+
},
21+
{
22+
"text": "Last text"
23+
}
24+
],
25+
"role": "model"
26+
},
27+
"finishReason": "STOP",
28+
"index": 0
29+
}
30+
]
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"candidates": [
3+
{
4+
"content": {
5+
"parts": [
6+
{
7+
"text": "First text"
8+
},
9+
{
10+
"executableCode": {
11+
"language": "PYTHON",
12+
"code": "print('Hello, World!')"
13+
}
14+
},
15+
{
16+
"codeExecutionResult": {
17+
"outcome": "OUTCOME_FAILED",
18+
"output": "An error occurred during code execution."
19+
}
20+
},
21+
{
22+
"text": "Last text"
23+
}
24+
],
25+
"role": "model"
26+
},
27+
"finishReason": "STOP",
28+
"index": 0
29+
}
30+
]
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"candidates": [
3+
{
4+
"content": {
5+
"parts": [
6+
{
7+
"text": "First text"
8+
},
9+
{
10+
"executableCode": {
11+
"language": "PYTHON",
12+
"code": "print('Hello, World!')"
13+
}
14+
},
15+
{
16+
"codeExecutionResult": {
17+
"outcome": "OUTCOME_OK",
18+
"output": "Hello, World!"
19+
}
20+
},
21+
{
22+
"text": "Second text\n"
23+
},
24+
{
25+
"text": "Third text\n"
26+
},
27+
{
28+
"text": "Fourth text"
29+
}
30+
],
31+
"role": "model"
32+
},
33+
"finishReason": "STOP",
34+
"index": 0
35+
}
36+
]
37+
}

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

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
*/
3232
final readonly class ResultConverter implements ResultConverterInterface
3333
{
34+
public const OUTCOME_OK = 'OUTCOME_OK';
35+
public const OUTCOME_FAILED = 'OUTCOME_FAILED';
36+
public const OUTCOME_DEADLINE_EXCEEDED = 'OUTCOME_DEADLINE_EXCEEDED';
37+
3438
public function supports(BaseModel $model): bool
3539
{
3640
return $model instanceof Model;
@@ -119,21 +123,44 @@ private function convertStream(HttpResponse $result): \Generator
119123
* text?: string
120124
* }[]
121125
* }
122-
* } $choices
126+
* } $choice
123127
*/
124-
private function convertChoice(array $choices): ToolCallResult|TextResult
128+
private function convertChoice(array $choice): ToolCallResult|TextResult
125129
{
126-
$content = $choices['content']['parts'][0] ?? [];
130+
$contentParts = $choice['content']['parts'];
131+
132+
if (1 === \count($contentParts)) {
133+
$contentPart = $contentParts[0];
134+
135+
if (isset($contentPart['functionCall'])) {
136+
return new ToolCallResult($this->convertToolCall($contentPart['functionCall']));
137+
}
138+
139+
if (isset($contentPart['text'])) {
140+
return new TextResult($contentPart['text']);
141+
}
142+
143+
throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finishReason']));
144+
}
145+
146+
$content = '';
147+
$successfulCodeExecutionDetected = false;
148+
foreach ($contentParts as $contentPart) {
149+
if ($this->isSuccessfulCodeExecution($contentPart)) {
150+
$successfulCodeExecutionDetected = true;
151+
continue;
152+
}
127153

128-
if (isset($content['functionCall'])) {
129-
return new ToolCallResult($this->convertToolCall($content['functionCall']));
154+
if ($successfulCodeExecutionDetected) {
155+
$content .= $contentPart['text'];
156+
}
130157
}
131158

132-
if (isset($content['text'])) {
133-
return new TextResult($content['text']);
159+
if ('' !== $content) {
160+
return new TextResult($content);
134161
}
135162

136-
throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choices['finishReason']));
163+
throw new RuntimeException('Code execution failed.');
137164
}
138165

139166
/**
@@ -146,4 +173,23 @@ private function convertToolCall(array $toolCall): ToolCall
146173
{
147174
return new ToolCall($toolCall['name'], $toolCall['name'], $toolCall['args']);
148175
}
176+
177+
/**
178+
* @param array{
179+
* codeExecutionResult?: array{
180+
* outcome: self::OUTCOME_*,
181+
* output: string
182+
* }
183+
* } $contentPart
184+
*/
185+
private function isSuccessfulCodeExecution(array $contentPart): bool
186+
{
187+
if (!isset($contentPart['codeExecutionResult'])) {
188+
return false;
189+
}
190+
191+
$result = $contentPart['codeExecutionResult'];
192+
193+
return self::OUTCOME_OK === $result['outcome'];
194+
}
149195
}

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,54 @@ public function testItConvertsAResponseToAVectorResult()
4646
$result = $resultConverter->convert(new RawHttpResult($response));
4747

4848
// Assert
49-
5049
$this->assertInstanceOf(TextResult::class, $result);
5150
$this->assertSame('Hello, world!', $result->getContent());
5251
}
52+
53+
public function testItReturnsAggregatedTextOnSuccess()
54+
{
55+
$response = $this->createStub(ResponseInterface::class);
56+
$responseContent = file_get_contents(\dirname(__DIR__, 6).'/fixtures/Bridge/VertexAi/code_execution_outcome_ok.json');
57+
58+
$response
59+
->method('toArray')
60+
->willReturn(json_decode($responseContent, true));
61+
62+
$converter = new ResultConverter();
63+
64+
$result = $converter->convert(new RawHttpResult($response));
65+
$this->assertInstanceOf(TextResult::class, $result);
66+
67+
$this->assertEquals("Second text\nThird text\nFourth text", $result->getContent());
68+
}
69+
70+
public function testItThrowsExceptionOnFailure()
71+
{
72+
$response = $this->createStub(ResponseInterface::class);
73+
$responseContent = file_get_contents(\dirname(__DIR__, 6).'/fixtures/Bridge/VertexAi/code_execution_outcome_failed.json');
74+
75+
$response
76+
->method('toArray')
77+
->willReturn(json_decode($responseContent, true));
78+
79+
$converter = new ResultConverter();
80+
81+
$this->expectException(\RuntimeException::class);
82+
$converter->convert(new RawHttpResult($response));
83+
}
84+
85+
public function testItThrowsExceptionOnTimeout()
86+
{
87+
$response = $this->createStub(ResponseInterface::class);
88+
$responseContent = file_get_contents(\dirname(__DIR__, 6).'/fixtures/Bridge/VertexAi/code_execution_outcome_deadline_exceeded.json');
89+
90+
$response
91+
->method('toArray')
92+
->willReturn(json_decode($responseContent, true));
93+
94+
$converter = new ResultConverter();
95+
96+
$this->expectException(\RuntimeException::class);
97+
$converter->convert(new RawHttpResult($response));
98+
}
5399
}

0 commit comments

Comments
 (0)