Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/components/platform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,70 @@ Thanks to Symfony's Cache component, platform calls can be cached to reduce call

echo $secondResult->getContent().\PHP_EOL;

High Availability
-----------------

As most platform exposes a REST API, errors can occurs during generation phase due to network issues, timeout and more.

To prevent exceptions at the application level and allows to keep a smooth experience for end users,
the :class:`Symfony\\AI\\Platform\\Bridge\\Failover\\FailoverPlatform` can be used to automatically call a backup platform::

use Symfony\AI\Platform\Bridge\Failover\FailoverPlatform;
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory;
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory as OpenAiPlatformFactory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;

$rateLimiter = new RateLimiterFactory([
'policy' => 'sliding_window',
'id' => 'failover',
'interval' => '3 seconds',
'limit' => 1,
], new InMemoryStorage());

// # Ollama will fail as 'gpt-4o' is not available in the catalog
$platform = new FailoverPlatform([
OllamaPlatformFactory::create(env('OLLAMA_HOST_URL'), http_client()),
OpenAiPlatformFactory::create(env('OPENAI_API_KEY'), http_client()),
], $rateLimiter);

$result = $platform->invoke('gpt-4o', new MessageBag(
Message::forSystem('You are a helpful assistant.'),
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
));

echo $result->asText().\PHP_EOL;

This platform can also be configured when using the bundle::

# config/packages/ai.yaml
ai:
platform:
openai:
# ...
ollama:
# ...
failover:
ollama_to_openai:
platforms:
- 'ai.platform.ollama'
- 'ai.platform.openai'
rate_limiter: 'limiter.failover_platform'

# config/packages/rate_limiter.yaml
framework:
rate_limiter:
failover_platform:
policy: 'sliding_window'
limit: 100
interval: '60 minutes'

.. note::

Platforms are executed in the order they're injected into :class:`Symfony\\AI\\Platform\\Bridge\\Failover\\FailoverPlatform`.

Testing Tools
-------------

Expand Down
1 change: 1 addition & 0 deletions examples/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"symfony/ai-docker-model-runner-platform": "@dev",
"symfony/ai-elasticsearch-store": "@dev",
"symfony/ai-eleven-labs-platform": "@dev",
"symfony/ai-failover-platform": "@dev",
"symfony/ai-gemini-platform": "@dev",
"symfony/ai-generic-platform": "@dev",
"symfony/ai-session-message-store": "@dev",
Expand Down
43 changes: 43 additions & 0 deletions examples/misc/failover-platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Platform\Bridge\Failover\FailoverPlatform;
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory;
use Symfony\AI\Platform\Bridge\OpenAi\Gpt\ResultConverter;
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory as OpenAiPlatformFactory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;

require_once dirname(__DIR__).'/bootstrap.php';

$rateLimiter = new RateLimiterFactory([
'policy' => 'sliding_window',
'id' => 'failover',
'interval' => '3 seconds',
'limit' => 1,
], new InMemoryStorage());

// # Ollama will fail as 'gpt-4o' is not available in the catalog
$platform = new FailoverPlatform([
OllamaPlatformFactory::create(env('OLLAMA_HOST_URL'), http_client()),
OpenAiPlatformFactory::create(env('OPENAI_API_KEY'), http_client()),
], $rateLimiter);

$result = $platform->invoke('gpt-4o', new MessageBag(
Message::forSystem('You are a helpful assistant.'),
Message::ofUser('Tina has one brother and one sister. How many sisters do Tina\'s siblings have?'),
));

assert($result->getResultConverter() instanceof ResultConverter);

