symfony/ai-bundle
Symfony AI Bundle integrates the Symfony AI components into your Symfony application, providing ready-made configuration and service wiring for building AI-powered features. It is experimental and not covered by Symfony's backward compatibility promise.
Install the Bundle (v0.15):
composer require symfony/ai-bundle:^0.15
Enable in config/bundles.php:
return [
// ...
Symfony\AI\Bundle\AiBundle::class => ['all' => true],
];
Configure a Platform (config/packages/ai.yaml):
ai:
platforms:
openai:
client: 'ai.platform.openai'
model: 'gpt-4'
api_key: '%env(OPENAI_API_KEY)%'
v0.15 adds Hub API model resolution with a model class per task. Explicit model values still work and are safer when you need deterministic behavior; use Hub/task-based resolution when you want the bundle to resolve models for a task.
First Use Case: Simple Chat Agent
use Symfony\AI\Agent\AgentFactoryInterface;
class ChatController
{
public function __construct(
private AgentFactoryInterface $agentFactory
) {}
public function chat(string $message): string
{
$agent = $this->agentFactory->createAgent('chat', [
'model' => 'gpt-4',
'platform' => 'openai',
'tools' => [],
'systemPrompt' => 'You are a helpful assistant.',
]);
$response = $agent->chat([$message]);
return $response->getContent();
}
}
Route the Controller:
# config/routes.yaml
chat:
path: /chat
controller: App\Controller\ChatController::chat
config/packages/ai.yaml for platform/store setup.src/Agent/ for custom agent logic.src/Bridge/Decision/ for decision types moved out of the TypeSafe bridge in v0.15.tests/ for integration patterns, including new profiler serialization regression tests for nested results.ai.yaml with client aliases and models.
ai:
platforms:
anthropic:
client: 'ai.platform.anthropic'
model: 'claude-3'
api_key: '%env(ANTHROPIC_API_KEY)%'
Provider abstraction to route requests to the correct platform:
$provider = $this->container->get('ai.platform_provider');
$response = $provider->get('anthropic')->chat([$message]);
# argument injection consistency. If you previously added manual aliases, verify them against the new defaults.#[AsTool] for automatic registration:
use Symfony\AI\Agent\Attribute\AsTool;
class MyTools {
#[AsTool(name: 'fetch_weather', description: 'Fetches weather data')]
public function fetchWeather(string $location): string {
return "Weather in $location: Sunny";
}
}
use Symfony\AI\Agent\InputProcessorInterface;
class SanitizeInputProcessor implements InputProcessorInterface {
public function process(array $input): array {
return array_map('htmlspecialchars', $input);
}
}
Register in services.yaml:
services:
App\Agent\SanitizeInputProcessor:
tags: ['ai.agent.input_processor']
kernel.reset. If you run long-lived workers, ensure the kernel is reset between jobs so tool changes are picked up.ai:
stores:
documents:
type: 'sqlite'
path: '%kernel.project_dir%/var/vector_store.sqlite'
setup_options to configure vector size, and index creation uses the store’s similarity and distance operator class:
ai:
stores:
documents:
type: 'postgres'
setup_options:
vector_size: 1536
$query = new Query('Symfony AI', [
'filter' => ['category' => 'framework'],
]);
$results = $store->search($query, 5);
use Symfony\AI\Agent\Attribute\IsGrantedTool;
class AdminTools {
#[IsGrantedTool('ROLE_ADMIN')]
#[AsTool(name: 'delete_user')]
public function deleteUser(int $userId): bool {
// ...
}
}
$agent->chat([$message]); // Logs to /_profiler/ai
$this->container->get('ai.data_collector')->collectTokens($agent, $response);
php bin/console ai:setup --help
Then pass the appropriate vector size / setup options for your store.ScopingHttpClient for platform-specific API keys:
ai:
platforms:
openai:
client: 'ai.platform.openai'
http_client: 'ai.http_client.openai'
Configure in config/packages/http_client.yaml:
http_clients:
ai.openai:
base_uri: 'https://api.openai.com/v1'
auth_bearer: '%env(OPENAI_API_KEY)%'
PlatformBridgeInterface for unsupported providers:
use Symfony\AI\Platform\PlatformBridgeInterface;
class CustomPlatformBridge implements PlatformBridgeInterface {
public function chat(array $messages, array $options): string {
// Custom logic
}
}
Register in services.yaml:
services:
App\AI\CustomPlatformBridge:
tags: ['ai.platform_bridge']
// Before v0.15
use Symfony\AI\Bridge\TypeSafe\...;
// v0.15+
use Symfony\AI\Bridge\Decision\...;
Check your composer.json to ensure you depend on the correct bridge package if you use decision types directly.MockPlatform for unit tests:
use Symfony\AI\Platform\MockPlatform;
$mockPlatform = new MockPlatform();
$mockPlatform->expects($this)->chat([$message])->willReturn('Mocked response');
$this->container->set('ai.platform.openai', $mockPlatform);
ai:
platforms:
openai:
cache: true
StreamingResponse for real-time interactions:
$response = $agent->chat([$message], ['stream' => true]);
foreach ($response->getDeltas() as $delta) {
echo $delta->getContent();
}
^0.15 after testing, and monitor Symfony AI issues.Symfony\AI\Bridge\TypeSafe references and migrate them to Symfony\AI\Bridge\Decision.setup_options.vector_size must match your embedding model dimensions. Changing it after an index exists usually requires recreating/reindexing the store.
Workaround: Set vector size at project setup, and document it alongside the embedding model choice.ai:setup or reindexing.kernel.reset. Long-running workers that do not reset the kernel may keep stale tools.
Workaround: Ensure workers reset the kernel between jobs, or restart workers after changing MCP tools.ai.platform.openai, the last one wins.
Workaround: Use explicit aliases in services.yaml:
services:
ai.platform.openai: '@ai.platform.openai_custom'
type: 'memory' for testing or upgrade to PostgreSQL.#[IsGrantedTool] are accessible to all users.
Workaround: Always annotate tools or use #[IsGrantedTool('IS_AUTHENTICATED_FULLY')] as a default.OutputProcessorInterface to redact data:
class RedactPIIProcessor implements OutputProcessorInterface {
public function process(string $content): string {
return preg_replace('/\b\d{3}-\d{2}-\d{4}\b/', '[REDACTED]', $content);
}
}
RewindableGenerator) may break streaming.
Workaround: Convert to arrays or use iterator_to_array():
$deltas = iterator_to_array($response->getDeltas());
CompilerPass may fail if services depend on each other.
Workaround: Use onLoad() to defer registration:
public function process(ContainerBuilder $container) {
$container->setCompilerPass(new MyCompilerPass(), PassConfig::TYPE_OPTIMIZE);
}
/_profiler/ai for request/response payloads, tool calls, and token usage.php bin/console ai:setup --help to see available setup options, especially vector size configuration.ai.yaml or service aliases.debug:container to verify platform, store, and bridge aliases after upgrading to v0.15.MockPlatform in tests to avoid Hub API/network dependencies.How can I help you explore Laravel packages today?