Skip to content

Commit e7bec23

Browse files
committed
feature #357 [Platform][OpenAI] Add file input normalizer (glengemann)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform][OpenAI] Add file input normalizer | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | no | Issues | Fix #... | License | MIT Hi there! Currently, if you try to add a file into the message input, the following error is thrown: ```php // PHP Fatal error: Uncaught Symfony\Component\Serializer\Exception\NotNormalizableValueException: Could not normalize object of type "Symfony\AI\Platform\Message\Content\File", no supporting normalizer found. Message::ofUser( 'What is this document about?', File::fromFile(dirname(__DIR__, 2).'/fixtures/document.pdf'), ) ``` This PR adds support for including files as inputs in the OpenAI completions API, as described in the official [documentation](https://platform.openai.com/docs/guides/pdf-files?api-mode=chat&lang=javascript#base64-encoded-files). The implementation follows the same approach already used for Gemini and Anthropic. Commits ------- e621538 [Platform][OpenAI] Add file input normalizer
2 parents 087ccb3 + e621538 commit e7bec23

File tree

6 files changed

+198
-2
lines changed

6 files changed

+198
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\OpenAi\Gpt;
14+
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Content\File;
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+
$model = new Gpt(Gpt::GPT_4O_MINI);
23+
24+
$agent = new Agent($platform, $model, logger: logger());
25+
$messages = new MessageBag(
26+
Message::ofUser(
27+
'What is this document about?',
28+
// Note: You can use either `File::fromFile` or `Document::fromFile` here.
29+
File::fromFile(dirname(__DIR__, 2).'/fixtures/document.pdf'),
30+
),
31+
);
32+
$result = $agent->call($messages);
33+
34+
echo $result->getContent().\PHP_EOL;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\OpenAi\Contract;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
15+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
16+
use Symfony\AI\Platform\Message\Content\File;
17+
use Symfony\AI\Platform\Model;
18+
19+
/**
20+
* @author Guillermo Lengemann <guillermo.lengemann@gmail.com>
21+
*/
22+
class FileNormalizer extends ModelContractNormalizer
23+
{
24+
/**
25+
* @param File $data
26+
*
27+
* @return array{type: 'file', file: array{filename: string, file_data: 'base64'}}
28+
*/
29+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
30+
{
31+
return [
32+
'type' => 'file',
33+
'file' => [
34+
'filename' => $data->getFilename(),
35+
'file_data' => $data->asDataUrl(),
36+
],
37+
];
38+
}
39+
40+
protected function supportedDataClass(): string
41+
{
42+
return File::class;
43+
}
44+
45+
protected function supportsModel(Model $model): bool
46+
{
47+
return $model instanceof Gpt;
48+
}
49+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\OpenAi\Contract;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Whisper\AudioNormalizer;
15+
use Symfony\AI\Platform\Contract;
16+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
17+
18+
/**
19+
* @author Guillermo Lengemann <guillermo.lengemann@gmail.com>
20+
*/
21+
final readonly class OpenAiContract extends Contract
22+
{
23+
public static function create(NormalizerInterface ...$normalizer): Contract
24+
{
25+
return parent::create(
26+
new AudioNormalizer(),
27+
new FileNormalizer(),
28+
...$normalizer
29+
);
30+
}
31+
}

src/platform/src/Bridge/OpenAi/PlatformFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\AI\Platform\Bridge\OpenAi;
1313

14-
use Symfony\AI\Platform\Bridge\OpenAi\Whisper\AudioNormalizer;
14+
use Symfony\AI\Platform\Bridge\OpenAi\Contract\OpenAiContract;
1515
use Symfony\AI\Platform\Bridge\OpenAi\Whisper\ModelClient as WhisperModelClient;
1616
use Symfony\AI\Platform\Bridge\OpenAi\Whisper\ResultConverter as WhisperResponseConverter;
1717
use Symfony\AI\Platform\Contract;
@@ -45,7 +45,7 @@ public static function create(
4545
new DallE\ResultConverter(),
4646
new WhisperResponseConverter(),
4747
],
48-
$contract ?? Contract::create(new AudioNormalizer()),
48+
$contract ?? OpenAiContract::create(),
4949
);
5050
}
5151
}

src/platform/src/Message/Content/File.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ public function asResource()
8989

9090
return fopen($this->path, 'r');
9191
}
92+
93+
public function getFilename(): ?string
94+
{
95+
return null === $this->path ? null : basename($this->path);
96+
}
9297
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\Bridge\OpenAi\Contract;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\Attributes\Medium;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\AI\Platform\Bridge\Gemini\Contract\MessageBagNormalizer;
19+
use Symfony\AI\Platform\Bridge\OpenAi\Contract\FileNormalizer;
20+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
21+
use Symfony\AI\Platform\Contract;
22+
use Symfony\AI\Platform\Message\Content\Document;
23+
use Symfony\AI\Platform\Message\Content\File;
24+
25+
#[Medium]
26+
#[CoversClass(FileNormalizer::class)]
27+
#[CoversClass(MessageBagNormalizer::class)]
28+
final class DocumentNormalizerTest extends TestCase
29+
{
30+
public function testSupportsNormalization()
31+
{
32+
$normalizer = new FileNormalizer();
33+
34+
$this->assertTrue($normalizer->supportsNormalization(new Document('some content', 'application/pdf'), context: [
35+
Contract::CONTEXT_MODEL => new Gpt(),
36+
]));
37+
$this->assertTrue($normalizer->supportsNormalization(new File('some content', 'application/pdf'), context: [
38+
Contract::CONTEXT_MODEL => new Gpt(),
39+
]));
40+
$this->assertFalse($normalizer->supportsNormalization('not a document'));
41+
}
42+
43+
public function testGetSupportedTypes()
44+
{
45+
$normalizer = new FileNormalizer();
46+
47+
$expected = [
48+
File::class => true,
49+
];
50+
51+
$this->assertSame($expected, $normalizer->getSupportedTypes(null));
52+
}
53+
54+
#[DataProvider('normalizeDataProvider')]
55+
public function testNormalize(File $file, array $expected)
56+
{
57+
$normalizer = new FileNormalizer();
58+
59+
$normalized = $normalizer->normalize($file);
60+
61+
$this->assertEquals($expected, $normalized);
62+
}
63+
64+
public static function normalizeDataProvider(): iterable
65+
{
66+
yield 'document from file' => [
67+
File::fromFile(\dirname(__DIR__, 6).'/fixtures/document.pdf'),
68+
[
69+
'type' => 'file',
70+
'file' => [
71+
'filename' => 'document.pdf',
72+
'file_data' => 'data:application/pdf;base64,'.base64_encode(file_get_contents(\dirname(__DIR__, 6).'/fixtures/document.pdf')),
73+
],
74+
],
75+
];
76+
}
77+
}

0 commit comments

Comments
 (0)