Skip to content

Commit e558600

Browse files
committed
feat(platform): meilisearch message bag
1 parent c867b03 commit e558600

File tree

5 files changed

+560
-0
lines changed

5 files changed

+560
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 Bridge\Meilisearch\MessageStore;
13+
use Symfony\AI\Agent\Agent;
14+
use Symfony\AI\Agent\Chat;
15+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
16+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
17+
use Symfony\AI\Platform\Message\Message;
18+
use Symfony\AI\Platform\Message\MessageBag;
19+
20+
require_once dirname(__DIR__).'/bootstrap.php';
21+
22+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
23+
$llm = new Gpt(Gpt::GPT_4O_MINI);
24+
25+
$agent = new Agent($platform, $llm, logger: logger());
26+
$store = new MessageStore(
27+
http_client(),
28+
env('MEILISEARCH_HOST'),
29+
env('MEILISEARCH_API_KEY'),
30+
'chat',
31+
);
32+
$store->initialize();
33+
34+
$chat = new Chat($agent, $store);
35+
36+
$messages = new MessageBag(
37+
Message::forSystem('You are a helpful assistant. You only answer with short sentences.'),
38+
);
39+
40+
$chat->initiate($messages);
41+
$chat->submit(Message::ofUser('My name is Christopher.'));
42+
$message = $chat->submit(Message::ofUser('What is my name?'));
43+
44+
echo $message->content.\PHP_EOL;

examples/misc/persistent-chat-memory.php

