Skip to content

Commit 059db10

Browse files
committed
feature #117 [Agent] Add basic setup for memory injections to system prompt (DZunke)
This PR was merged into the main branch. Discussion ---------- [Agent] Add basic setup for memory injections to system prompt | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | | License | MIT See original contribution at php-llm/llm-chain#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? Commits ------- d9b13f9 Add basic setup for memory injections to system prompt
2 parents 54708b0 + d9b13f9 commit 059db10

11 files changed

+896
-0
lines changed

examples/misc/chat-with-memory.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;
14+
use Symfony\AI\Agent\Memory\MemoryInputProcessor;
15+
use Symfony\AI\Agent\Memory\StaticMemoryProvider;
16+
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
17+
use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory;
18+
use Symfony\AI\Platform\Message\Message;
19+
use Symfony\AI\Platform\Message\MessageBag;
20+
use Symfony\Component\Dotenv\Dotenv;
21+
22+
require_once dirname(__DIR__).'/vendor/autoload.php';
23+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
24+
25+
if (!$_ENV['OPENAI_API_KEY']) {
26+
echo 'Please set the OPENAI_API_KEY environment variable.'.\PHP_EOL;
27+
exit(1);
28+
}
29+
30+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
31+
$model = new GPT(GPT::GPT_4O_MINI);
32+
33+
$systemPromptProcessor = new SystemPromptInputProcessor('You are a professional trainer with short, personalized advices and a motivating claim.');
34+
35+
$personalFacts = new StaticMemoryProvider(
36+
'My name is Wilhelm Tell',
37+
'I wish to be a swiss national hero',
38+
'I am struggling with hitting apples but want to be professional with the bow and arrow',
39+
);
40+
$memoryProcessor = new MemoryInputProcessor($personalFacts);
41+
42+
$chain = new Agent($platform, $model, [$systemPromptProcessor, $memoryProcessor]);
43+
$messages = new MessageBag(Message::ofUser('What do we do today?'));
44+
$response = $chain->call($messages);
45+
46+
echo $response->getContent().\PHP_EOL;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Doctrine\DBAL\DriverManager;
13+
use Doctrine\DBAL\Tools\DsnParser;
14+
use Symfony\AI\Agent\Agent;
15+
use Symfony\AI\Agent\Memory\EmbeddingProvider;
16+
use Symfony\AI\Agent\Memory\MemoryInputProcessor;
17+
use Symfony\AI\Platform\Bridge\OpenAI\Embeddings;
18+
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
19+
use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory;
20+
use Symfony\AI\Platform\Message\Message;
21+
use Symfony\AI\Platform\Message\MessageBag;
22+
use Symfony\AI\Store\Bridge\MariaDB\Store;
23+
use Symfony\AI\Store\Document\Metadata;
24+
use Symfony\AI\Store\Document\TextDocument;
25+
use Symfony\AI\Store\Document\Vectorizer;
26+
use Symfony\AI\Store\Indexer;
27+
use Symfony\Component\Dotenv\Dotenv;
28+
use Symfony\Component\Uid\Uuid;
29+
30+
require_once dirname(__DIR__).'/vendor/autoload.php';
31+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
32+
33+
if (!$_ENV['OPENAI_API_KEY'] || !$_ENV['MARIADB_URI']) {
34+
echo 'Please set OPENAI_API_KEY and MARIADB_URI environment variables.'.\PHP_EOL;
35+
exit(1);
36+
}
37+
38+
// initialize the store
39+
$store = Store::fromDbal(
40+
connection: DriverManager::getConnection((new DsnParser())->parse($_ENV['MARIADB_URI'])),
41+
tableName: 'my_table',
42+
indexName: 'my_index',
43+
vectorFieldName: 'embedding',
44+
);
45+
46+
// our data
47+
$pastConversationPieces = [
48+
['role' => 'user', 'timestamp' => '2024-12-14 12:00:00', 'content' => 'My friends John and Emma are friends, too, are there hints why?'],
49+
['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!'],
50+
['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?'],
51+
['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.'],
52+
];
53+
54+
// create embeddings and documents
55+
foreach ($pastConversationPieces as $i => $message) {
56+
$documents[] = new TextDocument(
57+
id: Uuid::v4(),
58+
content: 'Role: '.$message['role'].\PHP_EOL.'Timestamp: '.$message['timestamp'].\PHP_EOL.'Message: '.$message['content'],
59+
metadata: new Metadata($message),
60+
);
61+
}
62+
63+
// initialize the table
64+
$store->initialize();
65+
66+
// create embeddings for documents as preparation of the chain memory
67+
$platform = PlatformFactory::create($_ENV['OPENAI_API_KEY']);
68+
$vectorizer = new Vectorizer($platform, $embeddings = new Embeddings());
69+
$indexer = new Indexer($vectorizer, $store);
70+
$indexer->index($documents);
71+
72+
// Execute a chat call that is utilizing the memory
73+
$embeddingsMemory = new EmbeddingProvider($platform, $embeddings, $store);
74+
$memoryProcessor = new MemoryInputProcessor($embeddingsMemory);
75+
76+
$chain = new Agent($platform, new GPT(GPT::GPT_4O_MINI), [$memoryProcessor]);
77+
$messages = new MessageBag(Message::ofUser('Have we discussed about my friend John in the past? If yes, what did we talk about?'));
78+
$response = $chain->call($messages);
79+
80+
echo $response->getContent().\PHP_EOL;

