Skip to content

Commit a3c6e68

Browse files
committed
Create content normalizers for Responses API
In order to migrate from chat completions to Responses, which enforces different data structures
1 parent 543060c commit a3c6e68

File tree

8 files changed

+435
-0
lines changed

8 files changed

+435
-0
lines changed
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 <[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\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
17+
use Symfony\AI\Platform\Message\Content\Document;
18+
use Symfony\AI\Platform\Message\Content\File;
19+
use Symfony\AI\Platform\Model;
20+
21+
/**
22+
* @author Guillermo Lengemann <[email protected]>
23+
*/
24+
class DocumentNormalizer extends ModelContractNormalizer
25+
{
26+
/**
27+
* @param File $data
28+
*
29+
* @return array{type: 'input_file', filename: string, file_data: string}
30+
*/
31+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
32+
{
33+
return [
34+
'type' => 'input_file',
35+
'filename' => $data->getFilename(),
36+
'file_data' => $data->asDataUrl(),
37+
];
38+
}
39+
40+
protected function supportedDataClass(): string
41+
{
42+
return Document::class;
43+
}
44+
45+
protected function supportsModel(Model $model): bool
46+
{
47+
return $model instanceof Gpt && $model->supports(Capability::INPUT_PDF);
48+
}
49+
}
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\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
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+
/**
21+
* See: https://platform.openai.com/docs/guides/images-vision#giving-a-model-images-as-input.
22+
*/
23+
final class ImageNormalizer extends ModelContractNormalizer
24+
{
25+
/**
26+
* @param Image $data
27+
*
28+
* @return array{
29+
* type: 'input_image',
30+
* image_url: string
31+
* }
32+
*/
33+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
34+
{
35+
return [
36+
'type' => 'input_image',
37+
'image_url' => $data->asDataUrl(),
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 Gpt && $model->supports(Capability::INPUT_IMAGE);
49+
}
50+
}
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\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
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+
/**
21+
* See: https://platform.openai.com/docs/guides/images-vision#giving-a-model-images-as-input.
22+
*/
23+
final class ImageUrlNormalizer extends ModelContractNormalizer
24+
{
25+
/**
26+
* @param ImageUrl $data
27+
*
28+
* @return array{
29+
* type: 'input_image',
30+
* image_url: string
31+
* }
32+
*/
33+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
34+
{
35+
return [
36+
'type' => 'input_image',
37+
'image_url' => $data->getUrl(),
38+
];
39+
}
40+
41+
protected function supportedDataClass(): string
42+
{
43+
return ImageUrl::class;
44+
}
45+
46+
protected function supportsModel(Model $model): bool
47+
{
48+
return $model instanceof Gpt && $model->supports(Capability::INPUT_IMAGE);
49+
}
50+
}
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 <[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\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
15+
use Symfony\AI\Platform\Contract\Normalizer\ModelContractNormalizer;
16+
use Symfony\AI\Platform\Message\Content\Text;
17+
use Symfony\AI\Platform\Model;
18+
19+
/**
20+
* See: https://platform.openai.com/docs/guides/images-vision#giving-a-model-images-as-input.
21+
*/
22+
final class TextNormalizer extends ModelContractNormalizer
23+
{
24+
/**
25+
* @param Text $data
26+
*
27+
* @return array{
28+
* type: 'input_text',
29+
* text: string
30+
* }
31+
*/
32+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
33+
{
34+
return [
35+
'type' => 'input_text',
36+
'text' => $data->getText(),
37+
];
38+
}
39+
40+
protected function supportedDataClass(): string
41+
{
42+
return Text::class;
43+
}
44+
45+
protected function supportsModel(Model $model): bool
46+
{
47+
return $model instanceof Gpt;
48+
}
49+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 Bridge\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\AI\Platform\Bridge\Gemini\Gemini;
17+
use Symfony\AI\Platform\Bridge\OpenAi\Contract\Gpt\Message\Content\DocumentNormalizer;
18+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
19+
use Symfony\AI\Platform\Capability;
20+
use Symfony\AI\Platform\Contract;
21+
use Symfony\AI\Platform\Message\Content\Document;
22+
use Symfony\AI\Platform\Message\Content\Text;
23+
use Symfony\AI\Platform\Model;
24+
25+
class DocumentNormalizerTest extends TestCase
26+
{
27+
public function testNormalize()
28+
{
29+
$doc = Document::fromFile(\dirname(__DIR__, 9).'/fixtures/document.pdf');
30+
$actual = (new DocumentNormalizer())->normalize($doc, null, [Contract::CONTEXT_MODEL => new Gpt('o3')]);
31+
32+
$this->assertEquals([
33+
'type' => 'input_file',
34+
'filename' => $doc->getFilename(),
35+
'file_data' => $doc->asDataUrl(),
36+
], $actual);
37+
}
38+
39+
#[DataProvider('supportsNormalizationProvider')]
40+
public function testSupportsNormalization(mixed $data, Model $model, bool $expected)
41+
{
42+
$this->assertSame(
43+
$expected,
44+
(new DocumentNormalizer())->supportsNormalization($data, null, [Contract::CONTEXT_MODEL => $model])
45+
);
46+
}
47+
48+
public static function supportsNormalizationProvider(): \Generator
49+
{
50+
$doc = Document::fromFile(\dirname(__DIR__, 9).'/fixtures/document.pdf');
51+
$gpt = new Gpt('o3', [Capability::INPUT_PDF]);
52+
53+
yield 'supported' => [$doc, $gpt, true];
54+
55+
yield 'unsupported model' => [$doc, new Gemini('foo', [Capability::INPUT_PDF]), false];
56+
57+
yield 'model lacks image input capability' => [$doc, new Gpt('o3'), false];
58+
59+
yield 'unsupported data' => [new Text('foo'), $gpt, false];
60+
}
61+
}
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 Bridge\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\AI\Platform\Bridge\Gemini\Gemini;
17+
use Symfony\AI\Platform\Bridge\OpenAi\Contract\Gpt\Message\Content\ImageNormalizer;
18+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
19+
use Symfony\AI\Platform\Capability;
20+
use Symfony\AI\Platform\Contract;
21+
use Symfony\AI\Platform\Message\Content\Image;
22+
use Symfony\AI\Platform\Message\Content\Text;
23+
use Symfony\AI\Platform\Model;
24+
25+
class ImageNormalizerTest extends TestCase
26+
{
27+
public function testNormalize()
28+
{
29+
$image = Image::fromFile(\dirname(__DIR__, 9).'/fixtures/image.jpg');
30+
$actual = (new ImageNormalizer())->normalize($image, null, [Contract::CONTEXT_MODEL => new Gpt('o3')]);
31+
32+
$this->assertEquals([
33+
'type' => 'input_image',
34+
'image_url' => $image->asDataUrl(),
35+
], $actual);
36+
}
37+
38+
#[DataProvider('supportsNormalizationProvider')]
39+
public function testSupportsNormalization(mixed $data, Model $model, bool $expected)
40+
{
41+
$this->assertSame(
42+
$expected,
43+
(new ImageNormalizer())->supportsNormalization($data, null, [Contract::CONTEXT_MODEL => $model])
44+
);
45+
}
46+
47+
public static function supportsNormalizationProvider(): \Generator
48+
{
49+
$image = Image::fromFile(\dirname(__DIR__, 9).'/fixtures/image.jpg');
50+
$gpt = new Gpt('o3', [Capability::INPUT_IMAGE]);
51+
52+
yield 'supported' => [$image, $gpt, true];
53+
54+
yield 'unsupported model' => [$image, new Gemini('foo', [Capability::INPUT_IMAGE]), false];
55+
56+
yield 'model lacks image input capability' => [$image, new Gpt('o3'), false];
57+
58+
yield 'unsupported data' => [new Text('foo'), $gpt, false];
59+
}
60+
}
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\Tests\Bridge\OpenAi\Contract\Gpt\Message\Content;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\AI\Platform\Bridge\Gemini\Gemini;
17+
use Symfony\AI\Platform\Bridge\OpenAi\Contract\Gpt\Message\Content\ImageUrlNormalizer;
18+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt;
19+
use Symfony\AI\Platform\Capability;
20+
use Symfony\AI\Platform\Contract;
21+
use Symfony\AI\Platform\Message\Content\ImageUrl;
22+
use Symfony\AI\Platform\Message\Content\Text;
23+
use Symfony\AI\Platform\Model;
24+
25+
class ImageUrlNormalizerTest extends TestCase
26+
{
27+
public function testNormalize()
28+
{
29+
$url = new ImageUrl('https://example.com/image.jpg');
30+
$actual = (new ImageUrlNormalizer())->normalize($url, null, [Contract::CONTEXT_MODEL => new Gpt('o3')]);
31+
32+
$this->assertEquals([
33+
'type' => 'input_image',
34+
'image_url' => $url->getUrl(),
35+
], $actual);
36+
}
37+
38+
#[DataProvider('supportsNormalizationProvider')]
39+
public function testSupportsNormalization(mixed $data, Model $model, bool $expected)
40+
{
41+
$this->assertSame(
42+
$expected,
43+
(new ImageUrlNormalizer())->supportsNormalization($data, null, [Contract::CONTEXT_MODEL => $model])
44+
);
45+
}
46+
47+
public static function supportsNormalizationProvider(): \Generator
48+
{
49+
$url = new ImageUrl('https://example.com/image.jpg');
50+
$gpt = new Gpt('o3', [Capability::INPUT_IMAGE]);
51+
52+
yield 'supported' => [$url, $gpt, true];
53+
54+
yield 'unsupported model' => [$url, new Gemini('foo', [Capability::INPUT_IMAGE]), false];
55+
56+
yield 'model lacks image input capability' => [$url, new Gpt('o3'), false];
57+
58+
yield 'unsupported data' => [new Text('foo'), $gpt, false];
59+
}
60+
}

0 commit comments

Comments
 (0)