diff --git a/src/agent/tests/StructuredOutput/AgentProcessorTest.php b/src/agent/tests/StructuredOutput/AgentProcessorTest.php index 802497d7..8c1a380b 100644 --- a/src/agent/tests/StructuredOutput/AgentProcessorTest.php +++ b/src/agent/tests/StructuredOutput/AgentProcessorTest.php @@ -24,7 +24,6 @@ use Symfony\AI\Platform\Capability; use Symfony\AI\Platform\Message\MessageBag; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\Metadata\Metadata; use Symfony\AI\Platform\Result\ObjectResult; use Symfony\AI\Platform\Result\TextResult; @@ -34,7 +33,6 @@ #[UsesClass(Input::class)] #[UsesClass(Output::class)] #[UsesClass(MessageBag::class)] -#[UsesClass(Choice::class)] #[UsesClass(MissingModelSupportException::class)] #[UsesClass(TextResult::class)] #[UsesClass(ObjectResult::class)] diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index 27f0d05b..da8fe5c2 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -14,7 +14,6 @@ use Symfony\AI\Platform\Bridge\Gemini\Gemini; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -49,18 +48,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options throw new RuntimeException('Response does not contain any content.'); } - /** @var Choice[] $choices */ $choices = array_map($this->convertChoice(...), $data['candidates']); - if (1 !== \count($choices)) { - return new ChoiceResult(...$choices); - } - - if ($choices[0]->hasToolCall()) { - return new ToolCallResult(...$choices[0]->getToolCalls()); - } - - return new TextResult($choices[0]->getContent()); + return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } private function convertStream(HttpResponse $result): \Generator @@ -94,7 +84,6 @@ private function convertStream(HttpResponse $result): \Generator throw new RuntimeException('Failed to decode JSON response.', 0, $e); } - /** @var Choice[] $choices */ $choices = array_map($this->convertChoice(...), $data['candidates'] ?? []); if (!$choices) { @@ -106,13 +95,7 @@ private function convertStream(HttpResponse $result): \Generator continue; } - if ($choices[0]->hasToolCall()) { - yield new ToolCallResult(...$choices[0]->getToolCalls()); - } - - if ($choices[0]->hasContent()) { - yield $choices[0]->getContent(); - } + yield $choices[0]->getContent(); } } } @@ -132,16 +115,16 @@ private function convertStream(HttpResponse $result): \Generator * } * } $choice */ - private function convertChoice(array $choice): Choice + private function convertChoice(array $choice): ToolCallResult|TextResult { $contentPart = $choice['content']['parts'][0] ?? []; if (isset($contentPart['functionCall'])) { - return new Choice(toolCalls: [$this->convertToolCall($contentPart['functionCall'])]); + return new ToolCallResult($this->convertToolCall($contentPart['functionCall'])); } if (isset($contentPart['text'])) { - return new Choice($contentPart['text']); + return new TextResult($contentPart['text']); } throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finishReason'])); diff --git a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php index 6c4d93ec..a6b84026 100644 --- a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php +++ b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php @@ -14,7 +14,6 @@ use Symfony\AI\Platform\Bridge\Mistral\Mistral; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -60,18 +59,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options throw new RuntimeException('Response does not contain choices.'); } - /** @var Choice[] $choices */ $choices = array_map($this->convertChoice(...), $data['choices']); - if (1 !== \count($choices)) { - return new ChoiceResult(...$choices); - } - - if ($choices[0]->hasToolCall()) { - return new ToolCallResult(...$choices[0]->getToolCalls()); - } - - return new TextResult($choices[0]->getContent()); + return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } private function convertStream(HttpResponse $result): \Generator @@ -170,14 +160,14 @@ private function isToolCallsStreamFinished(array $data): bool * finish_reason: 'stop'|'length'|'tool_calls'|'content_filter', * } $choice */ - private function convertChoice(array $choice): Choice + private function convertChoice(array $choice): ToolCallResult|TextResult { if ('tool_calls' === $choice['finish_reason']) { - return new Choice(toolCalls: array_map([$this, 'convertToolCall'], $choice['message']['tool_calls'])); + return new ToolCallResult(...array_map([$this, 'convertToolCall'], $choice['message']['tool_calls'])); } if ('stop' === $choice['finish_reason']) { - return new Choice($choice['message']['content']); + return new TextResult($choice['message']['content']); } throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finish_reason'])); diff --git a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php index c07fd699..72714e81 100644 --- a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php @@ -15,7 +15,6 @@ use Symfony\AI\Platform\Exception\ContentFilterException; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -57,18 +56,9 @@ public function convert(RawResultInterface|RawHttpResult $result, array $options throw new RuntimeException('Response does not contain choices.'); } - /** @var Choice[] $choices */ $choices = array_map($this->convertChoice(...), $data['choices']); - if (1 !== \count($choices)) { - return new ChoiceResult(...$choices); - } - - if ($choices[0]->hasToolCall()) { - return new ToolCallResult(...$choices[0]->getToolCalls()); - } - - return new TextResult($choices[0]->getContent()); + return 1 === \count($choices) ? $choices[0] : new ChoiceResult(...$choices); } private function convertStream(HttpResponse $result): \Generator @@ -167,14 +157,14 @@ private function isToolCallsStreamFinished(array $data): bool * finish_reason: 'stop'|'length'|'tool_calls'|'content_filter', * } $choice */ - private function convertChoice(array $choice): Choice + private function convertChoice(array $choice): ToolCallResult|TextResult { if ('tool_calls' === $choice['finish_reason']) { - return new Choice(toolCalls: array_map([$this, 'convertToolCall'], $choice['message']['tool_calls'])); + return new ToolCallResult(...array_map([$this, 'convertToolCall'], $choice['message']['tool_calls'])); } if (\in_array($choice['finish_reason'], ['stop', 'length'], true)) { - return new Choice($choice['message']['content']); + return new TextResult($choice['message']['content']); } throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finish_reason'])); diff --git a/src/platform/src/Result/Choice.php b/src/platform/src/Result/Choice.php deleted file mode 100644 index 5016b2c2..00000000 --- a/src/platform/src/Result/Choice.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Result; - -/** - * @author Christopher Hertel - */ -final readonly class Choice -{ - /** - * @param ToolCall[] $toolCalls - */ - public function __construct( - private ?string $content = null, - private array $toolCalls = [], - ) { - } - - public function getContent(): ?string - { - return $this->content; - } - - public function hasContent(): bool - { - return null !== $this->content; - } - - /** - * @return ToolCall[] - */ - public function getToolCalls(): array - { - return $this->toolCalls; - } - - public function hasToolCall(): bool - { - return [] !== $this->toolCalls; - } -} diff --git a/src/platform/src/Result/ChoiceResult.php b/src/platform/src/Result/ChoiceResult.php index ac2f5404..551d408c 100644 --- a/src/platform/src/Result/ChoiceResult.php +++ b/src/platform/src/Result/ChoiceResult.php @@ -19,24 +19,24 @@ final class ChoiceResult extends BaseResult { /** - * @var Choice[] + * @var ResultInterface[] */ - private readonly array $choices; + private readonly array $results; - public function __construct(Choice ...$choices) + public function __construct(ResultInterface ...$results) { - if ([] === $choices) { - throw new InvalidArgumentException('Result must have at least one choice.'); + if (1 >= \count($results)) { + throw new InvalidArgumentException('A choice result must contain at least two results.'); } - $this->choices = $choices; + $this->results = $results; } /** - * @return Choice[] + * @return ResultInterface[] */ public function getContent(): array { - return $this->choices; + return $this->results; } } diff --git a/src/platform/tests/Bridge/OpenAi/Gpt/ResultConverterTest.php b/src/platform/tests/Bridge/OpenAi/Gpt/ResultConverterTest.php index 2232ac67..8f0a6f2f 100644 --- a/src/platform/tests/Bridge/OpenAi/Gpt/ResultConverterTest.php +++ b/src/platform/tests/Bridge/OpenAi/Gpt/ResultConverterTest.php @@ -18,7 +18,6 @@ use Symfony\AI\Platform\Bridge\OpenAi\Gpt\ResultConverter; use Symfony\AI\Platform\Exception\ContentFilterException; use Symfony\AI\Platform\Exception\RuntimeException; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\TextResult; @@ -29,7 +28,6 @@ #[CoversClass(ResultConverter::class)] #[Small] -#[UsesClass(Choice::class)] #[UsesClass(ChoiceResult::class)] #[UsesClass(TextResult::class)] #[UsesClass(ToolCall::class)] diff --git a/src/platform/tests/Result/ChoiceResultTest.php b/src/platform/tests/Result/ChoiceResultTest.php index 495f7c27..c683468f 100644 --- a/src/platform/tests/Result/ChoiceResultTest.php +++ b/src/platform/tests/Result/ChoiceResultTest.php @@ -13,34 +13,30 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Exception\InvalidArgumentException; -use Symfony\AI\Platform\Result\Choice; use Symfony\AI\Platform\Result\ChoiceResult; +use Symfony\AI\Platform\Result\TextResult; #[CoversClass(ChoiceResult::class)] -#[UsesClass(Choice::class)] #[Small] final class ChoiceResultTest extends TestCase { public function testChoiceResultCreation() { - $choice1 = new Choice('choice1'); - $choice2 = new Choice(null); - $choice3 = new Choice('choice3'); - $result = new ChoiceResult($choice1, $choice2, $choice3); + $choice1 = new TextResult('choice1'); + $choice3 = new TextResult('choice2'); + $result = new ChoiceResult($choice1, $choice3); - $this->assertCount(3, $result->getContent()); + $this->assertCount(2, $result->getContent()); $this->assertSame('choice1', $result->getContent()[0]->getContent()); - $this->assertNull($result->getContent()[1]->getContent()); - $this->assertSame('choice3', $result->getContent()[2]->getContent()); + $this->assertSame('choice2', $result->getContent()[1]->getContent()); } public function testChoiceResultWithNoChoices() { self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('Result must have at least one choice.'); + self::expectExceptionMessage('A choice result must contain at least two results.'); new ChoiceResult(); } diff --git a/src/platform/tests/Result/ChoiceTest.php b/src/platform/tests/Result/ChoiceTest.php deleted file mode 100644 index 4bdabf86..00000000 --- a/src/platform/tests/Result/ChoiceTest.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\Platform\Tests\Result; - -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Small; -use PHPUnit\Framework\Attributes\UsesClass; -use PHPUnit\Framework\TestCase; -use Symfony\AI\Platform\Result\Choice; -use Symfony\AI\Platform\Result\ToolCall; - -#[CoversClass(Choice::class)] -#[UsesClass(ToolCall::class)] -#[Small] -final class ChoiceTest extends TestCase -{ - public function testChoiceEmpty() - { - $choice = new Choice(); - $this->assertFalse($choice->hasContent()); - $this->assertNull($choice->getContent()); - $this->assertFalse($choice->hasToolCall()); - $this->assertCount(0, $choice->getToolCalls()); - } - - public function testChoiceWithContent() - { - $choice = new Choice('content'); - $this->assertTrue($choice->hasContent()); - $this->assertSame('content', $choice->getContent()); - $this->assertFalse($choice->hasToolCall()); - $this->assertCount(0, $choice->getToolCalls()); - } - - public function testChoiceWithToolCall() - { - $choice = new Choice(null, [new ToolCall('name', 'arguments')]); - $this->assertFalse($choice->hasContent()); - $this->assertNull($choice->getContent()); - $this->assertTrue($choice->hasToolCall()); - $this->assertCount(1, $choice->getToolCalls()); - } - - public function testChoiceWithContentAndToolCall() - { - $choice = new Choice('content', [new ToolCall('name', 'arguments')]); - $this->assertTrue($choice->hasContent()); - $this->assertSame('content', $choice->getContent()); - $this->assertTrue($choice->hasToolCall()); - $this->assertCount(1, $choice->getToolCalls()); - } -}