|
18 | 18 | use Symfony\AI\Platform\Exception\ContentFilterException; |
19 | 19 | use Symfony\AI\Platform\Exception\RuntimeException; |
20 | 20 | use Symfony\AI\Platform\Result\ChoiceResult; |
| 21 | +use Symfony\AI\Platform\Result\InMemoryRawResult; |
21 | 22 | use Symfony\AI\Platform\Result\RawHttpResult; |
| 23 | +use Symfony\AI\Platform\Result\Stream\Delta\TextDelta; |
| 24 | +use Symfony\AI\Platform\Result\Stream\Delta\ThinkingComplete; |
| 25 | +use Symfony\AI\Platform\Result\Stream\Delta\ThinkingDelta; |
| 26 | +use Symfony\AI\Platform\Result\Stream\Delta\ToolCallComplete; |
| 27 | +use Symfony\AI\Platform\Result\Stream\Delta\ToolCallStart; |
| 28 | +use Symfony\AI\Platform\Result\Stream\Delta\ToolInputDelta; |
| 29 | +use Symfony\AI\Platform\Result\StreamResult; |
22 | 30 | use Symfony\AI\Platform\Result\TextResult; |
23 | 31 | use Symfony\AI\Platform\Result\ToolCallResult; |
24 | 32 | use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; |
@@ -316,4 +324,86 @@ public function testThrowsDetailedErrorException() |
316 | 324 |
|
317 | 325 | $converter->convert(new RawHttpResult($httpResponse)); |
318 | 326 | } |
| 327 | + |
| 328 | + public function testStreamingInterleavedReasoningContentAndToolCalls() |
| 329 | + { |
| 330 | + $converter = new ResultConverter(); |
| 331 | + |
| 332 | + $events = [ |
| 333 | + ['choices' => [['index' => 0, 'delta' => ['reasoning_content' => 'I need to check the weather']]]], |
| 334 | + ['choices' => [['index' => 0, 'delta' => ['reasoning_content' => 'Let me call the tool']]]], |
| 335 | + ['choices' => [['index' => 0, 'delta' => ['content' => 'Let me check']]]], |
| 336 | + ['choices' => [['index' => 0, 'delta' => [ |
| 337 | + 'tool_calls' => [ |
| 338 | + [ |
| 339 | + 'id' => 'call_1', |
| 340 | + 'type' => 'function', |
| 341 | + 'function' => [ |
| 342 | + 'name' => 'get_weather', |
| 343 | + 'arguments' => '', |
| 344 | + ], |
| 345 | + ], |
| 346 | + ], |
| 347 | + ]]]], |
| 348 | + ['choices' => [['index' => 0, 'delta' => [ |
| 349 | + 'tool_calls' => [ |
| 350 | + [ |
| 351 | + 'function' => [ |
| 352 | + 'arguments' => '{"city":"Beijing"}', |
| 353 | + ], |
| 354 | + ], |
| 355 | + ], |
| 356 | + ]]]], |
| 357 | + ['choices' => [['index' => 0, 'delta' => [], 'finish_reason' => 'tool_calls']]], |
| 358 | + ]; |
| 359 | + |
| 360 | + $httpLike = new class { |
| 361 | + public function getStatusCode(): int |
| 362 | + { |
| 363 | + return 200; |
| 364 | + } |
| 365 | + }; |
| 366 | + |
| 367 | + $raw = new InMemoryRawResult([], $events, $httpLike); |
| 368 | + $streamResult = $converter->convert($raw, ['stream' => true]); |
| 369 | + |
| 370 | + $this->assertInstanceOf(StreamResult::class, $streamResult); |
| 371 | + |
| 372 | + $chunks = []; |
| 373 | + foreach ($streamResult->getContent() as $part) { |
| 374 | + $chunks[] = $part; |
| 375 | + } |
| 376 | + |
| 377 | + $thinkingDeltas = array_values(array_filter($chunks, static fn ($c) => $c instanceof ThinkingDelta)); |
| 378 | + $this->assertCount(2, $thinkingDeltas); |
| 379 | + $this->assertSame('I need to check the weather', $thinkingDeltas[0]->getThinking()); |
| 380 | + $this->assertSame('Let me call the tool', $thinkingDeltas[1]->getThinking()); |
| 381 | + |
| 382 | + $thinkingCompletes = array_values(array_filter($chunks, static fn ($c) => $c instanceof ThinkingComplete)); |
| 383 | + $this->assertCount(1, $thinkingCompletes); |
| 384 | + $this->assertSame('I need to check the weatherLet me call the tool', $thinkingCompletes[0]->getThinking()); |
| 385 | + |
| 386 | + $textDeltas = array_values(array_filter($chunks, static fn ($c) => $c instanceof TextDelta)); |
| 387 | + $this->assertCount(1, $textDeltas); |
| 388 | + $this->assertSame('Let me check', $textDeltas[0]->getText()); |
| 389 | + |
| 390 | + $toolCallStarts = array_values(array_filter($chunks, static fn ($c) => $c instanceof ToolCallStart)); |
| 391 | + $this->assertCount(1, $toolCallStarts); |
| 392 | + $this->assertSame('call_1', $toolCallStarts[0]->getId()); |
| 393 | + $this->assertSame('get_weather', $toolCallStarts[0]->getName()); |
| 394 | + |
| 395 | + $toolInputDeltas = array_values(array_filter($chunks, static fn ($c) => $c instanceof ToolInputDelta)); |
| 396 | + $this->assertCount(1, $toolInputDeltas); |
| 397 | + $this->assertSame('call_1', $toolInputDeltas[0]->getId()); |
| 398 | + $this->assertSame('get_weather', $toolInputDeltas[0]->getName()); |
| 399 | + $this->assertSame('{"city":"Beijing"}', $toolInputDeltas[0]->getPartialJson()); |
| 400 | + |
| 401 | + $toolCallCompletes = array_values(array_filter($chunks, static fn ($c) => $c instanceof ToolCallComplete)); |
| 402 | + $this->assertCount(1, $toolCallCompletes); |
| 403 | + $completed = $toolCallCompletes[0]->getToolCalls(); |
| 404 | + $this->assertCount(1, $completed); |
| 405 | + $this->assertSame('call_1', $completed[0]->getId()); |
| 406 | + $this->assertSame('get_weather', $completed[0]->getName()); |
| 407 | + $this->assertSame(['city' => 'Beijing'], $completed[0]->getArguments()); |
| 408 | + } |
319 | 409 | } |
0 commit comments