symfony/ai-agent
Framework for building AI agents on top of Symfony AI's Platform and Store components, letting agents interact with users, perform tasks and manage workflows. Experimental, not covered by Symfony's BC promise.
Install Core Packages (pin to the 0.15 release train — see Gotchas):
composer require symfony/ai-agent:^0.15 symfony/ai-platform:^0.15
Create a Basic Agent:
use Symfony\Component\Ai\Agent\Agent;
use Symfony\Component\Ai\Agent\Input;
use Symfony\Component\Ai\Agent\Output;
$agent = new Agent(
new Input('What is Laravel?'),
new \Symfony\Component\Ai\Platform\OpenAiPlatform('your-api-key')
);
$output = $agent->run();
echo $output->getContent();
First Use Case: Chatbot with Wikipedia Tool
composer require symfony/ai-wikipedia-tool:^0.15
use Symfony\Component\Ai\Tool\WikipediaTool;
$agent = new Agent(
new Input('Explain Symfony AI in simple terms'),
new \Symfony\Component\Ai\Platform\OpenAiPlatform('api-key'),
new \Symfony\Component\Ai\Toolbox\Toolbox([new WikipediaTool()])
);
src/Agent.php: Entry point for agent creation.src/Toolbox/: Tool integration patterns.tests/: Real-world usage examples (e.g., AgentTest.php, MultiAgentTest.php).CHANGELOG.md / release notes: The package is still pre-1.0 (0.15.x); check every minor bump — v0.15 removed InMemoryPlatform and reworked the mock/testing path.$agent->addInputProcessor(new \Symfony\Component\Ai\Agent\InputProcessor\SystemPromptInputProcessor('You are a helpful assistant.'));
$toolbox = new \Symfony\Component\Ai\Toolbox\Toolbox([
new \Symfony\Component\Ai\Tool\BraveSearchTool('your-api-key'),
new \Symfony\Component\Ai\Tool\FilesystemTool('/tmp'),
]);
$agent->setToolbox($toolbox);
$memory = new \Symfony\Component\Ai\Memory\StaticMemoryProvider();
$agent->setMemory($memory);
$output = $agent->run();
$output->getContent(); // Final response
$output->getToolCalls(); // Logged tool invocations
Service Provider Integration:
// app/Providers/AiServiceProvider.php
public function register()
{
$this->app->singleton(Agent::class, function ($app) {
return new Agent(
new Input(''),
new \Symfony\Component\Ai\Platform\OpenAiPlatform(config('services.openai.key')),
new \Symfony\Component\Ai\Toolbox\Toolbox([
new \Symfony\Component\Ai\Tool\SerpApiTool(config('services.serpapi.key')),
])
);
});
}
Tool Bridge Registration:
// app/Providers/AiServiceProvider.php
public function boot()
{
$this->app->bind(
\Symfony\Component\Ai\Tool\ClockTool::class,
fn() => new \Symfony\Component\Ai\Tool\ClockTool()
);
}
Event-Driven Tool Calls (Laravel Events):
// Listen to tool call events
event(new \Symfony\Component\Ai\Event\ToolCallRequested(
$toolCall,
$agent
));
Async Processing with Queues:
// Dispatch agent job
dispatch(new RunAgentJob($input, $agent));
use Symfony\Component\Ai\MultiAgent\MultiAgent;
$researchAgent = new Agent(/* ... */);
$responseAgent = new Agent(/* ... */);
$multiAgent = new MultiAgent([$researchAgent, $responseAgent]);
$output = $multiAgent->run(new Input('Research and summarize Symfony AI'));
$memory = new \Symfony\Component\Ai\Memory\StaticMemoryProvider();
$agent->setMemory($memory);
$store = new \Symfony\Component\Ai\Store\PostgresStore(
new \Doctrine\DBAL\Connection($connectionParams)
);
$memory = new \Symfony\Component\Ai\Memory\EmbeddingProvider($store);
$agent->setMemory($memory);
asObject() now accepts an optional class argument, which is what you want for static analysis (PHPStan/Larastan) and IDE autocompletion:
/** @var \App\Dto\Plan $plan */
$plan = $output->asObject(\App\Dto\Plan::class);
// Untyped (no static analysis support) still works:
$plan = $output->asObject();
use Symfony\Component\Ai\Agent\ProgressStage;
$agent->onProgress(function ($stage, $payload) {
match ($stage) {
ProgressStage::THINKING => /* ... */,
ProgressStage::TOOL_CALL => /* ... */,
ProgressStage::DONE => /* ... */,
default => null,
};
});
Constants give you a stable contract instead of magic strings that may change between 0.x releases.Tool Call Validation Failures:
symfony/validator for argument validation:
use Symfony\Component\Validator\Constraints as Assert;
#[Tool]
public function search(string #[Assert\NotBlank] $query): string { ... }
Memory Leaks:
EmbeddingProvider with TTL or clear memory manually:
$memory->clear();
Tool Dispatch Conflicts:
$toolbox->addTool(new \App\Tool\CustomBraveSearchTool(), 'custom_brave');
Streaming Quirks:
DeltaInterface for chunked responses:
$output->stream(function (DeltaInterface $delta) {
echo $delta->getContent();
});
Symfony Dependency Conflicts:
symfony/clock or symfony/http-client may conflict with Laravel.$this->app->bind(\Symfony\Component\Clock\ClockInterface::class, function () {
return \Illuminate\Support\Facades\Clock::getFacadeRoot();
});
InMemoryPlatform Removed (v0.15, breaking):
Symfony\Component\Ai\Platform\InMemoryPlatform no longer exists. Any test harness or local fake you built on top of it (including the previous MockPlatform example in this doc) will fatal on upgrade.MockPlatformFactory:
use Symfony\Component\Ai\Platform\MockPlatformFactory;
$agent = new Agent(
new Input('Test'),
(new MockPlatformFactory())->create()
);
symfony/ai-* packages together (composer require "symfony/ai-*:^0.15") or you will end up with mismatched platform interfaces.Enable Verbose Logging:
$agent->setLogger(new \Monolog\Logger('ai_agent', [
new \Monolog\Handler\StreamHandler('storage/logs/ai.log', Monolog\Logger::DEBUG),
]));
Inspect Tool Calls:
$output = $agent->run();
foreach ($output->getToolCalls() as $call) {
dump($call->getName(), $call->getArguments());
}
Test with the Mock Platform Factory (updated for v0.15):
use Symfony\Component\Ai\Platform\MockPlatformFactory;
$factory = new MockPlatformFactory();
$agent = new Agent(new Input('Test'), $factory->create());
Bind the factory in your TestCase::setUp() (or a dedicated FakeAiServiceProvider) so agents resolved from the container are faked consistently across your Laravel feature tests.
Trace Progress Stages:
foreach ($output->getProgress() as $event) {
dump($event->getStage()); // compare against ProgressStage::* constants
}
Custom Input/Output Processors:
class UppercaseOutputProcessor implements OutputProcessorInterface {
public function __invoke(Output $output): Output {
$output->setContent(strtoupper($output->getContent()));
return $output;
}
}
$agent->addOutputProcessor(new UppercaseOutputProcessor());
Dynamic Tool Registration:
$toolbox->addToolsFromDirectory(__DIR__.'/Tools');
Override JSON Schema Generation:
use Symfony\Component\Ai\Tool\Attribute\Tool;
#[Tool(
schema: [
'properties' => [
'query' => ['type' => 'string', 'description' => 'Custom search query']
]
]
)]
public function customSearch(string $query) { ... }
Cache Tool Responses:
$toolbox->addTool(new \App\Tool\CachedSerpApiTool(
new \Symfony\Component\Ai\Tool\SerpApiTool('key'),
new \Illuminate\Cache\Repository
));
Batch Tool Calls:
$agent->setToolbox(new \Symfony\Component\Ai\Toolbox\Toolbox([
new \App\Tool\BatchWebScraperTool(),
]));
Async Tool Execution:
$toolbox->addTool(new \App\Tool\AsyncClockTool(
new \Symfony\Component\Ai\Tool\ClockTool()
));
Blade Integration:
OutputProcessor to render responses:
$agent->addOutputProcessor(function (Output $output) {
return new Output(view('ai.response', ['content' => $output->getContent()])->render());
});
Nova Tool Integration:
public function tools(Nova $nova)
{
Nova::tools([
new \App\Nova\AiSearchTool(),
]);
}
Config Caching: agent/platform closures in a service provider are fine, but anything reading config('services.…') at construction time must be resolved lazily (inside the singleton closure) so php artisan config:cache doesn't bake stale keys into a serialized definition.
How can I help you explore Laravel packages today?