Skip to content

Commit d66738a

Browse files
authored
Anthropic PDF Handling (#142)
1 parent 99f2651 commit d66738a

File tree

6 files changed

+176
-4
lines changed

6 files changed

+176
-4
lines changed

docs/providers/anthropic.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ Anthropic's prompt caching feature allows you to drastically reduce latency and
1414
We support Anthropic prompt caching on:
1515

1616
- System Messages (text only)
17-
- User Messages (Text and Image)
17+
- User Messages (Text, Image and PDF (pdf only))
18+
- Assistant Messages (text only)
1819
- Tools
1920

20-
The API for enable prompt caching is the same for all, enabled via the `withProviderMeta()` method. Where a UserMessage contains both text and an image, both will be cached.
21+
The API for enabling prompt caching is the same for all, enabled via the `withProviderMeta()` method. Where a UserMessage contains both text and an image or document, both will be cached.
2122

2223
```php
2324
use EchoLabs\Enums\Provider;
@@ -55,6 +56,31 @@ Note that you must use the `withMessages()` method in order to enable prompt cac
5556

5657
Please ensure you read Anthropic's [prompt caching documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching), which covers some important information on e.g. minimum cacheable tokens and message order consistency.
5758

59+
### PDF Support
60+
61+
Prism supports Anthropic PDF processing on UserMessages via the `$additionalContent` parameter:
62+
63+
```php
64+
use EchoLabs\Enums\Provider;
65+
use EchoLabs\Prism\Prism;
66+
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
67+
68+
Prism::text()
69+
->using(Provider::Anthropic, 'claude-3-5-sonnet-20241022')
70+
->withMessages([
71+
new UserMessage('Here is the document from base64', [
72+
Document::fromBase64(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')), 'application/pdf'),
73+
]),
74+
new UserMessage('Here is the document from a local path', [
75+
Document::fromPath('tests/Fixtures/test-pdf.pdf', 'application/pdf'),
76+
]),
77+
])
78+
->generate();
79+
80+
```
81+
Anthropic use vision to process PDFs, and consequently there are some limitations detailed in their [feature documentation](https://docs.anthropic.com/en/docs/build-with-claude/pdf-support).
82+
83+
5884
## Considerations
5985
### Message Order
6086

src/Providers/Anthropic/Maps/MessageMap.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use EchoLabs\Prism\Contracts\Message;
99
use EchoLabs\Prism\Enums\Provider;
1010
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
11+
use EchoLabs\Prism\ValueObjects\Messages\Support\Document;
1112
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
1213
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1314
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
@@ -106,6 +107,7 @@ protected static function mapUserMessage(UserMessage $message): array
106107
'cache_control' => $cache_control,
107108
]),
108109
...self::mapImageParts($message->images(), $cache_control),
110+
...self::mapDocumentParts($message->documents(), $cache_control),
109111
],
110112
];
111113
}
@@ -145,7 +147,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array
145147
/**
146148
* @param Image[] $parts
147149
* @param array<string, mixed>|null $cache_control
148-
* @return array<string, mixed>
150+
* @return array<int, mixed>
149151
*/
150152
protected static function mapImageParts(array $parts, ?array $cache_control = null): array
151153
{
@@ -165,4 +167,22 @@ protected static function mapImageParts(array $parts, ?array $cache_control = nu
165167
]);
166168
}, $parts);
167169
}
170+
171+
/**
172+
* @param Document[] $parts
173+
* @param array<string, mixed>|null $cache_control
174+
* @return array<int, mixed>
175+
*/
176+
protected static function mapDocumentParts(array $parts, ?array $cache_control = null): array
177+
{
178+
return array_map(fn (Document $document): array => array_filter([
179+
'type' => 'document',
180+
'source' => [
181+
'type' => 'base64',
182+
'media_type' => $document->mimeType,
183+
'data' => $document->document,
184+
],
185+
'cache_control' => $cache_control,
186+
]), $parts);
187+
}
168188
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EchoLabs\Prism\ValueObjects\Messages\Support;
6+
7+
use Illuminate\Support\Facades\File;
8+
use InvalidArgumentException;
9+
10+
/**
11+
* Note: Prism currently only supports Documents with Anthropic.
12+
*/
13+
class Document
14+
{
15+
public function __construct(
16+
public readonly string $document,
17+
public readonly string $mimeType
18+
) {}
19+
20+
public static function fromPath(string $path): self
21+
{
22+
if (! is_file($path)) {
23+
throw new InvalidArgumentException("{$path} is not a file");
24+
}
25+
26+
$content = file_get_contents($path);
27+
28+
if ($content === '' || $content === '0' || $content === false) {
29+
throw new InvalidArgumentException("{$path} is empty");
30+
}
31+
32+
$mimeType = File::mimeType($path);
33+
34+
if ($mimeType === false) {
35+
throw new InvalidArgumentException("Could not determine mime type for {$path}");
36+
}
37+
38+
return new self(
39+
base64_encode($content),
40+
$mimeType,
41+
);
42+
}
43+
44+
public static function fromBase64(string $document, string $mimeType): self
45+
{
46+
return new self(
47+
$document,
48+
$mimeType
49+
);
50+
}
51+
}

src/ValueObjects/Messages/UserMessage.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use EchoLabs\Prism\Concerns\HasProviderMeta;
88
use EchoLabs\Prism\Contracts\Message;
9+
use EchoLabs\Prism\ValueObjects\Messages\Support\Document;
910
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
1011
use EchoLabs\Prism\ValueObjects\Messages\Support\Text;
1112

@@ -14,7 +15,7 @@ class UserMessage implements Message
1415
use HasProviderMeta;
1516

1617
/**
17-
* @param array<int, Text|Image> $additionalContent
18+
* @param array<int, Text|Image|Document> $additionalContent
1819
*/
1920
public function __construct(
2021
protected readonly string $content,
@@ -43,4 +44,16 @@ public function images(): array
4344
->where(fn ($part): bool => $part instanceof Image)
4445
->toArray();
4546
}
47+
48+
/**
49+
* Note: Prism currently only supports Documents with Anthropic.
50+
*
51+
* @return Document[]
52+
*/
53+
public function documents(): array
54+
{
55+
return collect($this->additionalContent)
56+
->where(fn ($part): bool => $part instanceof Document)
57+
->toArray();
58+
}
4659
}

tests/Fixtures/test-pdf.pdf

9.48 KB
Binary file not shown.

tests/Providers/Anthropic/MessageMapTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use EchoLabs\Prism\Providers\Anthropic\Enums\AnthropicCacheType;
99
use EchoLabs\Prism\Providers\Anthropic\Maps\MessageMap;
1010
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;
11+
use EchoLabs\Prism\ValueObjects\Messages\Support\Document;
1112
use EchoLabs\Prism\ValueObjects\Messages\Support\Image;
1213
use EchoLabs\Prism\ValueObjects\Messages\SystemMessage;
1314
use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage;
@@ -73,6 +74,40 @@
7374
->toBe('image/png');
7475
});
7576

77+
it('maps user messages with documents from path', function (): void {
78+
$mappedMessage = MessageMap::map([
79+
new UserMessage('Here is the document', [
80+
Document::fromPath('tests/Fixtures/test-pdf.pdf'),
81+
]),
82+
]);
83+
84+
expect(data_get($mappedMessage, '0.content.1.type'))
85+
->toBe('document');
86+
expect(data_get($mappedMessage, '0.content.1.source.type'))
87+
->toBe('base64');
88+
expect(data_get($mappedMessage, '0.content.1.source.data'))
89+
->toContain(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')));
90+
expect(data_get($mappedMessage, '0.content.1.source.media_type'))
91+
->toBe('application/pdf');
92+
});
93+
94+
it('maps user messages with documents from base64', function (): void {
95+
$mappedMessage = MessageMap::map([
96+
new UserMessage('Here is the document', [
97+
Document::fromBase64(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')), 'application/pdf'),
98+
]),
99+
]);
100+
101+
expect(data_get($mappedMessage, '0.content.1.type'))
102+
->toBe('document');
103+
expect(data_get($mappedMessage, '0.content.1.source.type'))
104+
->toBe('base64');
105+
expect(data_get($mappedMessage, '0.content.1.source.data'))
106+
->toContain(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')));
107+
expect(data_get($mappedMessage, '0.content.1.source.media_type'))
108+
->toBe('application/pdf');
109+
});
110+
76111
it('does not maps user messages with images from url', function (): void {
77112
$this->expectException(InvalidArgumentException::class);
78113
MessageMap::map([
@@ -215,6 +250,33 @@
215250
]]);
216251
});
217252

253+
it('sets the cache type on a UserMessage document if cacheType providerMeta is set on message', function (): void {
254+
expect(MessageMap::map([
255+
(new UserMessage(
256+
content: 'Who are you?',
257+
additionalContent: [Document::fromPath('tests/Fixtures/test-pdf.pdf')]
258+
))->withProviderMeta(Provider::Anthropic, ['cacheType' => 'ephemeral']),
259+
]))->toBe([[
260+
'role' => 'user',
261+
'content' => [
262+
[
263+
'type' => 'text',
264+
'text' => 'Who are you?',
265+
'cache_control' => ['type' => 'ephemeral'],
266+
],
267+
[
268+
'type' => 'document',
269+
'source' => [
270+
'type' => 'base64',
271+
'media_type' => 'application/pdf',
272+
'data' => base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')),
273+
],
274+
'cache_control' => ['type' => 'ephemeral'],
275+
],
276+
],
277+
]]);
278+
});
279+
218280
it('sets the cache type on an AssistantMessage if cacheType providerMeta is set on message', function (mixed $cacheType): void {
219281
expect(MessageMap::map([
220282
(new AssistantMessage(content: 'Who are you?'))->withProviderMeta(Provider::Anthropic, ['cacheType' => $cacheType]),

0 commit comments

Comments
 (0)