Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions examples/image-describer-binary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use PhpLlm\LlmChain\Chain;
use PhpLlm\LlmChain\Message\Content\Image;
use PhpLlm\LlmChain\Message\Message;
use PhpLlm\LlmChain\Message\MessageBag;
use PhpLlm\LlmChain\OpenAI\Model\Gpt;
use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version;
use PhpLlm\LlmChain\OpenAI\Platform\OpenAI;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\HttpClient\HttpClient;

require_once dirname(__DIR__).'/vendor/autoload.php';
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

if (empty($_ENV['OPENAI_API_KEY'])) {
echo 'Please set the OPENAI_API_KEY environment variable.'.PHP_EOL;
exit(1);
}

$platform = new OpenAI(HttpClient::create(), $_ENV['OPENAI_API_KEY']);
$llm = new Gpt($platform, Version::gpt4oMini());

$chain = new Chain($llm);
$messages = new MessageBag(
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
Message::ofUser(
'Describe the image as a comedian would do it.',
new Image(dirname(__DIR__).'/tests/Fixture/image.png'),
),
);
$response = $chain->call($messages);

echo $response->getContent().PHP_EOL;
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,8 @@
$messages = new MessageBag(
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
Message::ofUser(
'Describe the images as a comedian would do it.',
'Describe the image as a comedian would do it.',
new Image('https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Webysther_20160423_-_Elephpant.svg/350px-Webysther_20160423_-_Elephpant.svg.png'),
new Image('https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/African_Bush_Elephant.jpg/320px-African_Bush_Elephant.jpg'),
),
);
$response = $chain->call($messages);
Expand Down
28 changes: 24 additions & 4 deletions src/Message/Content/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@

namespace PhpLlm\LlmChain\Message\Content;

use PhpLlm\LlmChain\Exception\InvalidArgumentException;

final readonly class Image implements Content
{
public string $url;

/**
* @param string $url An URL like "http://localhost:3000/my-image.png" or a data url like "data:image/png;base64,iVBOR[...]"
* @param string $url An URL like "http://localhost:3000/my-image.png", a data url like "data:image/png;base64,iVBOR[...]"
* or a file path like "/path/to/my-image.png".
*/
public function __construct(
public string $url,
) {
public function __construct(string $url)
{
if (!str_starts_with($url, 'http') && !str_starts_with($url, 'data:')) {
$url = $this->fromFile($url);
}

$this->url = $url;
}

/**
Expand All @@ -21,4 +30,15 @@ public function jsonSerialize(): array
{
return ['type' => 'image_url', 'image_url' => ['url' => $this->url]];
}

private function fromFile(string $filePath): string
{
if (!is_readable($filePath) || false === $data = file_get_contents($filePath)) {
throw new InvalidArgumentException(sprintf('The file "%s" does not exist or is not readable.', $filePath));
}

$type = pathinfo($filePath, PATHINFO_EXTENSION);

return sprintf('data:image/%s;base64,%s', $type, base64_encode($data));
}
}
Binary file added tests/Fixture/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 29 additions & 5 deletions tests/Message/Content/ImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,42 @@
final class ImageTest extends TestCase
{
#[Test]
public function constructionIsPossible(): void
public function constructWithValidUrl(): void
{
$obj = new Image('foo');
$image = new Image('https://foo.com/test.png');

self::assertSame('foo', $obj->url);
self::assertSame('https://foo.com/test.png', $image->url);
}

#[Test]
public function constructWithValidDataUrl(): void
{
$image = new Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABKklEQVR42mNk+A8AAwMhIv9n+X');

self::assertStringStartsWith('data:image/png;base64', $image->url);
}

#[Test]
public function withValidFile(): void
{
$image = new Image(dirname(__DIR__, 2).'/Fixture/image.png');

self::assertStringStartsWith('data:image/png;base64,', $image->url);
}

#[Test]
public function fromBinaryWithInvalidFile(): void
{
$this->expectExceptionMessage('The file "foo.jpg" does not exist or is not readable.');

new Image('foo.jpg');
}

#[Test]
public function jsonConversionIsWorkingAsExpected(): void
{
$obj = new Image('foo');
$image = new Image('https://foo.com/test.png');

self::assertSame(['type' => 'image_url', 'image_url' => ['url' => 'foo']], $obj->jsonSerialize());
self::assertSame(['type' => 'image_url', 'image_url' => ['url' => 'https://foo.com/test.png']], $image->jsonSerialize());
}
}
14 changes: 7 additions & 7 deletions tests/Message/UserMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function constructionIsPossible(): void
#[Test]
public function constructionIsPossibleWithMultipleContent(): void
{
$message = new UserMessage(new Text('foo'), new Image('bar'));
$message = new UserMessage(new Text('foo'), new Image('https://foo.com/bar.jpg'));

self::assertCount(2, $message->content);
}
Expand All @@ -50,7 +50,7 @@ public function hasImageContentWithoutImage(): void
#[Test]
public function hasImageContentWithImage(): void
{
$message = new UserMessage(new Text('foo'), new Image('bar'));
$message = new UserMessage(new Text('foo'), new Image('https://foo.com/bar.jpg'));

self::assertTrue($message->hasImageContent());
}
Expand All @@ -70,24 +70,24 @@ public static function provideSerializationTests(): \Generator
];

yield 'With single image' => [
new UserMessage(new Text('foo'), new Image('bar')),
new UserMessage(new Text('foo'), new Image('https://foo.com/bar.jpg')),
[
'role' => Role::User,
'content' => [
['type' => 'text', 'text' => 'foo'],
['type' => 'image_url', 'image_url' => ['url' => 'bar']],
['type' => 'image_url', 'image_url' => ['url' => 'https://foo.com/bar.jpg']],
],
],
];

yield 'With single multiple images' => [
new UserMessage(new Text('foo'), new Image('bar'), new Image('baz')),
new UserMessage(new Text('foo'), new Image('https://foo.com/bar.jpg'), new Image('https://foo.com/baz.jpg')),
[
'role' => Role::User,
'content' => [
['type' => 'text', 'text' => 'foo'],
['type' => 'image_url', 'image_url' => ['url' => 'bar']],
['type' => 'image_url', 'image_url' => ['url' => 'baz']],
['type' => 'image_url', 'image_url' => ['url' => 'https://foo.com/bar.jpg']],
['type' => 'image_url', 'image_url' => ['url' => 'https://foo.com/baz.jpg']],
],
],
];
Expand Down