|
18 | 18 | use PHPUnit\Framework\Attributes\TestWith; |
19 | 19 | use PHPUnit\Framework\TestCase; |
20 | 20 | use Probots\Pinecone\Client as PineconeClient; |
| 21 | +use Psr\Log\LoggerInterface; |
| 22 | +use Psr\Log\NullLogger; |
21 | 23 | use Symfony\AI\Agent\AgentInterface; |
22 | 24 | use Symfony\AI\Agent\Memory\MemoryInputProcessor; |
23 | 25 | use Symfony\AI\Agent\Memory\StaticMemoryProvider; |
|
34 | 36 | use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog; |
35 | 37 | use Symfony\AI\Platform\Capability; |
36 | 38 | use Symfony\AI\Platform\EventListener\TemplateRendererListener; |
| 39 | +use Symfony\AI\Platform\FailoverPlatform; |
37 | 40 | use Symfony\AI\Platform\Message\TemplateRenderer\ExpressionLanguageTemplateRenderer; |
38 | 41 | use Symfony\AI\Platform\Message\TemplateRenderer\StringTemplateRenderer; |
39 | 42 | use Symfony\AI\Platform\Message\TemplateRenderer\TemplateRendererRegistry; |
|
74 | 77 | use Symfony\AI\Store\ManagedStoreInterface; |
75 | 78 | use Symfony\AI\Store\RetrieverInterface; |
76 | 79 | use Symfony\AI\Store\StoreInterface; |
| 80 | +use Symfony\Component\Clock\ClockInterface; |
| 81 | +use Symfony\Component\Clock\MonotonicClock; |
77 | 82 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; |
78 | 83 | use Symfony\Component\DependencyInjection\ContainerBuilder; |
79 | 84 | use Symfony\Component\DependencyInjection\ContainerInterface; |
@@ -4016,6 +4021,133 @@ public function testElevenLabsPlatformWithApiCatalogCanBeRegistered() |
4016 | 4021 | $this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy')); |
4017 | 4022 | } |
4018 | 4023 |
|
| 4024 | + public function testFailoverPlatformCanBeCreated() |
| 4025 | + { |
| 4026 | + $container = $this->buildContainer([ |
| 4027 | + 'ai' => [ |
| 4028 | + 'platform' => [ |
| 4029 | + 'ollama' => [ |
| 4030 | + 'host_url' => 'http://127.0.0.1:11434', |
| 4031 | + ], |
| 4032 | + 'openai' => [ |
| 4033 | + 'api_key' => 'sk-openai_key_full', |
| 4034 | + ], |
| 4035 | + 'failover' => [ |
| 4036 | + 'platforms' => [ |
| 4037 | + 'ai.platform.ollama', |
| 4038 | + 'ai.platform.openai', |
| 4039 | + ], |
| 4040 | + ], |
| 4041 | + ], |
| 4042 | + ], |
| 4043 | + ]); |
| 4044 | + |
| 4045 | + $this->assertTrue($container->hasDefinition('ai.platform.failover')); |
| 4046 | + |
| 4047 | + $definition = $container->getDefinition('ai.platform.failover'); |
| 4048 | + |
| 4049 | + $this->assertTrue($definition->isLazy()); |
| 4050 | + $this->assertSame(FailoverPlatform::class, $definition->getClass()); |
| 4051 | + |
| 4052 | + $this->assertCount(4, $definition->getArguments()); |
| 4053 | + $this->assertCount(2, $definition->getArgument(0)); |
| 4054 | + $this->assertEquals([ |
| 4055 | + new Reference('ai.platform.ollama'), |
| 4056 | + new Reference('ai.platform.openai'), |
| 4057 | + ], $definition->getArgument(0)); |
| 4058 | + $this->assertInstanceOf(Reference::class, $definition->getArgument(1)); |
| 4059 | + $this->assertSame(ClockInterface::class, (string) $definition->getArgument(1)); |
| 4060 | + $this->assertSame(60, $definition->getArgument(2)); |
| 4061 | + $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); |
| 4062 | + $this->assertSame(LoggerInterface::class, (string) $definition->getArgument(3)); |
| 4063 | + |
| 4064 | + $this->assertTrue($definition->hasTag('proxy')); |
| 4065 | + $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); |
| 4066 | + $this->assertTrue($definition->hasTag('ai.platform')); |
| 4067 | + $this->assertSame([['name' => 'failover']], $definition->getTag('ai.platform')); |
| 4068 | + |
| 4069 | + $this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $failover')); |
| 4070 | + |
| 4071 | + $container = $this->buildContainer([ |
| 4072 | + 'ai' => [ |
| 4073 | + 'platform' => [ |
| 4074 | + 'ollama' => [ |
| 4075 | + 'host_url' => 'http://127.0.0.1:11434', |
| 4076 | + ], |
| 4077 | + 'openai' => [ |
| 4078 | + 'api_key' => 'sk-openai_key_full', |
| 4079 | + ], |
| 4080 | + 'failover' => [ |
| 4081 | + 'platforms' => [ |
| 4082 | + 'ai.platform.ollama', |
| 4083 | + 'ai.platform.openai', |
| 4084 | + ], |
| 4085 | + 'retry_period' => 120, |
| 4086 | + ], |
| 4087 | + ], |
| 4088 | + ], |
| 4089 | + ]); |
| 4090 | + |
| 4091 | + $this->assertTrue($container->hasDefinition('ai.platform.failover')); |
| 4092 | + |
| 4093 | + $definition = $container->getDefinition('ai.platform.failover'); |
| 4094 | + |
| 4095 | + $this->assertTrue($definition->isLazy()); |
| 4096 | + $this->assertSame(FailoverPlatform::class, $definition->getClass()); |
| 4097 | + |
| 4098 | + $this->assertCount(4, $definition->getArguments()); |
| 4099 | + $this->assertCount(2, $definition->getArgument(0)); |
| 4100 | + $this->assertInstanceOf(Reference::class, $definition->getArgument(1)); |
| 4101 | + $this->assertSame(ClockInterface::class, (string) $definition->getArgument(1)); |
| 4102 | + $this->assertSame(120, $definition->getArgument(2)); |
| 4103 | + $this->assertInstanceOf(Reference::class, $definition->getArgument(3)); |
| 4104 | + $this->assertSame(LoggerInterface::class, (string) $definition->getArgument(3)); |
| 4105 | + |
| 4106 | + $this->assertTrue($definition->hasTag('proxy')); |
| 4107 | + $this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy')); |
| 4108 | + $this->assertTrue($definition->hasTag('ai.platform')); |
| 4109 | + $this->assertSame([['name' => 'failover']], $definition->getTag('ai.platform')); |
| 4110 | + |
| 4111 | + $this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $failover')); |
| 4112 | + } |
| 4113 | + |
| 4114 | + #[TestDox('Token usage processor tags use the correct agent ID')] |
| 4115 | + public function testTokenUsageProcessorTags() |
| 4116 | + { |
| 4117 | + $container = $this->buildContainer([ |
| 4118 | + 'ai' => [ |
| 4119 | + 'platform' => [ |
| 4120 | + 'openai' => [ |
| 4121 | + 'api_key' => 'sk-test_key', |
| 4122 | + ], |
| 4123 | + ], |
| 4124 | + 'agent' => [ |
| 4125 | + 'tracked_agent' => [ |
| 4126 | + 'platform' => 'ai.platform.openai', |
| 4127 | + 'model' => 'gpt-4', |
| 4128 | + 'track_token_usage' => true, |
| 4129 | + ], |
| 4130 | + ], |
| 4131 | + ], |
| 4132 | + ]); |
| 4133 | + |
| 4134 | + $agentId = 'ai.agent.tracked_agent'; |
| 4135 | + |
| 4136 | + // Token usage processor must exist for OpenAI platform |
| 4137 | + $tokenUsageProcessor = $container->getDefinition('ai.platform.token_usage_processor.openai'); |
| 4138 | + $outputTags = $tokenUsageProcessor->getTag('ai.agent.output_processor'); |
| 4139 | + |
| 4140 | + $foundTag = false; |
| 4141 | + foreach ($outputTags as $tag) { |
| 4142 | + if (($tag['agent'] ?? '') === $agentId) { |
| 4143 | + $foundTag = true; |
| 4144 | + break; |
| 4145 | + } |
| 4146 | + } |
| 4147 | + |
| 4148 | + $this->assertTrue($foundTag, 'Token usage processor should have output tag with full agent ID'); |
| 4149 | + } |
| 4150 | + |
4019 | 4151 | public function testOpenAiPlatformWithDefaultRegion() |
4020 | 4152 | { |
4021 | 4153 | $container = $this->buildContainer([ |
@@ -6987,6 +7119,8 @@ private function buildContainer(array $configuration): ContainerBuilder |
6987 | 7119 | $container->setParameter('kernel.debug', true); |
6988 | 7120 | $container->setParameter('kernel.environment', 'dev'); |
6989 | 7121 | $container->setParameter('kernel.build_dir', 'public'); |
| 7122 | + $container->setDefinition(ClockInterface::class, new Definition(MonotonicClock::class)); |
| 7123 | + $container->setDefinition(LoggerInterface::class, new Definition(NullLogger::class)); |
6990 | 7124 |
|
6991 | 7125 | $extension = (new AiBundle())->getContainerExtension(); |
6992 | 7126 | $extension->load($configuration, $container); |
@@ -7042,6 +7176,13 @@ private function getFullConfig(): array |
7042 | 7176 | 'host' => 'https://api.elevenlabs.io/v1', |
7043 | 7177 | 'api_key' => 'elevenlabs_key_full', |
7044 | 7178 | ], |
| 7179 | + 'failover' => [ |
| 7180 | + 'platforms' => [ |
| 7181 | + 'ai.platform.ollama', |
| 7182 | + 'ai.platform.openai', |
| 7183 | + ], |
| 7184 | + 'retry_period' => 120, |
| 7185 | + ], |
7045 | 7186 | 'gemini' => [ |
7046 | 7187 | 'api_key' => 'gemini_key_full', |
7047 | 7188 | ], |
|
0 commit comments