diff --git a/examples/misc/persistent-chat.php b/examples/misc/persistent-chat.php new file mode 100644 index 00000000..849cf31a --- /dev/null +++ b/examples/misc/persistent-chat.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\Chat; +use Symfony\AI\Agent\Chat\MessageStore\InMemoryStore; +use Symfony\AI\Platform\Bridge\OpenAI\GPT; +use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\Dotenv\Dotenv; + +require_once dirname(__DIR__).'/vendor/autoload.php'; +(new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); + +if (empty($_ENV['OPENAI_API_KEY'])) { + echo 'Please set the OPENAI_API_KEY environment variable.'.\PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']); +$llm = new GPT(GPT::GPT_4O_MINI); + +$agent = new Agent($platform, $llm); +$chat = new Chat($agent, new InMemoryStore()); + +$messages = new MessageBag( + Message::forSystem('You are a helpful assistant. You only answer with short sentences.'), +); + +$chat->initiate($messages); +$chat->submit(Message::ofUser('My name is Christopher.')); +$message = $chat->submit(Message::ofUser('What is my name?')); + +echo $message->content.\PHP_EOL; diff --git a/src/agent/composer.json b/src/agent/composer.json index 51cccaa6..bcf60cba 100644 --- a/src/agent/composer.json +++ b/src/agent/composer.json @@ -37,14 +37,19 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11.5.13", "symfony/ai-store": "@dev", + "symfony/cache": "^6.4 || ^7.1", "symfony/css-selector": "^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1", - "symfony/event-dispatcher": "^6.4 || ^7.1" + "symfony/event-dispatcher": "^6.4 || ^7.1", + "symfony/http-foundation": "^6.4 || ^7.1" }, "suggest": { "mrmysql/youtube-transcript": "For using the YouTube transcription tool.", "symfony/ai-store": "For using Similarity Search with a vector store.", - "symfony/dom-crawler": "For using the Crawler tool." + "symfony/css-selector": "For using the YouTube transcription tool.", + "symfony/dom-crawler": "For using the Crawler tool.", + "symfony/http-foundation": "For using the SessionStore as message store.", + "psr/cache": "For using the CacheStore as message store." }, "config": { "sort-packages": true diff --git a/src/agent/src/Chat.php b/src/agent/src/Chat.php new file mode 100644 index 00000000..64b1c029 --- /dev/null +++ b/src/agent/src/Chat.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent; + +use Symfony\AI\Agent\Chat\MessageStoreInterface; +use Symfony\AI\Platform\Message\AssistantMessage; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBagInterface; +use Symfony\AI\Platform\Message\UserMessage; +use Symfony\AI\Platform\Response\TextResponse; + +final readonly class Chat implements ChatInterface +{ + public function __construct( + private AgentInterface $agent, + private MessageStoreInterface $store, + ) { + } + + public function initiate(MessageBagInterface $messages): void + { + $this->store->clear(); + $this->store->save($messages); + } + + public function submit(UserMessage $message): AssistantMessage + { + $messages = $this->store->load(); + + $messages->add($message); + $response = $this->agent->call($messages); + + \assert($response instanceof TextResponse); + + $assistantMessage = Message::ofAssistant($response->getContent()); + $messages->add($assistantMessage); + + $this->store->save($messages); + + return $assistantMessage; + } +} diff --git a/src/agent/src/Chat/MessageStore/CacheStore.php b/src/agent/src/Chat/MessageStore/CacheStore.php new file mode 100644 index 00000000..873aadad --- /dev/null +++ b/src/agent/src/Chat/MessageStore/CacheStore.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Chat\MessageStore; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\AI\Agent\Chat\MessageStoreInterface; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\AI\Platform\Message\MessageBagInterface; + +final readonly class CacheStore implements MessageStoreInterface +{ + public function __construct( + private CacheItemPoolInterface $cache, + private string $cacheKey, + private int $ttl = 86400, + ) { + } + + public function save(MessageBagInterface $messages): void + { + $item = $this->cache->getItem($this->cacheKey); + + $item->set($messages); + $item->expiresAfter($this->ttl); + + $this->cache->save($item); + } + + public function load(): MessageBag + { + $item = $this->cache->getItem($this->cacheKey); + + return $item->isHit() ? $item->get() : new MessageBag(); + } + + public function clear(): void + { + $this->cache->deleteItem($this->cacheKey); + } +} diff --git a/src/agent/src/Chat/MessageStore/InMemoryStore.php b/src/agent/src/Chat/MessageStore/InMemoryStore.php new file mode 100644 index 00000000..b3971e11 --- /dev/null +++ b/src/agent/src/Chat/MessageStore/InMemoryStore.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Chat\MessageStore; + +use Symfony\AI\Agent\Chat\MessageStoreInterface; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\AI\Platform\Message\MessageBagInterface; + +final class InMemoryStore implements MessageStoreInterface +{ + private MessageBagInterface $messages; + + public function save(MessageBagInterface $messages): void + { + $this->messages = $messages; + } + + public function load(): MessageBagInterface + { + return $this->messages ?? new MessageBag(); + } + + public function clear(): void + { + $this->messages = new MessageBag(); + } +} diff --git a/src/agent/src/Chat/MessageStore/SessionStore.php b/src/agent/src/Chat/MessageStore/SessionStore.php new file mode 100644 index 00000000..4b6bb858 --- /dev/null +++ b/src/agent/src/Chat/MessageStore/SessionStore.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Chat\MessageStore; + +use Symfony\AI\Agent\Chat\MessageStoreInterface; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\AI\Platform\Message\MessageBagInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +final readonly class SessionStore implements MessageStoreInterface +{ + private SessionInterface $session; + + public function __construct( + RequestStack $requestStack, + private string $sessionKey = 'messages', + ) { + $this->session = $requestStack->getSession(); + } + + public function save(MessageBagInterface $messages): void + { + $this->session->set($this->sessionKey, $messages); + } + + public function load(): MessageBagInterface + { + return $this->session->get($this->sessionKey, new MessageBag()); + } + + public function clear(): void + { + $this->session->remove($this->sessionKey); + } +} diff --git a/src/agent/src/Chat/MessageStoreInterface.php b/src/agent/src/Chat/MessageStoreInterface.php new file mode 100644 index 00000000..3ab745bf --- /dev/null +++ b/src/agent/src/Chat/MessageStoreInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent\Chat; + +use Symfony\AI\Platform\Message\MessageBagInterface; + +interface MessageStoreInterface +{ + public function save(MessageBagInterface $messages): void; + + public function load(): MessageBagInterface; + + public function clear(): void; +} diff --git a/src/agent/src/ChatInterface.php b/src/agent/src/ChatInterface.php new file mode 100644 index 00000000..d5ac25a4 --- /dev/null +++ b/src/agent/src/ChatInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Agent; + +use Symfony\AI\Platform\Message\AssistantMessage; +use Symfony\AI\Platform\Message\MessageBagInterface; +use Symfony\AI\Platform\Message\UserMessage; + +interface ChatInterface +{ + public function initiate(MessageBagInterface $messages): void; + + public function submit(UserMessage $message): AssistantMessage; +}