-
-
Notifications
You must be signed in to change notification settings - Fork 102
[AI Bundle] Add chat console command for interactive AI agent conversations #395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
<?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\Agent\AgentInterface; | ||
use Symfony\AI\AiBundle\Exception\InvalidArgumentException; | ||
use Symfony\AI\Platform\Message\Message; | ||
use Symfony\AI\Platform\Message\MessageBag; | ||
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\Question\ChoiceQuestion; | ||
use Symfony\Component\Console\Style\SymfonyStyle; | ||
use Symfony\Component\DependencyInjection\ServiceLocator; | ||
|
||
/** | ||
* @author Oskar Stark <oskarstark@gmail.com> | ||
*/ | ||
#[AsCommand( | ||
name: 'ai:chat', | ||
description: 'Chat with an agent', | ||
)] | ||
final class ChatCommand extends Command | ||
{ | ||
private AgentInterface $agent; | ||
private string $agentName; | ||
|
||
/** | ||
* @param ServiceLocator<AgentInterface> $agents | ||
*/ | ||
public function __construct( | ||
private readonly ServiceLocator $agents, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void | ||
{ | ||
if ($input->mustSuggestArgumentValuesFor('agent')) { | ||
$suggestions->suggestValues($this->getAvailableAgentNames()); | ||
} | ||
} | ||
|
||
protected function configure(): void | ||
{ | ||
$this | ||
->addArgument('agent', InputArgument::OPTIONAL, 'The name of the agent to chat with') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The argument should be marked as required, as you use the |
||
->setHelp( | ||
<<<'HELP' | ||
The <info>%command.name%</info> command allows you to chat with different agents. | ||
|
||
Usage: | ||
<info>%command.full_name% [<agent_name>]</info> | ||
|
||
Examples: | ||
<info>%command.full_name% wikipedia</info> | ||
|
||
If no agent is specified, you'll be prompted to select one interactively. | ||
|
||
The chat session is interactive. Type your messages and press Enter to send. | ||
Type 'exit' or 'quit' to end the conversation. | ||
HELP | ||
); | ||
} | ||
|
||
protected function interact(InputInterface $input, OutputInterface $output): void | ||
{ | ||
// Skip interaction in non-interactive mode | ||
if (!$input->isInteractive()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useless check. The parent class already skips calling the |
||
return; | ||
} | ||
|
||
$agentArg = $input->getArgument('agent'); | ||
|
||
// If agent is already provided and valid, nothing to do | ||
if ($agentArg) { | ||
return; | ||
} | ||
|
||
$availableAgents = $this->getAvailableAgentNames(); | ||
|
||
if (0 === \count($availableAgents)) { | ||
throw new InvalidArgumentException('No agents are configured.'); | ||
} | ||
|
||
$question = new ChoiceQuestion( | ||
'Please select an agent to chat with:', | ||
$availableAgents, | ||
0 | ||
); | ||
$question->setErrorMessage('Agent %s is invalid.'); | ||
|
||
/** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ | ||
$helper = $this->getHelper('question'); | ||
$selectedAgent = $helper->ask($input, $output, $question); | ||
|
||
$input->setArgument('agent', $selectedAgent); | ||
} | ||
|
||
protected function initialize(InputInterface $input, OutputInterface $output): void | ||
{ | ||
// Initialization will be done in execute() after interact() has run | ||
} | ||
|
||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
// Initialize agent (moved from initialize() to execute() so it runs after interact()) | ||
$availableAgents = array_keys($this->agents->getProvidedServices()); | ||
|
||
if (0 === \count($availableAgents)) { | ||
throw new InvalidArgumentException('No agents are configured.'); | ||
} | ||
|
||
$agentArg = $input->getArgument('agent'); | ||
$this->agentName = \is_string($agentArg) ? $agentArg : ''; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why using instance properties (making the service stateful) when they are only used locally ? You should use local variables instead. |
||
|
||
// In non-interactive mode, agent is required | ||
if (!$this->agentName && !$input->isInteractive()) { | ||
throw new InvalidArgumentException(\sprintf('Agent name is required. Available agents: "%s"', implode(', ', $availableAgents))); | ||
} | ||
|
||
// Validate that the agent exists if one was provided | ||
if ($this->agentName && !$this->agents->has($this->agentName)) { | ||
throw new InvalidArgumentException(\sprintf('Agent "%s" not found. Available agents: "%s"', $this->agentName, implode(', ', $availableAgents))); | ||
} | ||
|
||
// If we still don't have an agent name at this point, something went wrong | ||
if (!$this->agentName) { | ||
throw new InvalidArgumentException(\sprintf('Agent name is required. Available agents: "%s"', implode(', ', $availableAgents))); | ||
} | ||
|
||
$this->agent = $this->agents->get($this->agentName); | ||
|
||
// Now start the chat | ||
$io = new SymfonyStyle($input, $output); | ||
|
||
$io->title(\sprintf('Chat with %s Agent', $this->agentName)); | ||
$io->info('Type your message and press Enter. Type "exit" or "quit" to end the conversation.'); | ||
$io->newLine(); | ||
|
||
$messages = new MessageBag(); | ||
$systemPromptDisplayed = false; | ||
|
||
while (true) { | ||
$userInput = $io->ask('You'); | ||
|
||
if (!\is_string($userInput) || '' === trim($userInput)) { | ||
continue; | ||
} | ||
|
||
if (\in_array(strtolower($userInput), ['exit', 'quit'], true)) { | ||
$io->success('Goodbye!'); | ||
break; | ||
} | ||
|
||
$messages->add(Message::ofUser($userInput)); | ||
|
||
try { | ||
$result = $this->agent->call($messages); | ||
|
||
// Display system prompt after first successful call | ||
if (!$systemPromptDisplayed && null !== ($systemMessage = $messages->getSystemMessage())) { | ||
$io->section('System Prompt'); | ||
$io->block($systemMessage->content, null, 'fg=gray', ' ', true); | ||
$systemPromptDisplayed = true; | ||
} | ||
|
||
if ($result instanceof TextResult) { | ||
$io->write('<fg=yellow>Assistant</>:'); | ||
$io->writeln(''); | ||
$io->writeln($result->getContent()); | ||
$io->newLine(); | ||
|
||
$messages->add(Message::ofAssistant($result->getContent())); | ||
} else { | ||
$io->error('Unexpected response type from agent'); | ||
} | ||
} catch (\Exception $e) { | ||
$io->error(\sprintf('Error: %s', $e->getMessage())); | ||
OskarStark marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if ($output->isVerbose()) { | ||
$io->writeln(''); | ||
$io->writeln('<comment>Exception trace:</comment>'); | ||
$io->text($e->getTraceAsString()); | ||
} | ||
} | ||
} | ||
|
||
return Command::SUCCESS; | ||
} | ||
|
||
/** | ||
* @return string[] | ||
*/ | ||
private function getAvailableAgentNames(): array | ||
{ | ||
return array_keys($this->agents->getProvidedServices()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?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\Exception; | ||
|
||
/** | ||
* @author Oskar Stark <oskarstark@googlemail.com> | ||
*/ | ||
class RuntimeException extends \RuntimeException implements ExceptionInterface | ||
{ | ||
} |
Uh oh!
There was an error while loading. Please reload this page.