Skip to content

Commit 5db564b

Browse files
committed
feature #811 [Platform] Support multimodal embeddings with Voyage AI (paulinevos)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Support multimodal embeddings with Voyage AI | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Docs? | yes <!-- required for new features --> | License | MIT This PR adds support for multimodal embeddings using Voyage. Commits ------- bd91b18 [Platform] Support multimodal embeddings with Voyage AI
2 parents 2ddd2b3 + bd91b18 commit 5db564b

24 files changed

+1054
-5
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
Voyage AI
2+
=========
3+
4+
Voyage AI offers a number of models for embedding text, contextualized chunks, interleaved multimodal data, and reranking.
5+
The bundle currently supports text embedding and multimodal embedding.
6+
7+
For comprehensive information about Voyage AI, see the `Voyage AI API reference`_
8+
9+
Setup
10+
-----
11+
12+
Authentication
13+
~~~~~~~~~~~~~~
14+
15+
Voyage AI requires an API key, which you can set up in `Voyage AI dashboard`_.
16+
17+
Usage
18+
-----
19+
20+
Basic text embedding usage example::
21+
22+
use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory;
23+
24+
$platform = PlatformFactory::create($_ENV['VOYAGE_API_KEY'], $httpClient);
25+
26+
$result = $platform->invoke('voyage-3', <<<TEXT
27+
Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and
28+
rivers. The people of Japan were very kind and hardworking. They loved their country very much and took care of
29+
it. The country was very peaceful and prosperous. The people lived happily ever after.
30+
TEXT);
31+
32+
echo $result->getContent();
33+
34+
Voyage AI supports text, base64 image data, and image URLs in its multimodal embedding model. It also allows for
35+
multiple data types per vector embedding. To do this, wrap the data in a `Collection` as shown in the example below.
36+
37+
Basic multimodal embedding usage example::
38+
39+
use Symfony\AI\Platform\Bridge\Voyage\PlatformFactory;
40+
use Symfony\AI\Platform\Message\Content\Collection;
41+
use Symfony\AI\Platform\Message\Content\ImageUrl;
42+
use Symfony\AI\Platform\Message\Content\Text;
43+
44+
$platform = PlatformFactory::create($_ENV['VOYAGE_API_KEY'], $httpClient);
45+
46+
$result = $platform->invoke(
47+
'voyage-multimodal-3',
48+
new ImageUrl('https://example.com/image1.jpg'),
49+
new Collection(new Text('Hello, world!'), new ImageUrl('https://example.com/image2.jpg')
50+
);
51+
52+
echo $result->getContent();
53+
54+
55+
Examples
56+
--------
57+
58+
See the ``examples/voyage/`` directory for complete working examples:
59+
60+
* ``text-embeddings.php`` - Basic text embedding example
61+
* ``multiple-text-embeddings.php`` - Embedding multiple text values
62+
* ``multimodal-embeddings.php`` - Embedding multimodal data (single and multiple values)
63+
64+
.. _Voyage AI API reference: https://docs.voyageai.com/reference/embeddings-api
65+
.. _Voyage AI dashboard: https://dashboard.voyageai.com/organization/api-keys
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Platform\Bridge\Voyage\PlatformFactory;
13+
use Symfony\AI\Platform\Message\Content\Collection;
14+
use Symfony\AI\Platform\Message\Content\Image;
15+
use Symfony\AI\Platform\Message\Content\Text;
16+
17+
require_once dirname(__DIR__).'/bootstrap.php';
18+
19+
$platform = PlatformFactory::create(env('VOYAGE_API_KEY'), http_client());
20+
21+
$image = Image::fromFile(dirname(__DIR__, 2).'/fixtures/image.jpg');
22+
23+
// Single value
24+
$result1 = $platform->invoke('voyage-multimodal-3',
25+
new Text('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'),
26+
);
27+
28+
// Multiple values
29+
$result2 = $platform->invoke('voyage-multimodal-3', [
30+
new Collection(new Text('Photo of a sunrise'), $image),
31+
new Collection(new Text('Photo of a sunset'), $image),
32+
]);
33+
34+
echo 'Dimensions for text: '.$result1->asVectors()[0]->getDimensions().\PHP_EOL;
35+
36+
echo 'Dimensions for sunrise image and description: '.$result2->asVectors()[0]->getDimensions().\PHP_EOL;
37+
echo 'Dimensions for sunset image and description: '.$result2->asVectors()[1]->getDimensions().\PHP_EOL;
File renamed without changes.

src/platform/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ CHANGELOG
6666
* Add tool calling support for Ollama platform
6767
* Allow beta feature flags to be passed into Anthropic model options
6868
* Add Ollama streaming output support
69+
* Add multimodal embedding support for Voyage AI
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Voyage\Contract\Multimodal;
13+
14+
use Symfony\AI\Platform\Bridge\Voyage\Voyage;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract;
17+
use Symfony\AI\Platform\Message\Content\Collection;
18+
use Symfony\AI\Platform\Model;
19+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
20+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
21+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
22+
23+
final class CollectionNormalizer extends Contract\Normalizer\ModelContractNormalizer implements NormalizerAwareInterface
24+
{
25+
use NormalizerAwareTrait;
26+
27+
public const KEY_CONTENT = 'content';
28+
29+
/**
30+
* @param Collection $data
31+
*
32+
* @throws ExceptionInterface
33+
*/
34+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
35+
{
36+
$content = [];
37+
foreach ($data->getContent() as $item) {
38+
$normalized = $this->normalizer->normalize($item, $format, $context);
39+
$content = array_merge($content, array_pop($normalized)[self::KEY_CONTENT]);
40+
}
41+
42+
return [['content' => $content]];
43+
}
44+
45+
protected function supportedDataClass(): string
46+
{
47+
return Collection::class;
48+
}
49+
50+
protected function supportsModel(Model $model): bool
51+
{
52+
return $model instanceof Voyage && $model->supports(Capability::INPUT_MULTIMODAL);
53+
}
54+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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\Voyage\Contract\Multimodal;
13+
14+
use Symfony\AI\Platform\Bridge\Voyage\Voyage;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
17+
use Symfony\AI\Platform\Message\Content\Image;
18+
use Symfony\AI\Platform\Model;
19+
20+
use function Symfony\Component\String\u;
21+
22+
final class ImageNormalizer extends ModelContractNormalizer
23+
{
24+
/**
25+
* @param Image $data
26+
*/
27+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
28+
{
29+
return [[
30+
CollectionNormalizer::KEY_CONTENT => [[
31+
'type' => 'image_base64',
32+
'image_base64' => \sprintf(
33+
'data:%s;base64,%s',
34+
u($data->getFormat())->replace('jpg', 'jpeg'),
35+
$data->asBase64()
36+
),
37+
]],
38+
]];
39+
}
40+
41+
protected function supportedDataClass(): string
42+
{
43+
return Image::class;
44+
}
45+
46+
protected function supportsModel(Model $model): bool
47+
{
48+
return $model instanceof Voyage && $model->supports(Capability::INPUT_MULTIMODAL);
49+
}
50+
}
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+
namespace Symfony\AI\Platform\Bridge\Voyage\Contract\Multimodal;
13+
14+
use Symfony\AI\Platform\Bridge\Voyage\Voyage;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
17+
use Symfony\AI\Platform\Message\Content\ImageUrl;
18+
use Symfony\AI\Platform\Model;
19+
20+
final class ImageUrlNormalizer extends ModelContractNormalizer
21+
{
22+
/**
23+
* @param ImageUrl $data
24+
*/
25+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
26+
{
27+
return [[
28+
CollectionNormalizer::KEY_CONTENT => [[
29+
'type' => 'image_url',
30+
'image_url' => $data->getUrl(),
31+
]],
32+
]];
33+
}
34+
35+
protected function supportedDataClass(): string
36+
{
37+
return ImageUrl::class;
38+
}
39+
40+
protected function supportsModel(Model $model): bool
41+
{
42+
return $model instanceof Voyage && $model->supports(Capability::INPUT_MULTIMODAL);
43+
}
44+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\Voyage\Contract\Multimodal;
13+
14+
use Symfony\AI\Platform\Bridge\Voyage\Voyage;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract;
17+
use Symfony\AI\Platform\Message\Content\ContentInterface;
18+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
19+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
20+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
21+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
22+
23+
final class MultimodalNormalizer implements NormalizerInterface, NormalizerAwareInterface
24+
{
25+
use NormalizerAwareTrait;
26+
27+
/**
28+
* @param ContentInterface[] $data
29+
*
30+
* @throws ExceptionInterface
31+
*/
32+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
33+
{
34+
return array_map(
35+
function (ContentInterface $item) use ($format, $context) {
36+
$normalized = $this->normalizer->normalize($item, $format, $context);
37+
38+
return array_pop($normalized);
39+
},
40+
$data
41+
);
42+
}
43+
44+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
45+
{
46+
$model = $context[Contract::CONTEXT_MODEL] ?? null;
47+
if (!$model instanceof Voyage || !$model->supports(Capability::INPUT_MULTIMODAL)) {
48+
return false;
49+
}
50+
51+
return \is_array($data) && [] === array_filter($data, fn ($item) => !$item instanceof ContentInterface);
52+
}
53+
54+
public function getSupportedTypes(?string $format): array
55+
{
56+
return [
57+
'native-array' => true,
58+
];
59+
}
60+
}
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+
namespace Symfony\AI\Platform\Bridge\Voyage\Contract\Multimodal;
13+
14+
use Symfony\AI\Platform\Bridge\Voyage\Voyage;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
17+
use Symfony\AI\Platform\Message\Content\Text;
18+
use Symfony\AI\Platform\Model;
19+
20+
final class TextNormalizer extends ModelContractNormalizer
21+
{
22+
/**
23+
* @param Text $data
24+
*/
25+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
26+
{
27+
return [[
28+
CollectionNormalizer::KEY_CONTENT => [[
29+
'type' => 'text',
30+
'text' => $data->getText(),
31+
]],
32+
]];
33+
}
34+
35+
protected function supportedDataClass(): string
36+
{
37+
return Text::class;
38+
}
39+
40+
protected function supportsModel(Model $model): bool
41+
{
42+
return $model instanceof Voyage && $model->supports(Capability::INPUT_MULTIMODAL);
43+
}
44+
}

0 commit comments

Comments
 (0)