Keep lazy iterator alive

This commit is contained in:
Christopher Hertel
2026-02-16 00:53:20 +01:00
parent efc0c87364
commit 7c8c876cd8
5 changed files with 48 additions and 77 deletions

View File

@@ -9,6 +9,7 @@ parameters:
- tests/
excludePaths:
- src/Bridge/
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#"

View File

@@ -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;
}
}

View File

@@ -19,17 +19,12 @@ use Symfony\AI\Agent\Toolbox\ToolFactoryInterface;
*/
final class ChainFactory implements ToolFactoryInterface
{
/**
* @var list<ToolFactoryInterface>
*/
private readonly array $factories;
/**
* @param iterable<ToolFactoryInterface> $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

View File

@@ -31,13 +31,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
*/
final class Toolbox implements ToolboxInterface
{
/**
* List of executable tools.
*
* @var list<object>
*/
private readonly array $tools;
/**
* List of tool metadata objects.
*
@@ -49,13 +42,12 @@ final class Toolbox implements ToolboxInterface
* @param iterable<object> $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

View File

@@ -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()