diff --git a/README.md b/README.md index fd06ca20..ae525ec5 100644 --- a/README.md +++ b/README.md @@ -824,3 +824,4 @@ For testing multi-modal features, the repository contains binary media content, * `tests/Fixture/image.jpg`: Chris F., Creative Commons, see [pexels.com](https://www.pexels.com/photo/blauer-und-gruner-elefant-mit-licht-1680755/) * `tests/Fixture/audio.mp3`: davidbain, Creative Commons, see [freesound.org](https://freesound.org/people/davidbain/sounds/136777/) +* `tests/Fixture/document.pdf`: Chem8240ja, Public Domain, see [Wikipedia](https://en.m.wikipedia.org/wiki/File:Re_example.pdf) diff --git a/examples/anthropic/image-input-binary.php b/examples/anthropic/image-input-binary.php new file mode 100644 index 00000000..c2e7a3dc --- /dev/null +++ b/examples/anthropic/image-input-binary.php @@ -0,0 +1,32 @@ +loadEnv(dirname(__DIR__, 2).'/.env'); + +if (empty($_ENV['ANTHROPIC_API_KEY'])) { + echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(Claude::SONNET_37); + +$chain = new Chain($platform, $llm); +$messages = new MessageBag( + Message::forSystem('You are an image analyzer bot that helps identify the content of images.'), + Message::ofUser( + Image::fromFile(dirname(__DIR__, 2).'/tests/Fixture/image.jpg'), + 'Describe this image.', + ), +); +$response = $chain->call($messages); + +echo $response->getContent().\PHP_EOL; diff --git a/examples/anthropic/image-input-url.php b/examples/anthropic/image-input-url.php new file mode 100644 index 00000000..3ab59790 --- /dev/null +++ b/examples/anthropic/image-input-url.php @@ -0,0 +1,32 @@ +loadEnv(dirname(__DIR__, 2).'/.env'); + +if (empty($_ENV['ANTHROPIC_API_KEY'])) { + echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(Claude::SONNET_37); + +$chain = new Chain($platform, $llm); +$messages = new MessageBag( + Message::forSystem('You are an image analyzer bot that helps identify the content of images.'), + Message::ofUser( + new ImageUrl('https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg'), + 'Describe this image.', + ), +); +$response = $chain->call($messages); + +echo $response->getContent().\PHP_EOL; diff --git a/examples/anthropic/pdf-input-binary.php b/examples/anthropic/pdf-input-binary.php new file mode 100644 index 00000000..936cfd68 --- /dev/null +++ b/examples/anthropic/pdf-input-binary.php @@ -0,0 +1,31 @@ +loadEnv(dirname(__DIR__, 2).'/.env'); + +if (empty($_ENV['ANTHROPIC_API_KEY'])) { + echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(Claude::SONNET_37); + +$chain = new Chain($platform, $llm); +$messages = new MessageBag( + Message::ofUser( + Document::fromFile(dirname(__DIR__, 2).'/tests/Fixture/document.pdf'), + 'What is this document about?', + ), +); +$response = $chain->call($messages); + +echo $response->getContent().\PHP_EOL; diff --git a/examples/anthropic/pdf-input-url.php b/examples/anthropic/pdf-input-url.php new file mode 100644 index 00000000..7af6d45d --- /dev/null +++ b/examples/anthropic/pdf-input-url.php @@ -0,0 +1,31 @@ +loadEnv(dirname(__DIR__, 2).'/.env'); + +if (empty($_ENV['ANTHROPIC_API_KEY'])) { + echo 'Please set the ANTHROPIC_API_KEY environment variable.'.\PHP_EOL; + exit(1); +} + +$platform = PlatformFactory::create($_ENV['ANTHROPIC_API_KEY']); +$llm = new Claude(Claude::SONNET_37); + +$chain = new Chain($platform, $llm); +$messages = new MessageBag( + Message::ofUser( + new DocumentUrl('https://upload.wikimedia.org/wikipedia/commons/2/20/Re_example.pdf'), + 'What is this document about?', + ), +); +$response = $chain->call($messages); + +echo $response->getContent().\PHP_EOL; diff --git a/src/Platform/Bridge/Anthropic/Contract/DocumentNormalizer.php b/src/Platform/Bridge/Anthropic/Contract/DocumentNormalizer.php new file mode 100644 index 00000000..187df337 --- /dev/null +++ b/src/Platform/Bridge/Anthropic/Contract/DocumentNormalizer.php @@ -0,0 +1,38 @@ + 'document', + 'source' => [ + 'type' => 'base64', + 'media_type' => $data->getFormat(), + 'data' => $data->asBase64(), + ], + ]; + } +} diff --git a/src/Platform/Bridge/Anthropic/Contract/DocumentUrlNormalizer.php b/src/Platform/Bridge/Anthropic/Contract/DocumentUrlNormalizer.php new file mode 100644 index 00000000..6d434a23 --- /dev/null +++ b/src/Platform/Bridge/Anthropic/Contract/DocumentUrlNormalizer.php @@ -0,0 +1,39 @@ + 'document', + 'source' => [ + 'type' => 'url', + 'url' => $data->url, + ], + ]; + } +} diff --git a/src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php b/src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php index bb15c183..1a3c7c43 100644 --- a/src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php +++ b/src/Platform/Bridge/Anthropic/Contract/ImageNormalizer.php @@ -26,14 +26,7 @@ protected function supportsModel(Model $model): bool /** * @param Image $data * - * @return array{ - * type: 'image', - * source: array{ - * type: 'base64', - * media_type: string, - * data: string - * } - * } + * @return array{type: 'image', source: array{type: 'base64', media_type: string, data: string}} */ public function normalize(mixed $data, ?string $format = null, array $context = []): array { diff --git a/src/Platform/Bridge/Anthropic/Contract/ImageUrlNormalizer.php b/src/Platform/Bridge/Anthropic/Contract/ImageUrlNormalizer.php new file mode 100644 index 00000000..72376579 --- /dev/null +++ b/src/Platform/Bridge/Anthropic/Contract/ImageUrlNormalizer.php @@ -0,0 +1,40 @@ + 'image', + 'source' => [ + 'type' => 'url', + 'url' => $data->url, + ], + ]; + } +} diff --git a/src/Platform/Bridge/Anthropic/ModelClient.php b/src/Platform/Bridge/Anthropic/ModelClient.php new file mode 100644 index 00000000..7bfaf0d1 --- /dev/null +++ b/src/Platform/Bridge/Anthropic/ModelClient.php @@ -0,0 +1,44 @@ +httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); + } + + public function supports(Model $model): bool + { + return $model instanceof Claude; + } + + public function request(Model $model, array|string $payload, array $options = []): ResponseInterface + { + if (isset($options['tools'])) { + $options['tool_choice'] = ['type' => 'auto']; + } + + return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ + 'headers' => [ + 'x-api-key' => $this->apiKey, + 'anthropic-version' => $this->version, + ], + 'json' => array_merge($options, $payload), + ]); + } +} diff --git a/src/Platform/Bridge/Anthropic/PlatformFactory.php b/src/Platform/Bridge/Anthropic/PlatformFactory.php index 56d9f9b5..b9bea890 100644 --- a/src/Platform/Bridge/Anthropic/PlatformFactory.php +++ b/src/Platform/Bridge/Anthropic/PlatformFactory.php @@ -5,6 +5,10 @@ namespace PhpLlm\LlmChain\Platform\Bridge\Anthropic; use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\AssistantMessageNormalizer; +use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\DocumentNormalizer; +use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\DocumentUrlNormalizer; +use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\ImageNormalizer; +use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\ImageUrlNormalizer; use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\MessageBagNormalizer; use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\ToolCallMessageNormalizer; use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Contract\ToolNormalizer; @@ -22,13 +26,20 @@ public static function create( ?HttpClientInterface $httpClient = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); - $responseHandler = new ModelHandler($httpClient, $apiKey, $version); - return new Platform([$responseHandler], [$responseHandler], Contract::create( - new AssistantMessageNormalizer(), - new MessageBagNormalizer(), - new ToolCallMessageNormalizer(), - new ToolNormalizer(), - )); + return new Platform( + [new ModelClient($httpClient, $apiKey, $version)], + [new ResponseConverter()], + Contract::create( + new AssistantMessageNormalizer(), + new DocumentNormalizer(), + new DocumentUrlNormalizer(), + new ImageNormalizer(), + new ImageUrlNormalizer(), + new MessageBagNormalizer(), + new ToolCallMessageNormalizer(), + new ToolNormalizer(), + ) + ); } } diff --git a/src/Platform/Bridge/Anthropic/ModelHandler.php b/src/Platform/Bridge/Anthropic/ResponseConverter.php similarity index 69% rename from src/Platform/Bridge/Anthropic/ModelHandler.php rename to src/Platform/Bridge/Anthropic/ResponseConverter.php index 29e097f3..e84d4eb6 100644 --- a/src/Platform/Bridge/Anthropic/ModelHandler.php +++ b/src/Platform/Bridge/Anthropic/ResponseConverter.php @@ -1,12 +1,9 @@ httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); - } - public function supports(Model $model): bool { return $model instanceof Claude; } - public function request(Model $model, array|string $payload, array $options = []): ResponseInterface - { - if (isset($options['tools'])) { - $options['tool_choice'] = ['type' => 'auto']; - } - - return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [ - 'headers' => [ - 'x-api-key' => $this->apiKey, - 'anthropic-version' => $this->version, - ], - 'json' => array_merge($options, $payload), - ]); - } - public function convert(ResponseInterface $response, array $options = []): LlmResponse { if ($options['stream'] ?? false) { diff --git a/src/Platform/Bridge/Bedrock/Platform.php b/src/Platform/Bridge/Bedrock/Platform.php index 8edf4917..dd57ce4a 100644 --- a/src/Platform/Bridge/Bedrock/Platform.php +++ b/src/Platform/Bridge/Bedrock/Platform.php @@ -27,7 +27,10 @@ public function __construct( ) { $this->contract = $contract ?? Contract::create( new AnthropicContract\AssistantMessageNormalizer(), + new AnthropicContract\DocumentNormalizer(), + new AnthropicContract\DocumentUrlNormalizer(), new AnthropicContract\ImageNormalizer(), + new AnthropicContract\ImageUrlNormalizer(), new AnthropicContract\MessageBagNormalizer(), new AnthropicContract\ToolCallMessageNormalizer(), new AnthropicContract\ToolNormalizer(), diff --git a/src/Platform/Message/Content/Document.php b/src/Platform/Message/Content/Document.php new file mode 100644 index 00000000..267f3710 --- /dev/null +++ b/src/Platform/Message/Content/Document.php @@ -0,0 +1,9 @@ +request('POST', 'https://api.anthropic.com/v1/messages'); - $handler = new ModelHandler($httpClient, 'test-api-key'); + $handler = new ResponseConverter(); $response = $handler->convert($httpResponse); self::assertInstanceOf(ToolCallResponse::class, $response);