From a1331120c8b98d90aa14abceed0e940ff6a8865a Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Thu, 6 Feb 2025 22:44:51 +0100 Subject: [PATCH] feat: chat + message store --- composer.json | 7 ++-- examples/persistent-chat.php | 34 +++++++++++++++++++ src/Chat.php | 44 +++++++++++++++++++++++++ src/Chat/MessageStore/CacheStore.php | 41 +++++++++++++++++++++++ src/Chat/MessageStore/InMemoryStore.php | 28 ++++++++++++++++ src/Chat/MessageStore/SessionStore.php | 37 +++++++++++++++++++++ src/Chat/MessageStoreInterface.php | 16 +++++++++ src/ChatInterface.php | 16 +++++++++ 8 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 examples/persistent-chat.php create mode 100644 src/Chat.php create mode 100644 src/Chat/MessageStore/CacheStore.php create mode 100644 src/Chat/MessageStore/InMemoryStore.php create mode 100644 src/Chat/MessageStore/SessionStore.php create mode 100644 src/Chat/MessageStoreInterface.php create mode 100644 src/ChatInterface.php diff --git a/composer.json b/composer.json index defefc5..8eb20a7 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ "php": ">=8.2", "oskarstark/enum-helper": "^1.5", "phpdocumentor/reflection-docblock": "^5.4", - "psr/cache": "^3.0", "psr/log": "^3.0", "symfony/clock": "^6.4 || ^7.1", "symfony/http-client": "^6.4 || ^7.1", @@ -33,12 +32,14 @@ "phpunit/phpunit": "^11.3", "probots-io/pinecone-php": "^1.0", "rector/rector": "^1.2", + "symfony/cache": "^6.4 || ^7.1", "symfony/console": "^6.4 || ^7.1", "symfony/css-selector": "^6.4 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.1", "symfony/dotenv": "^6.4 || ^7.1", "symfony/event-dispatcher": "^6.4 || ^7.1", "symfony/finder": "^6.4 || ^7.1", + "symfony/http-foundation": "^6.4 || ^7.1", "symfony/process": "^6.4 || ^7.1", "symfony/var-dumper": "^6.4 || ^7.1" }, @@ -50,7 +51,9 @@ "mongodb/mongodb": "For using MongoDB Atlas as retrieval vector store.", "probots-io/pinecone-php": "For using the Pinecone as retrieval vector store.", "symfony/css-selector": "For using the YouTube transcription tool.", - "symfony/dom-crawler": "For using the YouTube transcription tool." + "symfony/dom-crawler": "For using the YouTube transcription tool.", + "symfony/http-foundation": "For using the SessionStore as message store.", + "psr/cache": "For using the CacheStore as message store." }, "autoload": { "psr-4": { diff --git a/examples/persistent-chat.php b/examples/persistent-chat.php new file mode 100644 index 0000000..e572fe5 --- /dev/null +++ b/examples/persistent-chat.php @@ -0,0 +1,34 @@ +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); + +$chain = new Chain($platform, $llm); +$chat = new Chat($chain, new InMemoryStore()); + +$messages = new MessageBag( + Message::forSystem('You are a helpful assistant. You only answer with short sentences.'), +); + +$chat->start($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/Chat.php b/src/Chat.php new file mode 100644 index 0000000..4ca001f --- /dev/null +++ b/src/Chat.php @@ -0,0 +1,44 @@ +store->clear(); + $this->store->save($messages); + } + + public function submit(UserMessage $message): AssistantMessage + { + $messages = $this->store->load(); + + $messages->add($message); + $response = $this->chain->call($messages); + + assert($response instanceof TextResponse); + + $assistantMessage = Message::ofAssistant($response->getContent()); + $messages->add($assistantMessage); + + $this->store->save($messages); + + return $assistantMessage; + } +} diff --git a/src/Chat/MessageStore/CacheStore.php b/src/Chat/MessageStore/CacheStore.php new file mode 100644 index 0000000..ca20027 --- /dev/null +++ b/src/Chat/MessageStore/CacheStore.php @@ -0,0 +1,41 @@ +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/Chat/MessageStore/InMemoryStore.php b/src/Chat/MessageStore/InMemoryStore.php new file mode 100644 index 0000000..cbb556c --- /dev/null +++ b/src/Chat/MessageStore/InMemoryStore.php @@ -0,0 +1,28 @@ +messages = $messages; + } + + public function load(): MessageBag + { + return $this->messages ?? new MessageBag(); + } + + public function clear(): void + { + $this->messages = new MessageBag(); + } +} diff --git a/src/Chat/MessageStore/SessionStore.php b/src/Chat/MessageStore/SessionStore.php new file mode 100644 index 0000000..2a446f1 --- /dev/null +++ b/src/Chat/MessageStore/SessionStore.php @@ -0,0 +1,37 @@ +session = $requestStack->getSession(); + } + + public function save(MessageBag $messages): void + { + $this->session->set($this->sessionKey, $messages); + } + + public function load(): MessageBag + { + return $this->session->get($this->sessionKey, new MessageBag()); + } + + public function clear(): void + { + $this->session->remove($this->sessionKey); + } +} diff --git a/src/Chat/MessageStoreInterface.php b/src/Chat/MessageStoreInterface.php new file mode 100644 index 0000000..45433ae --- /dev/null +++ b/src/Chat/MessageStoreInterface.php @@ -0,0 +1,16 @@ +