Skip to content
Merged
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
6 changes: 6 additions & 0 deletions src/ai-bundle/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory;
use Symfony\AI\Agent\Toolbox\ToolResultConverter;
use Symfony\AI\AiBundle\Command\AgentCallCommand;
use Symfony\AI\AiBundle\Command\PlatformInvokeCommand;
use Symfony\AI\AiBundle\Profiler\DataCollector;
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
Expand Down Expand Up @@ -216,5 +217,10 @@
tagged_locator('ai.indexer', 'name'),
])
->tag('console.command')
->set('ai.command.platform_invoke', PlatformInvokeCommand::class)
->args([
tagged_locator('ai.platform', 'name'),
])
->tag('console.command')
;
};
21 changes: 21 additions & 0 deletions src/ai-bundle/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,27 @@ Example: Customer Service Bot
product_info: ['features', 'how to', 'tutorial', 'guide', 'documentation']
fallback: 'general_support' # Fallback for general inquiries

Commands
--------

The AI Bundle provides several console commands for interacting with AI platforms and agents.

``ai:platform:invoke``
~~~~~~~~~~~~~~~~~~~~~~

The ``ai:platform:invoke`` command allows you to directly invoke any configured AI platform with a message.
This is useful for testing platform configurations and quick interactions with AI models.

.. code-block:: terminal

$ php bin/console ai:platform:invoke <platform> <model> "<message>"

# Using OpenAI
$ php bin/console ai:platform:invoke openai gpt-4o-mini "Hello, world!"

# Using Anthropic
$ php bin/console ai:platform:invoke anthropic claude-3-5-sonnet-20241022 "Explain quantum physics"

Usage
-----

Expand Down
24 changes: 12 additions & 12 deletions src/ai-bundle/src/AiBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.anthropic'),
new Reference('ai.platform.contract.anthropic'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'anthropic']);

$container->setDefinition($platformId, $definition);

Expand All @@ -259,7 +259,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.azure.openai'),
new Reference('ai.platform.contract.openai'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'azure.'.$name]);

$container->setDefinition($platformId, $definition);
}
Expand All @@ -280,7 +280,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.elevenlabs'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'eleven_labs']);

$container->setDefinition($platformId, $definition);

Expand All @@ -299,7 +299,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.gemini'),
new Reference('ai.platform.contract.gemini'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'gemini']);

$container->setDefinition($platformId, $definition);

Expand Down Expand Up @@ -339,7 +339,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.vertexai.gemini'),
new Reference('ai.platform.contract.vertexai.gemini'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'vertexai']);

$container->setDefinition($platformId, $definition);

Expand All @@ -359,7 +359,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.contract.openai'),
$platform['region'] ?? null,
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'openai']);

$container->setDefinition($platformId, $definition);

Expand All @@ -378,7 +378,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.openrouter'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'openrouter']);

$container->setDefinition($platformId, $definition);

Expand All @@ -397,7 +397,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.mistral'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'mistral']);

$container->setDefinition($platformId, $definition);

Expand All @@ -416,7 +416,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.lmstudio'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'lmstudio']);

$container->setDefinition($platformId, $definition);

Expand All @@ -435,7 +435,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.ollama'),
new Reference('ai.platform.contract.ollama'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'ollama']);

$container->setDefinition($platformId, $definition);

Expand All @@ -454,7 +454,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.cerebras'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'cerebras']);

$container->setDefinition($platformId, $definition);

Expand All @@ -473,7 +473,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
new Reference('ai.platform.model_catalog.voyage'),
new Reference('ai.platform.contract.default'),
])
->addTag('ai.platform');
->addTag('ai.platform', ['name' => 'voyage']);

$container->setDefinition($platformId, $definition);

Expand Down
125 changes: 125 additions & 0 deletions src/ai-bundle/src/Command/PlatformInvokeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?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.
*/

namespace Symfony\AI\AiBundle\Command;

use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\PlatformInterface;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\ServiceLocator;

/**
* @author Oskar Stark <oskarstark@googlemail.com>
*/
#[AsCommand(
name: 'ai:platform:invoke',
description: 'Invoke an AI platform with a message',
)]
final class PlatformInvokeCommand extends Command
{
private string $message;
private PlatformInterface $platform;
private string $model;

/**
* @param ServiceLocator<PlatformInterface> $platforms
*/
public function __construct(
private readonly ServiceLocator $platforms,
) {
parent::__construct();
}

public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('platform')) {
$suggestions->suggestValues(array_keys($this->platforms->getProvidedServices()));
}
}

