From 7c8c876cd8eb34f769b87f20015cc5370344489e Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Mon, 16 Feb 2026 00:53:20 +0100 Subject: [PATCH] Keep lazy iterator alive --- phpstan.dist.neon | 1 + src/Agent.php | 61 ++++++++++-------------- src/Toolbox/ToolFactory/ChainFactory.php | 11 ++--- src/Toolbox/Toolbox.php | 10 +--- tests/AgentTest.php | 42 ++++++++-------- 5 files changed, 48 insertions(+), 77 deletions(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index f6134c5..f1ff4d6 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -9,6 +9,7 @@ parameters: - tests/ excludePaths: - src/Bridge/ + treatPhpDocTypesAsCertain: false ignoreErrors: - message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#" diff --git a/src/Agent.php b/src/Agent.php index 7b505ef..db7b0e6 100644 --- a/src/Agent.php +++ b/src/Agent.php @@ -23,16 +23,6 @@ use Symfony\AI\Platform\Result\ResultInterface; */ final class Agent implements AgentInterface { - /** - * @var InputProcessorInterface[] - */ - private readonly array $inputProcessors; - - /** - * @var OutputProcessorInterface[] - */ - private readonly array $outputProcessors; - /** * @param InputProcessorInterface[] $inputProcessors * @param OutputProcessorInterface[] $outputProcessors @@ -41,12 +31,10 @@ final class Agent implements AgentInterface public function __construct( private readonly PlatformInterface $platform, private readonly string $model, - iterable $inputProcessors = [], - iterable $outputProcessors = [], + private readonly iterable $inputProcessors = [], + private readonly iterable $outputProcessors = [], private readonly string $name = 'agent', ) { - $this->inputProcessors = $this->initializeProcessors($inputProcessors, InputProcessorInterface::class); - $this->outputProcessors = $this->initializeProcessors($outputProcessors, OutputProcessorInterface::class); } public function getModel(): string @@ -69,7 +57,17 @@ final class Agent implements AgentInterface public function call(MessageBag $messages, array $options = []): ResultInterface { $input = new Input($this->getModel(), $messages, $options); - array_map(static fn (InputProcessorInterface $processor) => $processor->processInput($input), $this->inputProcessors); + foreach ($this->inputProcessors as $inputProcessor) { + if (!$inputProcessor instanceof InputProcessorInterface) { + throw new InvalidArgumentException(\sprintf('Input processor "%s" must implement "%s".', $inputProcessor::class, InputProcessorInterface::class)); + } + + if ($inputProcessor instanceof AgentAwareInterface) { + $inputProcessor->setAgent($this); + } + + $inputProcessor->processInput($input); + } $model = $input->getModel(); $messages = $input->getMessageBag(); @@ -78,29 +76,18 @@ final class Agent implements AgentInterface $result = $this->platform->invoke($model, $messages, $options)->getResult(); $output = new Output($model, $result, $messages, $options); - array_map(static fn (OutputProcessorInterface $processor) => $processor->processOutput($output), $this->outputProcessors); + foreach ($this->outputProcessors as $outputProcessor) { + if (!$outputProcessor instanceof OutputProcessorInterface) { + throw new InvalidArgumentException(\sprintf('Output processor "%s" must implement "%s".', $outputProcessor::class, OutputProcessorInterface::class)); + } + + if ($outputProcessor instanceof AgentAwareInterface) { + $outputProcessor->setAgent($this); + } + + $outputProcessor->processOutput($output); + } return $output->getResult(); } - - /** - * @param InputProcessorInterface[]|OutputProcessorInterface[] $processors - * @param class-string $interface - * - * @return InputProcessorInterface[]|OutputProcessorInterface[] - */ - private function initializeProcessors(iterable $processors, string $interface): array - { - foreach ($processors as $processor) { - if (!$processor instanceof $interface) { - throw new InvalidArgumentException(\sprintf('Processor "%s" must implement "%s".', $processor::class, $interface)); - } - - if ($processor instanceof AgentAwareInterface) { - $processor->setAgent($this); - } - } - - return $processors instanceof \Traversable ? iterator_to_array($processors) : $processors; - } } diff --git a/src/Toolbox/ToolFactory/ChainFactory.php b/src/Toolbox/ToolFactory/ChainFactory.php index 17d0d1c..ce2b40e 100644 --- a/src/Toolbox/ToolFactory/ChainFactory.php +++ b/src/Toolbox/ToolFactory/ChainFactory.php @@ -19,17 +19,12 @@ use Symfony\AI\Agent\Toolbox\ToolFactoryInterface; */ final class ChainFactory implements ToolFactoryInterface { - /** - * @var list - */ - private readonly array $factories; - /** * @param iterable $factories */ - public function __construct(iterable $factories) - { - $this->factories = $factories instanceof \Traversable ? iterator_to_array($factories) : $factories; + public function __construct( + private readonly iterable $factories, + ) { } public function getTool(string $reference): iterable diff --git a/src/Toolbox/Toolbox.php b/src/Toolbox/Toolbox.php index 89b34a1..1d12acc 100644 --- a/src/Toolbox/Toolbox.php +++ b/src/Toolbox/Toolbox.php @@ -31,13 +31,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; */ final class Toolbox implements ToolboxInterface { - /** - * List of executable tools. - * - * @var list - */ - private readonly array $tools; - /** * List of tool metadata objects. * @@ -49,13 +42,12 @@ final class Toolbox implements ToolboxInterface * @param iterable $tools */ public function __construct( - iterable $tools, + private readonly iterable $tools, private readonly ToolFactoryInterface $toolFactory = new ReflectionToolFactory(), private readonly ToolCallArgumentResolverInterface $argumentResolver = new ToolCallArgumentResolver(), private readonly LoggerInterface $logger = new NullLogger(), private readonly ?EventDispatcherInterface $eventDispatcher = null, ) { - $this->tools = $tools instanceof \Traversable ? iterator_to_array($tools) : $tools; } public function getTools(): array diff --git a/tests/AgentTest.php b/tests/AgentTest.php index 2b30d6e..e35b088 100644 --- a/tests/AgentTest.php +++ b/tests/AgentTest.php @@ -30,6 +30,7 @@ use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Platform\Result\DeferredResult; use Symfony\AI\Platform\Result\RawResultInterface; use Symfony\AI\Platform\Result\ResultInterface; +use Symfony\AI\Platform\Test\InMemoryPlatform; final class AgentTest extends TestCase { @@ -53,10 +54,17 @@ final class AgentTest extends TestCase $this->assertInstanceOf(AgentInterface::class, $agent); } - public function testConstructorSetsAgentOnAgentAwareProcessors() + public function testAgentExposesHisModel() { $platform = $this->createMock(PlatformInterface::class); + $agent = new Agent($platform, 'gpt-4o'); + + $this->assertSame('gpt-4o', $agent->getModel()); + } + + public function testSetsAgentOnAgentAwareProcessors() + { $agentAwareProcessor = new class implements InputProcessorInterface, AgentAwareInterface { public ?AgentInterface $agent = null; @@ -70,42 +78,30 @@ final class AgentTest extends TestCase } }; - $agent = new Agent($platform, 'gpt-4o', [$agentAwareProcessor]); + $agent = new Agent(new InMemoryPlatform('Hi'), 'gpt-4o', [$agentAwareProcessor]); + $agent->call(new MessageBag()); $this->assertSame($agent, $agentAwareProcessor->agent); } public function testConstructorThrowsExceptionForInvalidInputProcessor() { - $platform = $this->createMock(PlatformInterface::class); - $invalidProcessor = new \stdClass(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(\sprintf('Processor "stdClass" must implement "%s".', InputProcessorInterface::class)); + $this->expectExceptionMessage(\sprintf('Input processor "stdClass" must implement "%s".', InputProcessorInterface::class)); - /* @phpstan-ignore-next-line */ - new Agent($platform, 'gpt-4o', [$invalidProcessor]); + /** @phpstan-ignore-next-line argument.type */ + $agent = new Agent(new InMemoryPlatform('Hi'), 'gpt-4o', [new \stdClass()]); + $agent->call(new MessageBag()); } public function testConstructorThrowsExceptionForInvalidOutputProcessor() { - $platform = $this->createMock(PlatformInterface::class); - $invalidProcessor = new \stdClass(); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage(\sprintf('Processor "stdClass" must implement "%s".', OutputProcessorInterface::class)); + $this->expectExceptionMessage(\sprintf('Output processor "stdClass" must implement "%s".', OutputProcessorInterface::class)); - /* @phpstan-ignore-next-line */ - new Agent($platform, 'gpt-4o', [], [$invalidProcessor]); - } - - public function testAgentExposesHisModel() - { - $platform = $this->createMock(PlatformInterface::class); - - $agent = new Agent($platform, 'gpt-4o'); - - $this->assertSame('gpt-4o', $agent->getModel()); + /** @phpstan-ignore-next-line argument.type */ + $agent = new Agent(new InMemoryPlatform('Hi'), 'gpt-4o', [], [new \stdClass()]); + $agent->call(new MessageBag()); } public function testCallProcessesInputThroughProcessors()