Whitespace-only changes.
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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\Chat\Bridge\Meilisearch;
13+
14+
use Symfony\AI\Agent\Chat\MessageStoreInterface;
15+
use Symfony\AI\Agent\Exception\InvalidArgumentException;
16+
use Symfony\AI\Agent\Exception\LogicException;
17+
use Symfony\AI\Chat\ManagedStoreInterface;
18+
use Symfony\AI\Platform\Message\AssistantMessage;
19+
use Symfony\AI\Platform\Message\Content\Audio;
20+
use Symfony\AI\Platform\Message\Content\ContentInterface;
21+
use Symfony\AI\Platform\Message\Content\DocumentUrl;
22+
use Symfony\AI\Platform\Message\Content\File;
23+
use Symfony\AI\Platform\Message\Content\Image;
24+
use Symfony\AI\Platform\Message\Content\ImageUrl;
25+
use Symfony\AI\Platform\Message\Content\Text;
26+
use Symfony\AI\Platform\Message\MessageBag;
27+
use Symfony\AI\Platform\Message\MessageInterface;
28+
use Symfony\AI\Platform\Message\SystemMessage;
29+
use Symfony\AI\Platform\Message\ToolCallMessage;
30+
use Symfony\AI\Platform\Message\UserMessage;
31+
use Symfony\AI\Platform\Result\ToolCall;
32+
use Symfony\Contracts\HttpClient\HttpClientInterface;
33+
34+
/**
35+
* @author Guillaume Loulier <[email protected]>
36+
*/
37+
final readonly class MessageStore implements ManagedStoreInterface, MessageStoreInterface
38+
{
39+
public function __construct(
40+
private HttpClientInterface $httpClient,
41+
private string $endpointUrl,
42+
#[\SensitiveParameter] private string $apiKey,
43+
private string $indexName,
44+
) {
45+
}
46+
47+
public function save(MessageBag $messages): void
48+
{
49+
$messages = $messages->getMessages();
50+
51+
$this->request('PUT', \sprintf('indexes/%s/documents', $this->indexName), array_map(
52+
$this->convertToIndexableArray(...),
53+
$messages,
54+
));
55+
}
56+
57+
public function load(): MessageBag
58+
{
59+
$messages = $this->request('POST', \sprintf('indexes/%s/documents/fetch', $this->indexName));
60+
61+
return new MessageBag(...array_map($this->convertToMessage(...), $messages['results']));
62+
}
63+
64+
public function clear(): void
65+
{
66+
$this->request('DELETE', \sprintf('indexes/%s/documents', $this->indexName));
67+
}
68+
69+
public function setup(array $options = []): void
70+
{
71+
if ([] !== $options) {
72+
throw new InvalidArgumentException('No supported options.');
73+
}
74+
75+
$this->request('POST', 'indexes', [
76+
'uid' => $this->indexName,
77+
'primaryKey' => 'id',
78+
]);
79+
}
80+
81+
public function drop(): void
82+
{
83+
$this->request('DELETE', \sprintf('indexes/%s', $this->indexName));
84+
}
85+
86+
/**
87+
* @param array<string, mixed>|list<array<string, mixed>> $payload
88+
*
89+
* @return array<string, mixed>
90+
*/
91+
private function request(string $method, string $endpoint, array $payload = []): array
92+
{
93+
$url = \sprintf('%s/%s', $this->endpointUrl, $endpoint);
94+
$result = $this->httpClient->request($method, $url, [
95+
'headers' => [
96+
'Authorization' => \sprintf('Bearer %s', $this->apiKey),
97+
],
98+
'json' => [] !== $payload ? $payload : new \stdClass(),
99+
]);
100+
101+
return $result->toArray();
102+
}
103+
104+
/**
105+
* @return array<string, mixed>
106+
*/
107+
private function convertToIndexableArray(MessageInterface $message): array
108+
{
109+
$toolsCalls = [];
110+
111+
if ($message instanceof AssistantMessage && $message->hasToolCalls()) {
112+
$toolsCalls = array_map(
113+
static fn (ToolCall $toolCall): array => $toolCall->jsonSerialize(),
114+
$message->toolCalls,
115+
);
116+
}
117+
118+
if ($message instanceof ToolCallMessage) {
119+
$toolsCalls = $message->toolCall->jsonSerialize();
120+
}
121+
122+
return [
123+
'id' => $message->getId()->toRfc4122(),
124+
'type' => $message::class,
125+
'content' => ($message instanceof SystemMessage || $message instanceof AssistantMessage || $message instanceof ToolCallMessage) ? $message->content : '',
126+
'contentAsBase64' => ($message instanceof UserMessage && [] !== $message->content) ? array_map(
127+
static fn (ContentInterface $content) => [
128+
'type' => $content::class,
129+
'content' => match ($content::class) {
130+
Text::class => $content->text,
131+
File::class,
132+
Image::class,
133+
Audio::class => $content->asBase64(),
134+
ImageUrl::class,
135+
DocumentUrl::class => $content->url,
136+
default => throw new LogicException(\sprintf('Unknown content type "%s".', $content::class)),
137+
},
138+
],
139+
$message->content,
140+
) : [],
141+
'toolsCalls' => $toolsCalls,
142+
];
143+
}
144+
145+
/**
146+
* @param array<string, mixed> $payload
147+
*/
148+
private function convertToMessage(array $payload): MessageInterface
149+
{
150+
$type = $payload['type'];
151+
$content = $payload['content'] ?? '';
152+
$contentAsBase64 = $payload['contentAsBase64'] ?? [];
153+
154+
return match ($type) {
155+
SystemMessage::class => new SystemMessage($content),
156+
AssistantMessage::class => new AssistantMessage($content, array_map(
157+
static fn (array $toolsCall): ToolCall => new ToolCall(
158+
$toolsCall['id'],
159+
$toolsCall['function']['name'],
160+
json_decode($toolsCall['function']['arguments'], true)
161+
),
162+
$payload['toolsCalls'],
163+
)),
164+
UserMessage::class => new UserMessage(...array_map(
165+
static fn (array $contentAsBase64) => \in_array($contentAsBase64['type'], [File::class, Image::class, Audio::class], true)
166+
? $contentAsBase64['type']::fromDataUrl($contentAsBase64['content'])
167+
: new $contentAsBase64['type']($contentAsBase64['content']),
168+
$contentAsBase64,
169+
)),
170+
ToolCallMessage::class => new ToolCallMessage(
171+
new ToolCall(
172+
$payload['toolsCalls']['id'],
173+
$payload['toolsCalls']['function']['name'],
174+
json_decode($payload['toolsCalls']['function']['arguments'], true)
175+
),
176+
$content
177+
),
178+
default => throw new LogicException(\sprintf('Unknown message type "%s".', $type)),
179+
};
180+
}
181+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Chat;
13+
14+
/**
15+
* @author Guillaume Loulier <[email protected]>
16+
*/
17+
interface ManagedStoreInterface
18+
{
19+
/**
20+
* @param array<mixed> $options
21+
*/
22+
public function setup(array $options = []): void;
23+
24+
public function drop(): void;
25+
}

0 commit comments

Comments
 (0)