Skip to content

Commit 601d958

Browse files
committed
feat(platform): meilisearch message bag
1 parent 0fc7fab commit 601d958

File tree

6 files changed

+531
-34
lines changed

6 files changed

+531
-34
lines changed

examples/chat/persistent-chat.php

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

0 commit comments

Comments
 (0)