Skip to content

Commit 4ab559d

Browse files
committed
[AI Bundle] Add ai:platform:invoke command
1 parent 02f20b4 commit 4ab559d

File tree

5 files changed

+239
-12
lines changed

5 files changed

+239
-12
lines changed

src/ai-bundle/config/services.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory;
2323
use Symfony\AI\Agent\Toolbox\ToolResultConverter;
2424
use Symfony\AI\AiBundle\Command\AgentCallCommand;
25+
use Symfony\AI\AiBundle\Command\PlatformInvokeCommand;
2526
use Symfony\AI\AiBundle\Profiler\DataCollector;
2627
use Symfony\AI\AiBundle\Profiler\TraceableToolbox;
2728
use Symfony\AI\AiBundle\Security\EventListener\IsGrantedToolAttributeListener;
@@ -216,5 +217,10 @@
216217
tagged_locator('ai.indexer', 'name'),
217218
])
218219
->tag('console.command')
220+
->set('ai.command.platform_invoke', PlatformInvokeCommand::class)
221+
->args([
222+
tagged_locator('ai.platform', 'name'),
223+
])
224+
->tag('console.command')
219225
;
220226
};

src/ai-bundle/doc/index.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,27 @@ Example: Customer Service Bot
612612
product_info: ['features', 'how to', 'tutorial', 'guide', 'documentation']
613613
fallback: 'general_support' # Fallback for general inquiries
614614
615+
Commands
616+
--------
617+
618+
The AI Bundle provides several console commands for interacting with AI platforms and agents.
619+
620+
``ai:platform:invoke``
621+
~~~~~~~~~~~~~~~~~~~~~~
622+
623+
The ``ai:platform:invoke`` command allows you to directly invoke any configured AI platform with a message.
624+
This is useful for testing platform configurations and quick interactions with AI models.
625+
626+
.. code-block:: terminal
627+
628+
$ php bin/console ai:platform:invoke <platform> <model> "<message>"
629+
630+
# Using OpenAI
631+
$ php bin/console ai:platform:invoke openai gpt-4o-mini "Hello, world!"
632+
633+
# Using Anthropic
634+
$ php bin/console ai:platform:invoke anthropic claude-3-5-sonnet-20241022 "Explain quantum physics"
635+
615636
Usage
616637
-----
617638