protected function configure(): void
{
$this
->addArgument('platform', InputArgument::REQUIRED, 'The name of the configured platform to invoke')
->addArgument('model', InputArgument::REQUIRED, 'The model to use for the request')
->addArgument('message', InputArgument::REQUIRED, 'The message to send to the AI platform')
->setHelp(
<<<'HELP'
The <info>%command.name%</info> command allows you to invoke configured AI platforms with a message.

Usage:
<info>%command.full_name% <platform_name> <model> "<message>"</info>

Examples:
<info>%command.full_name% openai gpt-4o-mini "Hello, world!"</info>
<info>%command.full_name% anthropic claude-3-5-sonnet-20241022 "Explain quantum physics"</info>

Available platforms depend on your configuration in config/packages/ai.yaml
HELP
);
}

protected function initialize(InputInterface $input, OutputInterface $output): void
{
$platformName = trim((string) $input->getArgument('platform'));

if (!$this->platforms->has($platformName)) {
throw new InvalidArgumentException(\sprintf('Platform "%s" not found. Available platforms: "%s"', $platformName, implode(', ', array_keys($this->platforms->getProvidedServices()))));
}

$this->platform = $this->platforms->get($platformName);
$this->model = trim((string) $input->getArgument('model'));
$this->message = trim((string) $input->getArgument('message'));
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

try {
$messages = new MessageBag();
$messages->add(Message::ofUser($this->message));

$resultPromise = $this->platform->invoke($this->model, $messages);
$result = $resultPromise->getResult();

if ($result instanceof TextResult) {
$io->writeln('<info>Response:</info> '.$result->getContent());
} else {
$io->error('Unexpected response type from platform');

return Command::FAILURE;
}
} catch (\Exception $e) {
$io->error(\sprintf('Error: %s', $e->getMessage()));

if ($output->isVerbose()) {
$io->writeln('');
$io->writeln('<comment>Exception trace:</comment>');
$io->text($e->getTraceAsString());
}

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
75 changes: 75 additions & 0 deletions src/ai-bundle/tests/Command/PlatformInvokeCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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.
*/

namespace Symfony\AI\AiBundle\Tests\Command;

use PHPUnit\Framework\TestCase;
use Symfony\AI\AiBundle\Command\PlatformInvokeCommand;
use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
use Symfony\AI\Platform\PlatformInterface;
use Symfony\AI\Platform\Result\InMemoryRawResult;
use Symfony\AI\Platform\Result\ResultPromise;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ServiceLocator;

final class PlatformInvokeCommandTest extends TestCase
{
public function testExecuteSuccessfully()
{
$textResult = new TextResult('Hello! How can I assist you?');
$rawResult = new InMemoryRawResult([]);
$promise = new ResultPromise(fn () => $textResult, $rawResult);

$platform = $this->createMock(PlatformInterface::class);
$platform->method('invoke')
->with('gpt-4o-mini', $this->anything())
->willReturn($promise);

$platforms = $this->createMock(ServiceLocator::class);
$platforms->method('getProvidedServices')->willReturn(['openai' => 'service_class']);
$platforms->method('has')->with('openai')->willReturn(true);
$platforms->method('get')->with('openai')->willReturn($platform);

$command = new PlatformInvokeCommand($platforms);
$commandTester = new CommandTester($command);

$exitCode = $commandTester->execute([
'platform' => 'openai',
'model' => 'gpt-4o-mini',
'message' => 'Hello!',
]);

$this->assertSame(Command::SUCCESS, $exitCode);
$this->assertStringContainsString('Response:', $commandTester->getDisplay());
$this->assertStringContainsString('Hello! How can I assist you?', $commandTester->getDisplay());
}

public function testExecuteWithNonExistentPlatform()
{
$platforms = $this->createMock(ServiceLocator::class);
$platforms->method('getProvidedServices')->willReturn(['openai' => 'service_class']);
$platforms->method('has')->with('invalid')->willReturn(false);

$command = new PlatformInvokeCommand($platforms);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Platform "invalid" not found. Available platforms: "openai"');

$commandTester = new CommandTester($command);
$commandTester->execute([
'platform' => 'invalid',
'model' => 'gpt-4o-mini',
'message' => 'Test message',
]);
}
}