Skip to content

Commit b994388

Browse files
committed
feat(platform): meilisearch message bag
1 parent bf941b4 commit b994388

File tree

5 files changed

+440
-1
lines changed

5 files changed

+440
-1
lines changed

examples/agent/meilisearch_bag.php

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\Platform\Bridge\Meilisearch\MessageBag;
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
15+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
16+
use Symfony\AI\Platform\Message\Message;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
21+
$model = new Gpt(Gpt::GPT_4O_MINI, [
22+
'temperature' => 0.5, // default options for the model
23+
]);
24+
25+
$agent = new Agent($platform, $model, logger: logger());
26+
27+
$messages = new MessageBag(
28+
http_client(),
29+
env('MEILISEARCH_HOST'),
30+
env('MEILISEARCH_API_KEY'),
31+
'meilisearch_agent'
32+
);
33+
34+
// Initialize the bag
35+
$messages->initialize();
36+
37+
$messages->add(Message::forSystem('You are a pirate and you write funny.'));
38+
$messages->add(Message::ofUser('What is the Symfony framework?'));
39+
40+
$result = $agent->call($messages, [
41+
'max_tokens' => 500, // specific options just for this call
42+
]);
43+
44+
echo $result->getContent().\PHP_EOL;

src/platform/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@ CHANGELOG
5858
* Add response promises for async operations
5959
* Add InMemoryPlatform and InMemoryRawResult for testing Platform without external Providers calls
6060
* Add tool calling support for Ollama platform
61-
61+
* Add support for message bags:
62+
- Meilisearch
6263

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

0 commit comments

Comments
 (0)