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

Commit 5b9e952

Browse files
DZunkechr-hertel
andauthored
feat: add basic setup for memory injections to system prompt (#387)
This PR introduces a flexible memory system that allows the LLM to recall contextual information that are permantent for the conversation. In difference to tools it can be always utilized, when `use_memory` option is not disabled. It would be possible to fetch memory by a tool with a system instruction like `always call the tool foo_bar` but, for me, this feels like a bad design to always force the model to do a tool call without further need. Currently i have added just two memory providers to show what my idea is. I could also think about a write layer to fill a memory, together with a read layer, this could, for example, be working good with a graph database and tools for memory handling. But these are ideas for the future. So for this first throw i hope you get what i was thinking about to reach. I decided to inject the memory to the system prompt, when it is available, instead of adding a second system prompt to the message bag. But this was just a 50/50 thinking. I tried both seem to be working equally, at least for open ai. I am open to change it back again. What do you think? --------- Co-authored-by: Christopher Hertel <[email protected]>
1 parent 351af37 commit 5b9e952

File tree

11 files changed

+822
-4
lines changed

11 files changed

+822
-4
lines changed

README.md

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory;
5353
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
5454

5555
// Language Model: GPT (OpenAI)
56-
$llm = new GPT(GPT::GPT_4O_MINI);
56+
$llm = new GPT(GPT::GPT_4O_MINI);
5757

5858
// Embeddings Model: Embeddings (OpenAI)
5959
$embeddings = new Embeddings();
@@ -268,7 +268,7 @@ final class MyTool
268268
public function __invoke(
269269
#[With(pattern: '/([a-z0-1]){5}/')]
270270
string $name,
271-
#[With(minimum: 0, maximum: 10)]
271+
#[With(minimum: 0, maximum: 10)]
272272
int $number,
273273
): string {
274274
// ...
@@ -501,7 +501,7 @@ $response = $chain->call($messages);
501501
* [MongoDB Atlas Search](https://mongodb.com/products/platform/atlas-vector-search) (requires `mongodb/mongodb` as additional dependency)
502502
* [Pinecone](https://pinecone.io) (requires `probots-io/pinecone-php` as additional dependency)
503503

504-
See [issue #28](https://github.com/php-llm/llm-chain/issues/28) for planned support of other models and platforms.
504+
See [issue #28](https://github.com/php-llm/llm-chain/issues/28) for planned support of other models and platforms.
505505

506506
## Advanced Usage & Features
507507

@@ -749,7 +749,7 @@ final class MyProcessor implements InputProcessorInterface
749749
$options = $input->getOptions();
750750
$options['foo'] = 'bar';
751751
$input->setOptions($options);
752-
752+
753753
// mutate MessageBag
754754
$input->messages->append(new AssistantMessage(sprintf('Please answer using the locale %s', $this->locale)));
755755
}
@@ -801,6 +801,81 @@ final class MyProcessor implements OutputProcessorInterface, ChainAwareInterface
801801
}
802802
```
803803

804+
805+
## Memory
806+
807+
LLM Chain supports adding contextual memory to your conversations, which allows the model to recall past interactions or relevant information from different sources. Memory providers inject information into the system prompt, providing the model with context without changing your application logic.
808+
809+
### Using Memory
810+
811+
Memory integration is handled through the `MemoryInputProcessor` and one or more `MemoryProviderInterface` implementations. Here's how to set it up:
812+
813+
```php
814+
<?php
815+
use PhpLlm\LlmChain\Chain\Chain;
816+
use PhpLlm\LlmChain\Chain\Memory\MemoryInputProcessor;
817+
use PhpLlm\LlmChain\Chain\Memory\StaticMemoryProvider;
818+
819+
// Platform & LLM instantiation
820+
821+
$personalFacts = new StaticMemoryProvider(
822+
'My name is Wilhelm Tell',
823+
'I wish to be a swiss national hero',
824+
'I am struggling with hitting apples but want to be professional with the bow and arrow',
825+
);
826+
$memoryProcessor = new MemoryInputProcessor($personalFacts);
827+
828+
$chain = new Chain($platform, $model, [$memoryProcessor]);
829+
$messages = new MessageBag(Message::ofUser('What do we do today?'));
830+
$response = $chain->call($messages);
831+
```
832+
833+
### Memory Providers
834+
835+
The library includes some implementations that are usable out of the box.
836+
837+
#### Static Memory
838+
839+
The static memory can be utilized to provide static information form, for example, user settings, basic knowledge of your application
840+
or any other thing that should be remembered als always there without the need of having it statically added to the system prompt by
841+
yourself.
842+
843+
```php
844+
use PhpLlm\LlmChain\Chain\Memory\StaticMemoryProvider;
845+
846+
$staticMemory = new StaticMemoryProvider(
847+
'The user is allergic to nuts',
848+
'The user prefers brief explanations',
849+
);
850+
```
851+
852+
#### Embedding Provider
853+
854+
Based on an embedding storage the given user message is utilized to inject knowledge from the storage. This could be general knowledge that was stored there and could fit the users input without the need for tools or past conversation pieces that should be recalled for
855+
the current message bag.
856+
857+
```php
858+
use PhpLlm\LlmChain\Chain\Memory\EmbeddingProvider;
859+
860+
$embeddingsMemory = new EmbeddingProvider(
861+
$platform,
862+
$embeddings, // Your embeddings model to use for vectorizing the users message
863+
$store // Your vector store to query for fitting context
864+
);
865+
866+
```
867+
868+
### Dynamically Memory Usage
869+
870+
The memory configuration is globally given for the chain. Sometimes there is the need to explicit disable the memory when it is not needed for some calls or calls are not in the wanted context for a call. So there is the option `use_memory` that is enabled by default but can be disabled on premise.
871+
872+
```php
873+
$response = $chain->call($messages, [
874+
'use_memory' => false,
875+
]);
876+
```
877+
878+
804879
## HuggingFace
805880

806881
LLM Chain comes out of the box with an integration for [HuggingFace](https://huggingface.co/) which is a platform for

examples/chat-with-memory.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain\Chain;
4+
use PhpLlm\LlmChain\Chain\InputProcessor\SystemPromptInputProcessor;
5+
use PhpLlm\LlmChain\Chain\Memory\MemoryInputProcessor;
6+
use PhpLlm\LlmChain\Chain\Memory\StaticMemoryProvider;
7+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
8+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory;
9+
use PhpLlm\LlmChain\Platform\Message\Message;
10+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
11+
use Symfony\Component\Dotenv\Dotenv;
12+
13+
require_once dirname(__DIR__).'/vendor/autoload.php';
14+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
15+
16+
if (!$_ENV['OPENAI_API_KEY']) {
17+
echo 'Please set the OPENAI_API_KEY environment variable.'.\PHP_EOL;
18+
exit(1);
19+
}
20+
21+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
22+
$model = new GPT(GPT::GPT_4O_MINI);
23+
24+
$systemPromptProcessor = new SystemPromptInputProcessor('You are a professional trainer with short, personalized advices and a motivating claim.');
25+
26+
$personalFacts = new StaticMemoryProvider(
27+
'My name is Wilhelm Tell',
28+
'I wish to be a swiss national hero',
29+
'I am struggling with hitting apples but want to be professional with the bow and arrow',
30+
);
31+
$memoryProcessor = new MemoryInputProcessor($personalFacts);
32+
33+
$chain = new Chain($platform, $model, [$systemPromptProcessor, $memoryProcessor]);
34+
$messages = new MessageBag(Message::ofUser('What do we do today?'));
35+
$response = $chain->call($messages);
36+
37+
echo $response->getContent().\PHP_EOL;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
use Doctrine\DBAL\DriverManager;
4+
use Doctrine\DBAL\Tools\DsnParser;
5+
use PhpLlm\LlmChain\Chain\Chain;
6+
use PhpLlm\LlmChain\Chain\Memory\EmbeddingProvider;
7+
use PhpLlm\LlmChain\Chain\Memory\MemoryInputProcessor;
8+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings;
9+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
10+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory;
11+
use PhpLlm\LlmChain\Platform\Message\Message;
12+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
13+
use PhpLlm\LlmChain\Store\Bridge\MariaDB\Store;
14+
use PhpLlm\LlmChain\Store\Document\Metadata;
15+
use PhpLlm\LlmChain\Store\Document\TextDocument;
16+
use PhpLlm\LlmChain\Store\Document\Vectorizer;
17+
use PhpLlm\LlmChain\Store\Indexer;
18+
use Symfony\Component\Dotenv\Dotenv;
19+
use Symfony\Component\Uid\Uuid;
20+
21+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
22+
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
23+
24+
if (!$_ENV['OPENAI_API_KEY'] || !$_ENV['MARIADB_URI']) {
25+
echo 'Please set OPENAI_API_KEY and MARIADB_URI environment variables.'.\PHP_EOL;
26+
exit(1);
27+
}
28+
29+
// initialize the store
30+
$store = Store::fromDbal(
31+
connection: DriverManager::getConnection((new DsnParser())->parse($_ENV['MARIADB_URI'])),
32+
tableName: 'my_table',
33+
indexName: 'my_index',
34+
vectorFieldName: 'embedding',
35+
);
36+
37+
// our data
38+
$pastConversationPieces = [
39+
['role' => 'user', 'timestamp' => '2024-12-14 12:00:00', 'content' => 'My friends John and Emma are friends, too, are there hints why?'],
40+
['role' => 'assistant', 'timestamp' => '2024-12-14 12:00:01', 'content' => 'Based on the found documents i would expect they are friends since childhood, this can give a deep bound!'],
41+
['role' => 'user', 'timestamp' => '2024-12-14 12:02:02', 'content' => 'Yeah but how does this bound? I know John was once there with a wound dressing as Emma fell, could this be a hint?'],
42+
['role' => 'assistant', 'timestamp' => '2024-12-14 12:02:03', 'content' => 'Yes, this could be a hint that they have been through difficult times together, which can strengthen their bond.'],
43+
];
44+
45+
// create embeddings and documents
46+
foreach ($pastConversationPieces as $i => $message) {
47+
$documents[] = new TextDocument(
48+
id: Uuid::v4(),
49+
content: 'Role: '.$message['role'].\PHP_EOL.'Timestamp: '.$message['timestamp'].\PHP_EOL.'Message: '.$message['content'],
50+
metadata: new Metadata($message),
51+
);
52+
}
53+
54+
// initialize the table
55+
$store->initialize();
56+
57+
// create embeddings for documents as preparation of the chain memory
58+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
59+
$vectorizer = new Vectorizer($platform, $embeddings = new Embeddings());
60+
$indexer = new Indexer($vectorizer, $store);
61+
$indexer->index($documents);
62+
63+
// Execute a chat call that is utilizing the memory
64+
$embeddingsMemory = new EmbeddingProvider($platform, $embeddings, $store);
65+
$memoryProcessor = new MemoryInputProcessor($embeddingsMemory);
66+
67+
$chain = new Chain($platform, new GPT(GPT::GPT_4O_MINI), [$memoryProcessor]);
68+
$messages = new MessageBag(Message::ofUser('Have we discussed about my friend John in the past? If yes, what did we talk about?'));
69+
$response = $chain->call($messages);
70+
71+
echo $response->getContent().\PHP_EOL;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\Memory;
6+
7+
use PhpLlm\LlmChain\Chain\Input;
8+
use PhpLlm\LlmChain\Platform\Message\Content\ContentInterface;
9+
use PhpLlm\LlmChain\Platform\Message\Content\Text;
10+
use PhpLlm\LlmChain\Platform\Message\MessageInterface;
11+
use PhpLlm\LlmChain\Platform\Message\UserMessage;
12+
use PhpLlm\LlmChain\Platform\Model;
13+
use PhpLlm\LlmChain\Platform\PlatformInterface;
14+
use PhpLlm\LlmChain\Store\VectorStoreInterface;
15+
16+
/**
17+
* @author Denis Zunke <[email protected]>
18+
*/
19+
final readonly class EmbeddingProvider implements MemoryProviderInterface
20+
{
21+
public function __construct(
22+
private PlatformInterface $platform,
23+
private Model $model,
24+
private VectorStoreInterface $vectorStore,
25+
) {
26+
}
27+
28+
public function loadMemory(Input $input): array
29+
{
30+
$messages = $input->messages->getMessages();
31+
/** @var MessageInterface|null $userMessage */
32+
$userMessage = $messages[array_key_last($messages)] ?? null;
33+
34+
if (!$userMessage instanceof UserMessage) {
35+
return [];
36+
}
37+
38+
$userMessageTextContent = array_filter(
39+
$userMessage->content,
40+
static fn (ContentInterface $content): bool => $content instanceof Text,
41+
);
42+
43+
if (0 === \count($userMessageTextContent)) {
44+
return [];
45+
}
46+
47+
$userMessageTextContent = array_shift($userMessageTextContent);
48+
\assert($userMessageTextContent instanceof Text);
49+
50+
$vectors = $this->platform->request($this->model, $userMessageTextContent->text)->asVectors();
51+
$foundEmbeddingContent = $this->vectorStore->query($vectors[0]);
52+
if (0 === \count($foundEmbeddingContent)) {
53+
return [];
54+
}
55+
56+
$content = '## Dynamic memories fitting user message'.\PHP_EOL.\PHP_EOL;
57+
foreach ($foundEmbeddingContent as $document) {
58+
$content .= json_encode($document->metadata);
59+
}
60+
61+
return [new Memory($content)];
62+
}
63+
}

src/Chain/Memory/Memory.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\Memory;
6+
7+
/**
8+
* @author Denis Zunke <[email protected]>
9+
*/
10+
final readonly class Memory
11+
{
12+
public function __construct(public string $content)
13+
{
14+
}
15+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Chain\Memory;
6+
7+
use PhpLlm\LlmChain\Chain\Input;
8+
use PhpLlm\LlmChain\Chain\InputProcessorInterface;
9+
use PhpLlm\LlmChain\Platform\Message\Message;
10+
11+
/**
12+
* @author Denis Zunke <[email protected]>
13+
*/
14+
final readonly class MemoryInputProcessor implements InputProcessorInterface
15+
{
16+
private const MEMORY_PROMPT_MESSAGE = <<<MARKDOWN
17+
# Conversation Memory
18+
This is the memory I have found for this conversation. The memory has more weight to answer user input,
19+
so try to answer utilizing the memory as much as possible. Your answer must be changed to fit the given
20+
memory. If the memory is irrelevant, ignore it. Do not reply to the this section of the prompt and do not
21+
reference it as this is just for your reference.
22+
MARKDOWN;
23+
24+
/**
25+
* @var MemoryProviderInterface[]
26+
*/
27+
private array $memoryProviders;
28+
29+
public function __construct(
30+
MemoryProviderInterface ...$memoryProviders,
31+
) {
32+
$this->memoryProviders = $memoryProviders;
33+
}
34+
35+
public function processInput(Input $input): void
36+
{
37+
$options = $input->getOptions();
38+
$useMemory = $options['use_memory'] ?? true;
39+
unset($options['use_memory']);
40+
$input->setOptions($options);
41+
42+
if (false === $useMemory || 0 === \count($this->memoryProviders)) {
43+
return;
44+
}
45+
46+
$memory = '';
47+
foreach ($this->memoryProviders as $provider) {
48+
$memoryMessages = $provider->loadMemory($input);
49+
50+
if (0 === \count($memoryMessages)) {
51+
continue;
52+
}
53+
54+
$memory .= \PHP_EOL.\PHP_EOL;
55+
$memory .= implode(
56+
\PHP_EOL,
57+
array_map(static fn (Memory $memory): string => $memory->content, $memoryMessages),
58+
);
59+
}
60+
61+
if ('' === $memory) {
62+
return;
63+
}
64+
65+
$systemMessage = $input->messages->getSystemMessage()->content ?? '';
66+
if ('' !== $systemMessage) {
67+
$systemMessage .= \PHP_EOL.\PHP_EOL;
68+
}
69+
70+
$messages = $input->messages
71+
->withoutSystemMessage()
72+
->prepend(Message::forSystem($systemMessage.self::MEMORY_PROMPT_MESSAGE.$memory));
73+
74+
$input->messages = $messages;
75+
}
76+
}

0 commit comments

Comments
 (0)