Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

feat: chat + message store #208

Merged
merged 1 commit into from
Jun 30, 2025
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
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"oskarstark/enum-helper": "^1.5",
"phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^2.1",
"psr/cache": "^3.0",
"psr/log": "^3.0",
"symfony/clock": "^6.4 || ^7.1",
"symfony/http-client": "^6.4 || ^7.1",
Expand Down Expand Up @@ -53,12 +52,14 @@
"probots-io/pinecone-php": "^1.0",
"psr/http-factory-implementation": "*",
"rector/rector": "^2.0",
"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"
},
Expand All @@ -71,7 +72,9 @@
"mongodb/mongodb": "For using MongoDB Atlas as retrieval vector store.",
"mrmysql/youtube-transcript": "For using the YouTube transcription tool.",
"probots-io/pinecone-php": "For using the Pinecone as retrieval vector store.",
"symfony/dom-crawler": "For using the Crawler 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": {
"allow-plugins": {
Expand Down
34 changes: 34 additions & 0 deletions examples/persistent-chat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use PhpLlm\LlmChain\Chain\Chain;
use PhpLlm\LlmChain\Chain\Chat;
use PhpLlm\LlmChain\Chain\Chat\MessageStore\InMemoryStore;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory;
use PhpLlm\LlmChain\Platform\Message\Message;
use PhpLlm\LlmChain\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);

$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->initiate($messages);
$chat->submit(Message::ofUser('My name is Christopher.'));
$message = $chat->submit(Message::ofUser('What is my name?'));

echo $message->content.\PHP_EOL;
44 changes: 44 additions & 0 deletions src/Chain/Chat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

use PhpLlm\LlmChain\Chain\Chat\MessageStoreInterface;
use PhpLlm\LlmChain\Platform\Message\AssistantMessage;
use PhpLlm\LlmChain\Platform\Message\Message;
use PhpLlm\LlmChain\Platform\Message\MessageBagInterface;
use PhpLlm\LlmChain\Platform\Message\UserMessage;
use PhpLlm\LlmChain\Platform\Response\TextResponse;

final readonly class Chat implements ChatInterface
{
public function __construct(
private ChainInterface $chain,
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->chain->call($messages);

\assert($response instanceof TextResponse);

$assistantMessage = Message::ofAssistant($response->getContent());
$messages->add($assistantMessage);

$this->store->save($messages);

return $assistantMessage;
}
}
42 changes: 42 additions & 0 deletions src/Chain/Chat/MessageStore/CacheStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\Chat\MessageStore;

use PhpLlm\LlmChain\Chain\Chat\MessageStoreInterface;
use PhpLlm\LlmChain\Platform\Message\MessageBag;
use PhpLlm\LlmChain\Platform\Message\MessageBagInterface;
use Psr\Cache\CacheItemPoolInterface;

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);
}
}
29 changes: 29 additions & 0 deletions src/Chain/Chat/MessageStore/InMemoryStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\Chat\MessageStore;

use PhpLlm\LlmChain\Chain\Chat\MessageStoreInterface;
use PhpLlm\LlmChain\Platform\Message\MessageBag;
use PhpLlm\LlmChain\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();
}
}
38 changes: 38 additions & 0 deletions src/Chain/Chat/MessageStore/SessionStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\Chat\MessageStore;

use PhpLlm\LlmChain\Chain\Chat\MessageStoreInterface;
use PhpLlm\LlmChain\Platform\Message\MessageBag;
use PhpLlm\LlmChain\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);
}
}
16 changes: 16 additions & 0 deletions src/Chain/Chat/MessageStoreInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain\Chat;

use PhpLlm\LlmChain\Platform\Message\MessageBagInterface;

interface MessageStoreInterface
{
public function save(MessageBagInterface $messages): void;

public function load(): MessageBagInterface;

public function clear(): void;
}
16 changes: 16 additions & 0 deletions src/Chain/ChatInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Chain;

use PhpLlm\LlmChain\Platform\Message\AssistantMessage;
use PhpLlm\LlmChain\Platform\Message\MessageBagInterface;
use PhpLlm\LlmChain\Platform\Message\UserMessage;

interface ChatInterface
{
public function initiate(MessageBagInterface $messages): void;

public function submit(UserMessage $message): AssistantMessage;
}