echo $result->asText().\PHP_EOL;
1 change: 1 addition & 0 deletions splitsh.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"ai-deep-seek-platform": "src/platform/src/Bridge/DeepSeek",
"ai-docker-model-runner-platform": "src/platform/src/Bridge/DockerModelRunner",
"ai-eleven-labs-platform": "src/platform/src/Bridge/ElevenLabs",
"ai-failover-platform": "src/platform/src/Bridge/Failover",
"ai-gemini-platform": "src/platform/src/Bridge/Gemini",
"ai-generic-platform": "src/platform/src/Bridge/Generic",
"ai-hugging-face-platform": "src/platform/src/Bridge/HuggingFace",
Expand Down
1 change: 1 addition & 0 deletions src/ai-bundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"symfony/ai-doctrine-message-store": "@dev",
"symfony/ai-eleven-labs-platform": "@dev",
"symfony/ai-elasticsearch-store": "@dev",
"symfony/ai-failover-platform": "@dev",
"symfony/ai-gemini-platform": "@dev",
"symfony/ai-generic-platform": "@dev",
"symfony/ai-session-message-store": "@dev",
Expand Down
11 changes: 11 additions & 0 deletions src/ai-bundle/config/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@
->end()
->end()
->end()
->arrayNode('failover')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->arrayNode('platforms')
->scalarPrototype()->end()
->end()
->stringNode('rate_limiter')->cannotBeEmpty()->end()
->end()
->end()
->end()
->arrayNode('gemini')
->children()
->stringNode('api_key')->isRequired()->end()
Expand Down
31 changes: 31 additions & 0 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Google\Auth\ApplicationDefaultCredentials;
use Google\Auth\FetchAuthTokenInterface;
use Psr\Log\LoggerInterface;
use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Agent\Attribute\AsInputProcessor;
Expand Down Expand Up @@ -60,6 +61,8 @@
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
use Symfony\AI\Platform\Bridge\Failover\FailoverPlatform;
use Symfony\AI\Platform\Bridge\Failover\FailoverPlatformFactory;
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
use Symfony\AI\Platform\Bridge\Generic\PlatformFactory as GenericPlatformFactory;
use Symfony\AI\Platform\Bridge\HuggingFace\PlatformFactory as HuggingFacePlatformFactory;
Expand Down Expand Up @@ -511,6 +514,34 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
return;
}

if ('failover' === $type) {
if (!ContainerBuilder::willBeAvailable('symfony/ai-failover-platform', FailoverPlatformFactory::class, ['symfony/ai-bundle'])) {
throw new RuntimeException('Failover platform configuration requires "symfony/ai-failover-platform" package. Try running "composer require symfony/ai-failover-platform".');
}

foreach ($platform as $name => $config) {
$definition = (new Definition(FailoverPlatform::class))
->setFactory(FailoverPlatformFactory::class.'::create')
->setLazy(true)
->setArguments([
array_map(
static fn (string $wrappedPlatform): Reference => new Reference($wrappedPlatform),
$config['platforms'],
),
new Reference($config['rate_limiter']),
new Reference(ClockInterface::class),
new Reference(LoggerInterface::class),
])
->addTag('proxy', ['interface' => PlatformInterface::class])
->addTag('ai.platform', ['name' => $type]);

$container->setDefinition('ai.platform.'.$type.'.'.$name, $definition);
$container->registerAliasForArgument('ai.platform.'.$type.'.'.$name, PlatformInterface::class, $name);
}

return;
}

