diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index df6e86a..5d860d7 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -14,10 +14,11 @@ Anthropic's prompt caching feature allows you to drastically reduce latency and We support Anthropic prompt caching on: - System Messages (text only) -- User Messages (Text and Image) +- User Messages (Text, Image and PDF (pdf only)) +- Assistant Messages (text only) - Tools -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. +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. ```php use EchoLabs\Enums\Provider; @@ -55,6 +56,31 @@ Note that you must use the `withMessages()` method in order to enable prompt cac 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. +### PDF Support + +Prism supports Anthropic PDF processing on UserMessages via the `$additionalContent` parameter: + +```php +use EchoLabs\Enums\Provider; +use EchoLabs\Prism\Prism; +use EchoLabs\Prism\ValueObjects\Messages\UserMessage; + +Prism::text() + ->using(Provider::Anthropic, 'claude-3-5-sonnet-20241022') + ->withMessages([ + new UserMessage('Here is the document from base64', [ + Document::fromBase64(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')), 'application/pdf'), + ]), + new UserMessage('Here is the document from a local path', [ + Document::fromPath('tests/Fixtures/test-pdf.pdf', 'application/pdf'), + ]), + ]) + ->generate(); + +``` +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). + + ## Considerations ### Message Order diff --git a/src/Providers/Anthropic/Maps/MessageMap.php b/src/Providers/Anthropic/Maps/MessageMap.php index 6aecbb4..d450ce8 100644 --- a/src/Providers/Anthropic/Maps/MessageMap.php +++ b/src/Providers/Anthropic/Maps/MessageMap.php @@ -8,6 +8,7 @@ use EchoLabs\Prism\Contracts\Message; use EchoLabs\Prism\Enums\Provider; use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage; +use EchoLabs\Prism\ValueObjects\Messages\Support\Document; use EchoLabs\Prism\ValueObjects\Messages\Support\Image; use EchoLabs\Prism\ValueObjects\Messages\SystemMessage; use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage; @@ -106,6 +107,7 @@ protected static function mapUserMessage(UserMessage $message): array 'cache_control' => $cache_control, ]), ...self::mapImageParts($message->images(), $cache_control), + ...self::mapDocumentParts($message->documents(), $cache_control), ], ]; } @@ -145,7 +147,7 @@ protected static function mapAssistantMessage(AssistantMessage $message): array /** * @param Image[] $parts * @param array|null $cache_control - * @return array + * @return array */ protected static function mapImageParts(array $parts, ?array $cache_control = null): array { @@ -165,4 +167,22 @@ protected static function mapImageParts(array $parts, ?array $cache_control = nu ]); }, $parts); } + + /** + * @param Document[] $parts + * @param array|null $cache_control + * @return array + */ + protected static function mapDocumentParts(array $parts, ?array $cache_control = null): array + { + return array_map(fn (Document $document): array => array_filter([ + 'type' => 'document', + 'source' => [ + 'type' => 'base64', + 'media_type' => $document->mimeType, + 'data' => $document->document, + ], + 'cache_control' => $cache_control, + ]), $parts); + } } diff --git a/src/ValueObjects/Messages/Support/Document.php b/src/ValueObjects/Messages/Support/Document.php new file mode 100644 index 0000000..d2337cc --- /dev/null +++ b/src/ValueObjects/Messages/Support/Document.php @@ -0,0 +1,51 @@ + $additionalContent + * @param array $additionalContent */ public function __construct( protected readonly string $content, @@ -43,4 +44,16 @@ public function images(): array ->where(fn ($part): bool => $part instanceof Image) ->toArray(); } + + /** + * Note: Prism currently only supports Documents with Anthropic. + * + * @return Document[] + */ + public function documents(): array + { + return collect($this->additionalContent) + ->where(fn ($part): bool => $part instanceof Document) + ->toArray(); + } } diff --git a/tests/Fixtures/test-pdf.pdf b/tests/Fixtures/test-pdf.pdf new file mode 100644 index 0000000..cc01b97 Binary files /dev/null and b/tests/Fixtures/test-pdf.pdf differ diff --git a/tests/Providers/Anthropic/MessageMapTest.php b/tests/Providers/Anthropic/MessageMapTest.php index 1c5642f..d16b3a9 100644 --- a/tests/Providers/Anthropic/MessageMapTest.php +++ b/tests/Providers/Anthropic/MessageMapTest.php @@ -8,6 +8,7 @@ use EchoLabs\Prism\Providers\Anthropic\Enums\AnthropicCacheType; use EchoLabs\Prism\Providers\Anthropic\Maps\MessageMap; use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage; +use EchoLabs\Prism\ValueObjects\Messages\Support\Document; use EchoLabs\Prism\ValueObjects\Messages\Support\Image; use EchoLabs\Prism\ValueObjects\Messages\SystemMessage; use EchoLabs\Prism\ValueObjects\Messages\ToolResultMessage; @@ -73,6 +74,40 @@ ->toBe('image/png'); }); +it('maps user messages with documents from path', function (): void { + $mappedMessage = MessageMap::map([ + new UserMessage('Here is the document', [ + Document::fromPath('tests/Fixtures/test-pdf.pdf'), + ]), + ]); + + expect(data_get($mappedMessage, '0.content.1.type')) + ->toBe('document'); + expect(data_get($mappedMessage, '0.content.1.source.type')) + ->toBe('base64'); + expect(data_get($mappedMessage, '0.content.1.source.data')) + ->toContain(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf'))); + expect(data_get($mappedMessage, '0.content.1.source.media_type')) + ->toBe('application/pdf'); +}); + +it('maps user messages with documents from base64', function (): void { + $mappedMessage = MessageMap::map([ + new UserMessage('Here is the document', [ + Document::fromBase64(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')), 'application/pdf'), + ]), + ]); + + expect(data_get($mappedMessage, '0.content.1.type')) + ->toBe('document'); + expect(data_get($mappedMessage, '0.content.1.source.type')) + ->toBe('base64'); + expect(data_get($mappedMessage, '0.content.1.source.data')) + ->toContain(base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf'))); + expect(data_get($mappedMessage, '0.content.1.source.media_type')) + ->toBe('application/pdf'); +}); + it('does not maps user messages with images from url', function (): void { $this->expectException(InvalidArgumentException::class); MessageMap::map([ @@ -215,6 +250,33 @@ ]]); }); +it('sets the cache type on a UserMessage document if cacheType providerMeta is set on message', function (): void { + expect(MessageMap::map([ + (new UserMessage( + content: 'Who are you?', + additionalContent: [Document::fromPath('tests/Fixtures/test-pdf.pdf')] + ))->withProviderMeta(Provider::Anthropic, ['cacheType' => 'ephemeral']), + ]))->toBe([[ + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Who are you?', + 'cache_control' => ['type' => 'ephemeral'], + ], + [ + 'type' => 'document', + 'source' => [ + 'type' => 'base64', + 'media_type' => 'application/pdf', + 'data' => base64_encode(file_get_contents('tests/Fixtures/test-pdf.pdf')), + ], + 'cache_control' => ['type' => 'ephemeral'], + ], + ], + ]]); +}); + it('sets the cache type on an AssistantMessage if cacheType providerMeta is set on message', function (mixed $cacheType): void { expect(MessageMap::map([ (new AssistantMessage(content: 'Who are you?'))->withProviderMeta(Provider::Anthropic, ['cacheType' => $cacheType]),