src/ai-bundle/src/AiBundle.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
236236
new Reference('ai.platform.model_catalog.anthropic'),
237237
new Reference('ai.platform.contract.anthropic'),
238238
])
239-
->addTag('ai.platform');
239+
->addTag('ai.platform', ['name' => 'anthropic']);
240240

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\AiBundle\Command;
13+
14+
use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\AI\Platform\PlatformInterface;
18+
use Symfony\AI\Platform\Result\TextResult;
19+
use Symfony\Component\Console\Attribute\AsCommand;
20+
use Symfony\Component\Console\Command\Command;
21+
use Symfony\Component\Console\Completion\CompletionInput;
22+
use Symfony\Component\Console\Completion\CompletionSuggestions;
23+
use Symfony\Component\Console\Input\InputArgument;
24+
use Symfony\Component\Console\Input\InputInterface;
25+
use Symfony\Component\Console\Output\OutputInterface;
26+
use Symfony\Component\Console\Style\SymfonyStyle;
27+
use Symfony\Component\DependencyInjection\ServiceLocator;
28+
29+
/**
30+
* @author Oskar Stark <[email protected]>
31+
*/
32+
#[AsCommand(
33+
name: 'ai:platform:invoke',
34+
description: 'Invoke an AI platform with a message',
35+
)]
36+
final class PlatformInvokeCommand extends Command
37+
{
38+
private string $message;
39+
private PlatformInterface $platform;
40+
private string $model;
41+
42+
/**
43+
* @param ServiceLocator<PlatformInterface> $platforms
44+
*/
45+
public function __construct(
46+
private readonly ServiceLocator $platforms,
47+
) {
48+
parent::__construct();
49+
}
50+
51+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
52+
{
53+
if ($input->mustSuggestArgumentValuesFor('platform')) {
54+
$suggestions->suggestValues(array_keys($this->platforms->getProvidedServices()));
55+
}
56+
}
57+
58+
protected function configure(): void
59+
{
60+
$this
61+
->addArgument('platform', InputArgument::REQUIRED, 'The name of the configured platform to invoke')
62+
->addArgument('model', InputArgument::REQUIRED, 'The model to use for the request')
63+
->addArgument('message', InputArgument::REQUIRED, 'The message to send to the AI platform')
64+
->setHelp(
65+
<<<'HELP'
66+
The <info>%command.name%</info> command allows you to invoke configured AI platforms with a message.
67+
68+
Usage:
69+
<info>%command.full_name% <platform_name> <model> "<message>"</info>
70+
71+
Examples:
72+
<info>%command.full_name% openai gpt-4o-mini "Hello, world!"</info>
73+
<info>%command.full_name% anthropic claude-3-5-sonnet-20241022 "Explain quantum physics"</info>
74+
75+
Available platforms depend on your configuration in config/packages/ai.yaml
76+
HELP
77+
);
78+
}
79+
80+
protected function initialize(InputInterface $input, OutputInterface $output): void
81+
{
82+
$platformName = trim((string) $input->getArgument('platform'));
83+
84+
if (!$this->platforms->has($platformName)) {
85+
throw new InvalidArgumentException(\sprintf('Platform "%s" not found. Available platforms: "%s"', $platformName, implode(', ', array_keys($this->platforms->getProvidedServices()))));
86+
}
87+
88+
$this->platform = $this->platforms->get($platformName);
89+
$this->model = trim((string) $input->getArgument('model'));
90+
$this->message = trim((string) $input->getArgument('message'));
91+
}
92+
93+
protected function execute(InputInterface $input, OutputInterface $output): int
94+
{
95+
$io = new SymfonyStyle($input, $output);
96+
97+
try {
98+
$messages = new MessageBag();
99+
$messages->add(Message::ofUser($this->message));
100+
101+
$resultPromise = $this->platform->invoke($this->model, $messages);
102+
$result = $resultPromise->getResult();
103+
104+
if ($result instanceof TextResult) {
105+
$io->writeln('<info>Response:</info> '.$result->getContent());
106+
} else {
107+
$io->error('Unexpected response type from platform');
108+
109+
return Command::FAILURE;
110+
}
111+
} catch (\Exception $e) {
112+
$io->error(\sprintf('Error: %s', $e->getMessage()));
113+
114+
if ($output->isVerbose()) {
115+
$io->writeln('');
116+
$io->writeln('<comment>Exception trace:</comment>');
117+
$io->text($e->getTraceAsString());
118+
}
119+
120+
return Command::FAILURE;
121+
}
122+
123+
return Command::SUCCESS;
124+
}
125+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\AiBundle\Tests\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\AI\AiBundle\Command\PlatformInvokeCommand;
16+
use Symfony\AI\AiBundle\Exception\InvalidArgumentException;
17+
use Symfony\AI\Platform\PlatformInterface;
18+
use Symfony\AI\Platform\Result\InMemoryRawResult;
19+
use Symfony\AI\Platform\Result\ResultPromise;
20+
use Symfony\AI\Platform\Result\TextResult;
21+
use Symfony\Component\Console\Command\Command;
22+
use Symfony\Component\Console\Tester\CommandTester;
23+
use Symfony\Component\DependencyInjection\ServiceLocator;
24+
25+
final class PlatformInvokeCommandTest extends TestCase
26+
{
27+
public function testExecuteSuccessfully()
28+
{
29+
$textResult = new TextResult('Hello! How can I assist you?');
30+
$rawResult = new InMemoryRawResult([]);
31+
$promise = new ResultPromise(fn () => $textResult, $rawResult);
32+
33+
$platform = $this->createMock(PlatformInterface::class);
34+
$platform->method('invoke')
35+
->with('gpt-4o-mini', $this->anything())
36+
->willReturn($promise);
37+
38+
$platforms = $this->createMock(ServiceLocator::class);
39+
$platforms->method('getProvidedServices')->willReturn(['openai' => 'service_class']);
40+
$platforms->method('has')->with('openai')->willReturn(true);
41+
$platforms->method('get')->with('openai')->willReturn($platform);
42+
43+
$command = new PlatformInvokeCommand($platforms);
44+
$commandTester = new CommandTester($command);
45+
46+
$exitCode = $commandTester->execute([
47+
'platform' => 'openai',
48+
'model' => 'gpt-4o-mini',
49+
'message' => 'Hello!',
50+
]);
51+
52+
$this->assertSame(Command::SUCCESS, $exitCode);
53+
$this->assertStringContainsString('Response:', $commandTester->getDisplay());
54+
$this->assertStringContainsString('Hello! How can I assist you?', $commandTester->getDisplay());
55+
}
56+
57+
public function testExecuteWithNonExistentPlatform()
58+
{
59+
$platforms = $this->createMock(ServiceLocator::class);
60+
$platforms->method('getProvidedServices')->willReturn(['openai' => 'service_class']);
61+
$platforms->method('has')->with('invalid')->willReturn(false);
62+
63+
$command = new PlatformInvokeCommand($platforms);
64+
65+
$this->expectException(InvalidArgumentException::class);
66+
$this->expectExceptionMessage('Platform "invalid" not found. Available platforms: "openai"');
67+
68+
$commandTester = new CommandTester($command);
69+
$commandTester->execute([
70+
'platform' => 'invalid',
71+
'model' => 'gpt-4o-mini',
72+
'message' => 'Test message',
73+
]);
74+
}
75+
}

0 commit comments

Comments
 (0)