if ('gemini' === $type) {
if (!ContainerBuilder::willBeAvailable('symfony/ai-gemini-platform', GeminiPlatformFactory::class, ['symfony/ai-bundle'])) {
throw new RuntimeException('Gemini platform configuration requires "symfony/ai-gemini-platform" package. Try running "composer require symfony/ai-gemini-platform".');
Expand Down
81 changes: 81 additions & 0 deletions src/ai-bundle/tests/DependencyInjection/AiBundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Probots\Pinecone\Client as PineconeClient;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Agent\Memory\MemoryInputProcessor;
use Symfony\AI\Agent\Memory\StaticMemoryProvider;
Expand All @@ -31,6 +33,8 @@
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog;
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
use Symfony\AI\Platform\Bridge\Failover\FailoverPlatform;
use Symfony\AI\Platform\Bridge\Failover\FailoverPlatformFactory;
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
use Symfony\AI\Platform\CachedPlatform;
use Symfony\AI\Platform\Capability;
Expand Down Expand Up @@ -84,6 +88,8 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class AiBundleTest extends TestCase
Expand Down Expand Up @@ -4019,6 +4025,62 @@ public function testElevenLabsPlatformWithApiCatalogCanBeRegistered()
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
}

public function testFailoverPlatformCanBeCreated()
{
$container = $this->buildContainer([
'ai' => [
'platform' => [
'ollama' => [
'host_url' => 'http://127.0.0.1:11434',
],
'openai' => [
'api_key' => 'sk-openai_key_full',
],
'failover' => [
'main' => [
'platforms' => [
'ai.platform.ollama',
'ai.platform.openai',
],
'rate_limiter' => 'limiter.failover_platform',
],
],
],
],
]);

$this->assertTrue($container->hasDefinition('ai.platform.failover.main'));

$definition = $container->getDefinition('ai.platform.failover.main');

$this->assertSame([
FailoverPlatformFactory::class,
'create',
], $definition->getFactory());
$this->assertTrue($definition->isLazy());
$this->assertSame(FailoverPlatform::class, $definition->getClass());

$this->assertCount(4, $definition->getArguments());
$this->assertCount(2, $definition->getArgument(0));
$this->assertEquals([
new Reference('ai.platform.ollama'),
new Reference('ai.platform.openai'),
], $definition->getArgument(0));
$this->assertInstanceOf(Reference::class, $definition->getArgument(1));
$this->assertSame('limiter.failover_platform', (string) $definition->getArgument(1));
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
$this->assertSame(ClockInterface::class, (string) $definition->getArgument(2));
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
$this->assertSame(LoggerInterface::class, (string) $definition->getArgument(3));

$this->assertTrue($definition->hasTag('proxy'));
$this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy'));
$this->assertTrue($definition->hasTag('ai.platform'));
$this->assertSame([['name' => 'failover']], $definition->getTag('ai.platform'));

$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $main'));
}

public function testOpenAiPlatformWithDefaultRegion()
{
$container = $this->buildContainer([
Expand Down Expand Up @@ -7013,6 +7075,16 @@ private function buildContainer(array $configuration): ContainerBuilder
$container->setParameter('kernel.environment', 'dev');
$container->setParameter('kernel.build_dir', 'public');
$container->setDefinition(ClockInterface::class, new Definition(MonotonicClock::class));
$container->setDefinition(LoggerInterface::class, new Definition(NullLogger::class));
$container->setDefinition('limiter.failover_platform', new Definition(RateLimiterFactory::class, [
[
'policy' => 'sliding_window',
'id' => 'test',
'interval' => '60 seconds',
'limit' => 1,
],
new Definition(InMemoryStorage::class),
]));

$extension = (new AiBundle())->getContainerExtension();
$extension->load($configuration, $container);
Expand Down Expand Up @@ -7068,6 +7140,15 @@ private function getFullConfig(): array
'host' => 'https://api.elevenlabs.io/v1',
'api_key' => 'elevenlabs_key_full',
],
'failover' => [
'main' => [
'platforms' => [
'ai.platform.ollama',
'ai.platform.openai',
],
'rate_limiter' => 'limiter.failover_platform',
],
],
'gemini' => [
'api_key' => 'gemini_key_full',
],
Expand Down
1 change: 1 addition & 0 deletions src/platform/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"phpstan/phpstan": "^2.1.17",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5.46",
"symfony/ai-agent": "@dev",
"symfony/cache": "^7.3|^8.0",
"symfony/console": "^7.3|^8.0",
"symfony/dotenv": "^7.3|^8.0",
Expand Down
3 changes: 3 additions & 0 deletions src/platform/src/Bridge/Failover/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.git* export-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Please do not submit any Pull Requests here. They will be closed.
---

Please submit your PR here instead:
https://github.com/symfony/ai

This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Close Pull Request

on:
pull_request_target:
types: [opened]

jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.

However, you should instead open your PR on the main repository:
https://github.com/symfony/ai

This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
4 changes: 4 additions & 0 deletions src/platform/src/Bridge/Failover/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
vendor/
composer.lock
phpunit.xml
.phpunit.result.cache
7 changes: 7 additions & 0 deletions src/platform/src/Bridge/Failover/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CHANGELOG
=========

0.1
---

* Add the bridge
Loading
Loading