Skip to content

Commit 773a700

Browse files
authored
feat: extend Claude support for images and pdf input (#298)
- [x] image input - [x] pdf input - [ ] ~streamed toolcalls~ scoped out for now - [ ] ~thinking~ scoped out for now
1 parent 5a64eaa commit 773a700

File tree

17 files changed

+337
-49
lines changed

17 files changed

+337
-49
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,3 +824,4 @@ For testing multi-modal features, the repository contains binary media content,
824824

825825
* `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/)
826826
* `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/)
827+
* `tests/Fixture/document.pdf`: Chem8240ja, Public Domain, see [Wikipedia](https://en.m.wikipedia.org/wiki/File:Re_example.pdf)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain\Chain;
4+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
5+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\PlatformFactory;
6+
use PhpLlm\LlmChain\Platform\Message\Content\Image;
7+
use PhpLlm\LlmChain\Platform\Message\Message;
8+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
9+
use Symfony\Component\Dotenv\Dotenv;
10+
11+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
13+
14+
if (empty($_ENV['ANTHROPIC_API_KEY'])) {
15+
echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']);
20+
$llm = new Claude(Claude::SONNET_37);
21+
22+
$chain = new Chain($platform, $llm);
23+
$messages = new MessageBag(
24+
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
25+
Message::ofUser(
26+
Image::fromFile(dirname(__DIR__, 2).'/tests/Fixture/image.jpg'),
27+
'Describe this image.',
28+
),
29+
);
30+
$response = $chain->call($messages);
31+
32+
echo $response->getContent().\PHP_EOL;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain\Chain;
4+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
5+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\PlatformFactory;
6+
use PhpLlm\LlmChain\Platform\Message\Content\ImageUrl;
7+
use PhpLlm\LlmChain\Platform\Message\Message;
8+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
9+
use Symfony\Component\Dotenv\Dotenv;
10+
11+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
13+
14+
if (empty($_ENV['ANTHROPIC_API_KEY'])) {
15+
echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']);
20+
$llm = new Claude(Claude::SONNET_37);
21+
22+
$chain = new Chain($platform, $llm);
23+
$messages = new MessageBag(
24+
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
25+
Message::ofUser(
26+
new ImageUrl('https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg'),
27+
'Describe this image.',
28+
),
29+
);
30+
$response = $chain->call($messages);
31+
32+
echo $response->getContent().\PHP_EOL;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain\Chain;
4+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
5+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\PlatformFactory;
6+
use PhpLlm\LlmChain\Platform\Message\Content\Document;
7+
use PhpLlm\LlmChain\Platform\Message\Message;
8+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
9+
use Symfony\Component\Dotenv\Dotenv;
10+
11+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
13+
14+
if (empty($_ENV['ANTHROPIC_API_KEY'])) {
15+
echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']);
20+
$llm = new Claude(Claude::SONNET_37);
21+
22+
$chain = new Chain($platform, $llm);
23+
$messages = new MessageBag(
24+
Message::ofUser(
25+
Document::fromFile(dirname(__DIR__, 2).'/tests/Fixture/document.pdf'),
26+
'What is this document about?',
27+
),
28+
);
29+
$response = $chain->call($messages);
30+
31+
echo $response->getContent().\PHP_EOL;

examples/anthropic/pdf-input-url.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Chain\Chain;
4+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
5+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\PlatformFactory;
6+
use PhpLlm\LlmChain\Platform\Message\Content\DocumentUrl;
7+
use PhpLlm\LlmChain\Platform\Message\Message;
8+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
9+
use Symfony\Component\Dotenv\Dotenv;
10+
11+
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
12+
(new Dotenv())->loadEnv(dirname(__DIR__, 2).'/.env');
13+
14+
if (empty($_ENV['ANTHROPIC_API_KEY'])) {
15+
echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL;
16+
exit(1);
17+
}
18+
19+
$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']);
20+
$llm = new Claude(Claude::SONNET_37);
21+
22+
$chain = new Chain($platform, $llm);
23+
$messages = new MessageBag(
24+
Message::ofUser(
25+
new DocumentUrl('https://upload.wikimedia.org/wikipedia/commons/2/20/Re_example.pdf'),
26+
'What is this document about?',
27+
),
28+
);
29+
$response = $chain->call($messages);
30+
31+
echo $response->getContent().\PHP_EOL;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract;
4+
5+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
6+
use PhpLlm\LlmChain\Platform\Contract\Normalizer\ModelContractNormalizer;
7+
use PhpLlm\LlmChain\Platform\Message\Content\Document;
8+
use PhpLlm\LlmChain\Platform\Model;
9+
10+
class DocumentNormalizer extends ModelContractNormalizer
11+
{
12+
protected function supportedDataClass(): string
13+
{
14+
return Document::class;
15+
}
16+
17+
protected function supportsModel(Model $model): bool
18+
{
19+
return $model instanceof Claude;
20+
}
21+
22+
/**
23+
* @param Document $data
24+
*
25+
* @return array{type: 'document', source: array{type: 'base64', media_type: string, data: string}}
26+
*/
27+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
28+
{
29+
return [
30+
'type' => 'document',
31+
'source' => [
32+
'type' => 'base64',
33+
'media_type' => $data->getFormat(),
34+
'data' => $data->asBase64(),
35+
],
36+
];
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
8+
use PhpLlm\LlmChain\Platform\Contract\Normalizer\ModelContractNormalizer;
9+
use PhpLlm\LlmChain\Platform\Message\Content\DocumentUrl;
10+
use PhpLlm\LlmChain\Platform\Model;
11+
12+
final class DocumentUrlNormalizer extends ModelContractNormalizer
13+
{
14+
protected function supportedDataClass(): string
15+
{
16+
return DocumentUrl::class;
17+
}
18+
19+
protected function supportsModel(Model $model): bool
20+
{
21+
return $model instanceof Claude;
22+
}
23+
24+
/**
25+
* @param DocumentUrl $data
26+
*
27+
* @return array{type: 'document', source: array{type: 'url', url: string}}
28+
*/
29+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
30+
{
31+
return [
32+
'type' => 'document',
33+
'source' => [
34+
'type' => 'url',
35+
'url' => $data->url,
36+
],
37+
];
38+
}
39+
}

src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,7 @@ protected function supportsModel(Model $model): bool
2626
/**
2727
* @param Image $data
2828
*
29-
* @return array{
30-
* type: 'image',
31-
* source: array{
32-
* type: 'base64',
33-
* media_type: string,
34-
* data: string
35-
* }
36-
* }
29+
* @return array{type: 'image', source: array{type: 'base64', media_type: string, data: string}}
3730
*/
3831
public function normalize(mixed $data, ?string $format = null, array $context = []): array
3932
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
8+
use PhpLlm\LlmChain\Platform\Contract\Normalizer\ModelContractNormalizer;
9+
use PhpLlm\LlmChain\Platform\Message\Content\Image;
10+
use PhpLlm\LlmChain\Platform\Message\Content\ImageUrl;
11+
use PhpLlm\LlmChain\Platform\Model;
12+
13+
final class ImageUrlNormalizer extends ModelContractNormalizer
14+
{
15+
protected function supportedDataClass(): string
16+
{
17+
return ImageUrl::class;
18+
}
19+
20+
protected function supportsModel(Model $model): bool
21+
{
22+
return $model instanceof Claude;
23+
}
24+
25+
/**
26+
* @param ImageUrl $data
27+
*
28+
* @return array{type: 'image', source: array{type: 'url', url: string}}
29+
*/
30+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
31+
{
32+
return [
33+
'type' => 'image',
34+
'source' => [
35+
'type' => 'url',
36+
'url' => $data->url,
37+
],
38+
];
39+
}
40+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Anthropic;
6+
7+
use PhpLlm\LlmChain\Platform\Model;
8+
use PhpLlm\LlmChain\Platform\ModelClientInterface;
9+
use Symfony\Component\HttpClient\EventSourceHttpClient;
10+
use Symfony\Contracts\HttpClient\HttpClientInterface;
11+
use Symfony\Contracts\HttpClient\ResponseInterface;
12+
13+
final readonly class ModelClient implements ModelClientInterface
14+
{
15+
private EventSourceHttpClient $httpClient;
16+
17+
public function __construct(
18+
HttpClientInterface $httpClient,
19+
#[\SensitiveParameter] private string $apiKey,
20+
private string $version = '2023-06-01',
21+
) {
22+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
23+
}
24+
25+
public function supports(Model $model): bool
26+
{
27+
return $model instanceof Claude;
28+
}
29+
30+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
31+
{
32+
if (isset($options['tools'])) {
33+
$options['tool_choice'] = ['type' => 'auto'];
34+
}
35+
36+
return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [
37+
'headers' => [
38+
'x-api-key' => $this->apiKey,
39+
'anthropic-version' => $this->version,
40+
],
41+
'json' => array_merge($options, $payload),
42+
]);
43+
}
44+
}

0 commit comments

Comments
 (0)