src/agent/doc/index.rst

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,84 @@ AgentAwareTrait::
465465
}
466466
}
467467

468+
Agent Memory Management
469+
-----------------------
470+
471+
Symfony AI supports adding contextual memory to agent conversations, allowing the model to recall past interactions or
472+
relevant information from different sources. Memory providers inject information into the system prompt, providing the
473+
model with context without changing your application logic.
474+
475+
Using Memory
476+
~~~~~~~~~~~~
477+
478+
Memory integration is handled through the ``MemoryInputProcessor`` and one or more ``MemoryProviderInterface`` implementations::
479+
480+
use Symfony\AI\Agent\Agent;
481+
use Symfony\AI\Agent\Memory\MemoryInputProcessor;
482+
use Symfony\AI\Agent\Memory\StaticMemoryProvider;
483+
use Symfony\AI\Platform\Message\Message;
484+
use Symfony\AI\Platform\Message\MessageBag;
485+
486+
// Platform & LLM instantiation
487+
488+
$personalFacts = new StaticMemoryProvider(
489+
'My name is Wilhelm Tell',
490+
'I wish to be a swiss national hero',
491+
'I am struggling with hitting apples but want to be professional with the bow and arrow',
492+
);
493+
$memoryProcessor = new MemoryInputProcessor($personalFacts);
494+
495+
$agent = new Agent($platform, $model, [$memoryProcessor]);
496+
$messages = new MessageBag(Message::ofUser('What do we do today?'));
497+
$response = $agent->call($messages);
498+
499+
Memory Providers
500+
~~~~~~~~~~~~~~~~
501+
502+
The library includes several memory provider implementations that are ready to use out of the box.
503+
504+
**Static Memory**
505+
506+
Static memory provides fixed information to the agent, such as user preferences, application context, or any other
507+
information that should be consistently available without being directly added to the system prompt::
508+
509+
use Symfony\AI\Agent\Memory\StaticMemoryProvider;
510+
511+
$staticMemory = new StaticMemoryProvider(
512+
'The user is allergic to nuts',
513+
'The user prefers brief explanations',
514+
);
515+
516+
**Embedding Provider**
517+
518+
This provider leverages vector storage to inject relevant knowledge based on the user's current message. It can be used
519+
for retrieving general knowledge from a store or recalling past conversation pieces that might be relevant::
520+
521+
use Symfony\AI\Agent\Memory\EmbeddingProvider;
522+
523+
$embeddingsMemory = new EmbeddingProvider(
524+
$platform,
525+
$embeddings, // Your embeddings model for vectorizing user messages
526+
$store // Your vector store to query for relevant context
527+
);
528+
529+
Dynamic Memory Control
530+
~~~~~~~~~~~~~~~~~~~~~~
531+
532+
Memory is globally configured for the agent, but you can selectively disable it for specific calls when needed. This is
533+
useful when certain interactions shouldn't be influenced by the memory context::
534+
535+
$response = $agent->call($messages, [
536+
'use_memory' => false, // Disable memory for this specific call
537+
]);
538+
539+
540+
**Code Examples**
541+
542+
* `Chat with static memory`_
543+
* `Chat with embedding search memory`_
544+
545+
468546
.. _`Platform Component`: https://github.com/symfony/ai-platform
469547
.. _`Brave Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/brave.php
470548
.. _`Clock Tool`: https://github.com/symfony/ai/blob/main/examples/toolbox/clock.php
@@ -479,3 +557,5 @@ AgentAwareTrait::
479557
.. _`RAG with Pinecone`: https://github.com/symfony/ai/blob/main/examples/store/pinecone-similarity-search.php
480558
.. _`Structured Output with PHP class`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-math.php
481559
.. _`Structured Output with array`: https://github.com/symfony/ai/blob/main/examples/openai/structured-output-clock.php
560+
.. _`Chat with static memory`: https://github.com/symfony/ai/blob/main/examples/misc/chat-with-memory.php
561+
.. _`Chat with embedding search memory`: https://github.com/symfony/ai/blob/main/examples/store/mariadb-chat-memory.php
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Agent\Memory;
13+
14+
use Symfony\AI\Agent\Input;
15+
use Symfony\AI\Platform\Message\Content\ContentInterface;
16+
use Symfony\AI\Platform\Message\Content\Text;
17+
use Symfony\AI\Platform\Message\MessageInterface;
18+
use Symfony\AI\Platform\Message\UserMessage;
19+
use Symfony\AI\Platform\Model;
20+
use Symfony\AI\Platform\PlatformInterface;
21+
use Symfony\AI\Store\VectorStoreInterface;
22+
23+
/**
24+
* @author Denis Zunke <[email protected]>
25+
*/
26+
final readonly class EmbeddingProvider implements MemoryProviderInterface
27+
{
28+
public function __construct(
29+
private PlatformInterface $platform,
30+
private Model $model,
31+
private VectorStoreInterface $vectorStore,
32+
) {
33+
}
34+
35+
public function loadMemory(Input $input): array
36+
{
37+
$messages = $input->messages->getMessages();
38+
/** @var MessageInterface|null $userMessage */
39+
$userMessage = $messages[array_key_last($messages)] ?? null;
40+
41+
if (!$userMessage instanceof UserMessage) {
42+
return [];
43+
}
44+
45+
$userMessageTextContent = array_filter(
46+
$userMessage->content,
47+
static fn (ContentInterface $content): bool => $content instanceof Text,
48+
);
49+
50+
if (0 === \count($userMessageTextContent)) {
51+
return [];
52+
}
53+
54+
$userMessageTextContent = array_shift($userMessageTextContent);
55+
56+
$vectors = $this->platform->request($this->model, $userMessageTextContent->text)->asVectors();
57+
$foundEmbeddingContent = $this->vectorStore->query($vectors[0]);
58+
if (0 === \count($foundEmbeddingContent)) {
59+
return [];
60+
}
61+
62+
$content = '## Dynamic memories fitting user message'.\PHP_EOL.\PHP_EOL;
63+
foreach ($foundEmbeddingContent as $document) {
64+
$content .= json_encode($document->metadata);
65+
}
66+
67+
return [new Memory($content)];
68+
}
69+
}

src/agent/src/Memory/Memory.php

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

0 commit comments

Comments
 (0)