From af16dc5641ee4ad2f524f7d11cc0bca18d4239a0 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 17:59:46 +0200 Subject: [PATCH 1/8] Allow usage of gpt visions api --- examples/image-describer.php | 27 ++++++++ phpstan.dist.neon | 5 -- src/Message/AssistantMessage.php | 53 +++++++++++++++ src/Message/Content/ContentInterface.php | 9 +++ src/Message/Content/ImageUrlContent.php | 20 ++++++ src/Message/Content/TextContent.php | 20 ++++++ src/Message/Message.php | 85 ++++-------------------- src/Message/MessageBag.php | 19 +++--- src/Message/MessageInterface.php | 10 +++ src/Message/SystemMessage.php | 31 +++++++++ src/Message/ToolCallMessage.php | 37 +++++++++++ src/Message/UserMessage.php | 56 ++++++++++++++++ tests/Message/MessageBagTest.php | 32 +++++++-- tests/Message/MessageTest.php | 79 +--------------------- 14 files changed, 317 insertions(+), 166 deletions(-) create mode 100755 examples/image-describer.php create mode 100644 src/Message/AssistantMessage.php create mode 100644 src/Message/Content/ContentInterface.php create mode 100644 src/Message/Content/ImageUrlContent.php create mode 100644 src/Message/Content/TextContent.php create mode 100644 src/Message/MessageInterface.php create mode 100644 src/Message/SystemMessage.php create mode 100644 src/Message/ToolCallMessage.php create mode 100644 src/Message/UserMessage.php diff --git a/examples/image-describer.php b/examples/image-describer.php new file mode 100755 index 00000000..6fcd12f2 --- /dev/null +++ b/examples/image-describer.php @@ -0,0 +1,27 @@ +call($messages); + +echo $response.PHP_EOL; diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ae5c8505..138b9b5e 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -4,8 +4,3 @@ parameters: - examples/ - src/ - tests/ - ignoreErrors: - - - message: '#no value type specified in iterable type array#' - path: tests/* - diff --git a/src/Message/AssistantMessage.php b/src/Message/AssistantMessage.php new file mode 100644 index 00000000..23eae76e --- /dev/null +++ b/src/Message/AssistantMessage.php @@ -0,0 +1,53 @@ +toolCalls && 0 !== \count($this->toolCalls); + } + + /** + * @return array{ + * role: Role::Assistant, + * content: ?string, + * tool_calls?: ToolCall[], + * } + */ + public function jsonSerialize(): array + { + $array = [ + 'role' => Role::Assistant, + ]; + + if (null !== $this->content) { + $array['content'] = $this->content; + } + + if ($this->hasToolCalls()) { + $array['tool_calls'] = $this->toolCalls; + } + + return $array; + } +} diff --git a/src/Message/Content/ContentInterface.php b/src/Message/Content/ContentInterface.php new file mode 100644 index 00000000..8e60b66a --- /dev/null +++ b/src/Message/Content/ContentInterface.php @@ -0,0 +1,9 @@ + 'image_url', 'image_url' => ['url' => $this->imageUrl]]; + } +} diff --git a/src/Message/Content/TextContent.php b/src/Message/Content/TextContent.php new file mode 100644 index 00000000..c265302f --- /dev/null +++ b/src/Message/Content/TextContent.php @@ -0,0 +1,20 @@ + 'text', 'text' => $this->text]; + } +} diff --git a/src/Message/Message.php b/src/Message/Message.php index 57fd59de..26202ab0 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -4,94 +4,37 @@ namespace PhpLlm\LlmChain\Message; +use PhpLlm\LlmChain\Message\Content\ImageUrlContent; +use PhpLlm\LlmChain\Message\Content\TextContent; use PhpLlm\LlmChain\Response\ToolCall; -final readonly class Message implements \JsonSerializable +final readonly class Message { - /** - * @param ?ToolCall[] $toolCalls - */ - public function __construct( - public ?string $content, - public Role $role, - public ?array $toolCalls = null, - ) { + // Disabled by default, just a bridge to the specific messages + private function __construct() + { } - public static function forSystem(string $content): self + public static function forSystem(string $content): SystemMessage { - return new self($content, Role::System); + return new SystemMessage($content); } /** * @param ?ToolCall[] $toolCalls */ - public static function ofAssistant(?string $content = null, ?array $toolCalls = null): self - { - return new self($content, Role::Assistant, $toolCalls); - } - - public static function ofUser(string $content): self - { - return new self($content, Role::User); - } - - public static function ofToolCall(ToolCall $toolCall, string $content): self - { - return new self($content, Role::ToolCall, [$toolCall]); - } - - public function isSystem(): bool - { - return Role::System === $this->role; - } - - public function isAssistant(): bool - { - return Role::Assistant === $this->role; - } - - public function isUser(): bool - { - return Role::User === $this->role; - } - - public function isToolCall(): bool + public static function ofAssistant(?string $content = null, ?array $toolCalls = null): AssistantMessage { - return Role::ToolCall === $this->role; + return new AssistantMessage($content, $toolCalls); } - public function hasToolCalls(): bool + public static function ofUser(string|TextContent $content, ImageUrlContent|string ...$images): UserMessage { - return null !== $this->toolCalls && 0 !== count($this->toolCalls); + return new UserMessage($content, ...$images); } - /** - * @return array{ - * role: 'system'|'assistant'|'user'|'tool', - * content: ?string, - * tool_calls?: ToolCall[], - * tool_call_id?: string - * } - */ - public function jsonSerialize(): array + public static function ofToolCall(ToolCall $toolCall, string $content): ToolCallMessage { - $array = [ - 'role' => $this->role->value, - ]; - - if (null !== $this->content) { - $array['content'] = $this->content; - } - - if ($this->hasToolCalls() && $this->isToolCall()) { - $array['tool_call_id'] = $this->toolCalls[0]->id; - } - - if ($this->hasToolCalls() && $this->isAssistant()) { - $array['tool_calls'] = $this->toolCalls; - } - - return $array; + return new ToolCallMessage($toolCall, $content); } } diff --git a/src/Message/MessageBag.php b/src/Message/MessageBag.php index 19ff56a3..0f1dcbcc 100644 --- a/src/Message/MessageBag.php +++ b/src/Message/MessageBag.php @@ -5,19 +5,19 @@ namespace PhpLlm\LlmChain\Message; /** - * @template-extends \ArrayObject + * @template-extends \ArrayObject */ final class MessageBag extends \ArrayObject implements \JsonSerializable { - public function __construct(Message ...$messages) + public function __construct(MessageInterface ...$messages) { parent::__construct(array_values($messages)); } - public function getSystemMessage(): ?Message + public function getSystemMessage(): ?SystemMessage { foreach ($this as $message) { - if (Role::System === $message->role) { + if ($message instanceof SystemMessage) { return $message; } } @@ -25,7 +25,7 @@ public function getSystemMessage(): ?Message return null; } - public function with(Message $message): self + public function with(MessageInterface $message): self { $messages = clone $this; $messages->append($message); @@ -45,13 +45,16 @@ public function withoutSystemMessage(): self { $messages = clone $this; $messages->exchangeArray( - array_values(array_filter($messages->getArrayCopy(), fn (Message $message) => !$message->isSystem())) + array_values(array_filter( + $messages->getArrayCopy(), + static fn (MessageInterface $message) => !$message instanceof SystemMessage, + )) ); return $messages; } - public function prepend(Message $message): self + public function prepend(MessageInterface $message): self { $messages = clone $this; $messages->exchangeArray(array_merge([$message], $messages->getArrayCopy())); @@ -60,7 +63,7 @@ public function prepend(Message $message): self } /** - * @return Message[] + * @return MessageInterface[] */ public function jsonSerialize(): array { diff --git a/src/Message/MessageInterface.php b/src/Message/MessageInterface.php new file mode 100644 index 00000000..a6f73d0e --- /dev/null +++ b/src/Message/MessageInterface.php @@ -0,0 +1,10 @@ + Role::System, + 'content' => $this->content, + ]; + } +} diff --git a/src/Message/ToolCallMessage.php b/src/Message/ToolCallMessage.php new file mode 100644 index 00000000..2595f12c --- /dev/null +++ b/src/Message/ToolCallMessage.php @@ -0,0 +1,37 @@ + Role::ToolCall, + 'content' => $this->content, + 'tool_call_id' => $this->toolCall->id, + ]; + } +} diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php new file mode 100644 index 00000000..3ced1ee8 --- /dev/null +++ b/src/Message/UserMessage.php @@ -0,0 +1,56 @@ + + */ + public array $images; + + public function __construct( + public TextContent|string $content, + ImageUrlContent|string ...$images, + ) { + $this->images = $images; + } + + public function getRole(): Role + { + return Role::User; + } + + /** + * @return array{ + * role: Role::User, + * content: string|list + * } + */ + public function jsonSerialize(): array + { + $array = ['role' => Role::User]; + if ([] === $this->images) { + $array['content'] = \is_string($this->content) ? $this->content : $this->content->text; + + return $array; + } + + $content = \is_string($this->content) ? new TextContent($this->content) : $this->content; + + $array['content'][] = $content->jsonSerialize(); + + foreach ($this->images as $image) { + $image = \is_string($image) ? new ImageUrlContent($image) : $image; + + $array['content'][] = $image->jsonSerialize(); + } + + return $array; + } +} diff --git a/tests/Message/MessageBagTest.php b/tests/Message/MessageBagTest.php index f179d046..838e383a 100644 --- a/tests/Message/MessageBagTest.php +++ b/tests/Message/MessageBagTest.php @@ -4,8 +4,11 @@ namespace PhpLlm\LlmChain\Tests\Message; +use PhpLlm\LlmChain\Message\AssistantMessage; +use PhpLlm\LlmChain\Message\Content\ImageUrlContent; use PhpLlm\LlmChain\Message\Message; use PhpLlm\LlmChain\Message\MessageBag; +use PhpLlm\LlmChain\Message\SystemMessage; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; @@ -56,7 +59,11 @@ public function with(): void self::assertCount(3, $messageBag); self::assertCount(4, $newMessageBag); - self::assertSame('It is time to wake up.', $newMessageBag[3]->content); + + $newMessageFromBag = $newMessageBag[3]; + assert($newMessageFromBag instanceof AssistantMessage); + + self::assertSame('It is time to wake up.', $newMessageFromBag->content); } #[Test] @@ -73,7 +80,11 @@ public function merge(): void )); self::assertCount(4, $messageBag); - self::assertSame('It is time to wake up.', $messageBag[3]->content); + + $messageFromBag = $messageBag[3]; + assert($messageFromBag instanceof AssistantMessage); + + self::assertSame('It is time to wake up.', $messageFromBag->content); } #[Test] @@ -89,7 +100,11 @@ public function withoutSystemMessage(): void self::assertCount(3, $messageBag); self::assertCount(2, $newMessageBag); - self::assertSame('It is time to sleep.', $newMessageBag[0]->content); + + $messageFromNewBag = $newMessageBag[0]; + assert($messageFromNewBag instanceof AssistantMessage); + + self::assertSame('It is time to sleep.', $messageFromNewBag->content); } #[Test] @@ -105,7 +120,11 @@ public function prepend(): void self::assertCount(2, $messageBag); self::assertCount(3, $newMessageBag); - self::assertSame('My amazing system prompt.', $newMessageBag[0]->content); + + $newMessageBagMessage = $newMessageBag[0]; + assert($newMessageBagMessage instanceof SystemMessage); + + self::assertSame('My amazing system prompt.', $newMessageBagMessage->content); } #[Test] @@ -115,6 +134,7 @@ public function jsonSerialize(): void Message::forSystem('My amazing system prompt.'), Message::ofAssistant('It is time to sleep.'), Message::ofUser('Hello, world!'), + Message::ofUser('My hint for how to analyze an image.', new ImageUrlContent('http://image-generator.local/my-fancy-image.png')), ); $json = json_encode($messageBag); @@ -125,6 +145,10 @@ public function jsonSerialize(): void ['role' => 'system', 'content' => 'My amazing system prompt.'], ['role' => 'assistant', 'content' => 'It is time to sleep.'], ['role' => 'user', 'content' => 'Hello, world!'], + ['role' => 'user', 'content' => [ + ['type' => 'text', 'text' => 'My hint for how to analyze an image.'], + ['type' => 'image_url', 'image_url' => ['url' => 'http://image-generator.local/my-fancy-image.png']], + ]], ]), $json ); diff --git a/tests/Message/MessageTest.php b/tests/Message/MessageTest.php index 3e27b3fd..edff8734 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Message/MessageTest.php @@ -7,7 +7,6 @@ use PhpLlm\LlmChain\Message\Message; use PhpLlm\LlmChain\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -24,11 +23,6 @@ public function createSystemMessage(): void $message = Message::forSystem('My amazing system prompt.'); self::assertSame('My amazing system prompt.', $message->content); - self::assertTrue($message->isSystem()); - self::assertFalse($message->isAssistant()); - self::assertFalse($message->isUser()); - self::assertFalse($message->isToolCall()); - self::assertFalse($message->hasToolCalls()); } #[Test] @@ -37,11 +31,6 @@ public function createAssistantMessage(): void $message = Message::ofAssistant('It is time to sleep.'); self::assertSame('It is time to sleep.', $message->content); - self::assertFalse($message->isSystem()); - self::assertTrue($message->isAssistant()); - self::assertFalse($message->isUser()); - self::assertFalse($message->isToolCall()); - self::assertFalse($message->hasToolCalls()); } #[Test] @@ -54,10 +43,6 @@ public function createAssistantMessageWithToolCalls(): void $message = Message::ofAssistant(toolCalls: $toolCalls); self::assertCount(2, $message->toolCalls); - self::assertFalse($message->isSystem()); - self::assertTrue($message->isAssistant()); - self::assertFalse($message->isUser()); - self::assertFalse($message->isToolCall()); self::assertTrue($message->hasToolCalls()); } @@ -67,11 +52,6 @@ public function createUserMessage(): void $message = Message::ofUser('Hi, my name is John.'); self::assertSame('Hi, my name is John.', $message->content); - self::assertFalse($message->isSystem()); - self::assertFalse($message->isAssistant()); - self::assertTrue($message->isUser()); - self::assertFalse($message->isToolCall()); - self::assertFalse($message->hasToolCalls()); } #[Test] @@ -81,63 +61,6 @@ public function createToolCallMessage(): void $message = Message::ofToolCall($toolCall, 'Foo bar.'); self::assertSame('Foo bar.', $message->content); - self::assertCount(1, $message->toolCalls); - self::assertFalse($message->isSystem()); - self::assertFalse($message->isAssistant()); - self::assertFalse($message->isUser()); - self::assertTrue($message->isToolCall()); - self::assertTrue($message->hasToolCalls()); - } - - #[DataProvider('provideJsonScenarios')] - #[Test] - public function jsonSerialize(Message $message, array $expected): void - { - self::assertSame($expected, $message->jsonSerialize()); - } - - public static function provideJsonScenarios(): array - { - $toolCall1 = new ToolCall('call_123456', 'my_tool', ['foo' => 'bar']); - $toolCall2 = new ToolCall('call_456789', 'my_faster_tool'); - - return [ - 'system' => [ - Message::forSystem('My amazing system prompt.'), - [ - 'role' => 'system', - 'content' => 'My amazing system prompt.', - ], - ], - 'assistant' => [ - Message::ofAssistant('It is time to sleep.'), - [ - 'role' => 'assistant', - 'content' => 'It is time to sleep.', - ], - ], - 'assistant_with_tool_calls' => [ - Message::ofAssistant(toolCalls: [$toolCall1, $toolCall2]), - [ - 'role' => 'assistant', - 'tool_calls' => [$toolCall1, $toolCall2], - ], - ], - 'user' => [ - Message::ofUser('Hi, my name is John.'), - [ - 'role' => 'user', - 'content' => 'Hi, my name is John.', - ], - ], - 'tool_call' => [ - Message::ofToolCall($toolCall1, 'Foo bar.'), - [ - 'role' => 'tool', - 'content' => 'Foo bar.', - 'tool_call_id' => 'call_123456', - ], - ], - ]; + self::assertSame($toolCall, $message->toolCall); } } From 461f8a6a35091984a7724a5c15e675d15712f55c Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 18:39:52 +0200 Subject: [PATCH 2/8] Add tests --- phpstan.dist.neon | 5 ++ src/Message/AssistantMessage.php | 2 +- tests/Message/AssistantMessageTest.php | 70 +++++++++++++++ tests/Message/Content/ImageUrlContentTest.php | 35 ++++++++ tests/Message/Content/TextContentTest.php | 35 ++++++++ tests/Message/MessageBagTest.php | 2 + tests/Message/MessageTest.php | 9 ++ tests/Message/SystemMessageTest.php | 37 ++++++++ tests/Message/ToolCallMessageTest.php | 42 +++++++++ tests/Message/UserMessageTest.php | 87 +++++++++++++++++++ 10 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 tests/Message/AssistantMessageTest.php create mode 100644 tests/Message/Content/ImageUrlContentTest.php create mode 100644 tests/Message/Content/TextContentTest.php create mode 100644 tests/Message/SystemMessageTest.php create mode 100644 tests/Message/ToolCallMessageTest.php create mode 100644 tests/Message/UserMessageTest.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 138b9b5e..ae5c8505 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -4,3 +4,8 @@ parameters: - examples/ - src/ - tests/ + ignoreErrors: + - + message: '#no value type specified in iterable type array#' + path: tests/* + diff --git a/src/Message/AssistantMessage.php b/src/Message/AssistantMessage.php index 23eae76e..925b4806 100644 --- a/src/Message/AssistantMessage.php +++ b/src/Message/AssistantMessage.php @@ -12,7 +12,7 @@ * @param ?ToolCall[] $toolCalls */ public function __construct( - public ?string $content = '', + public ?string $content = null, public ?array $toolCalls = null, ) { } diff --git a/tests/Message/AssistantMessageTest.php b/tests/Message/AssistantMessageTest.php new file mode 100644 index 00000000..24dbfb11 --- /dev/null +++ b/tests/Message/AssistantMessageTest.php @@ -0,0 +1,70 @@ +getRole()); + } + + #[Test] + public function constructionWithoutToolCallIsPossible(): void + { + $message = new AssistantMessage('foo'); + + self::assertSame('foo', $message->content); + self::assertNull($message->toolCalls); + } + + #[Test] + public function constructionWithoutContentIsPossible(): void + { + $toolCall = new ToolCall('foo', 'foo'); + $message = new AssistantMessage(toolCalls: [$toolCall]); + + self::assertNull($message->content); + self::assertSame([$toolCall], $message->toolCalls); + self::assertTrue($message->hasToolCalls()); + } + + #[Test] + #[DataProvider('provideJsonSerializerTests')] + public function jsonConversionIsWorkingAsExpected(AssistantMessage $message, array $expectedResult): void + { + self::assertEqualsCanonicalizing($expectedResult, $message->jsonSerialize()); + } + + public static function provideJsonSerializerTests(): \Generator + { + yield 'Message with content' => [ + new AssistantMessage('Foo Bar Baz'), + ['role' => Role::Assistant, 'content' => 'Foo Bar Baz'], + ]; + + $toolCall1 = new ToolCall('call_123456', 'my_tool', ['foo' => 'bar']); + $toolCall2 = new ToolCall('call_456789', 'my_faster_tool'); + + yield 'Message with tool calls' => [ + new AssistantMessage(toolCalls: [$toolCall1, $toolCall2]), + ['role' => Role::Assistant, 'tool_calls' => [$toolCall1, $toolCall2]], + ]; + } +} diff --git a/tests/Message/Content/ImageUrlContentTest.php b/tests/Message/Content/ImageUrlContentTest.php new file mode 100644 index 00000000..5e2b3071 --- /dev/null +++ b/tests/Message/Content/ImageUrlContentTest.php @@ -0,0 +1,35 @@ +imageUrl); + } + + #[Test] + public function jsonConversionIsWorkingAsExpected(): void + { + $obj = new ImageUrlContent('foo'); + + self::assertSame( + ['type' => 'image_url', 'image_url' => ['url' => 'foo']], + $obj->jsonSerialize(), + ); + } +} diff --git a/tests/Message/Content/TextContentTest.php b/tests/Message/Content/TextContentTest.php new file mode 100644 index 00000000..8e230745 --- /dev/null +++ b/tests/Message/Content/TextContentTest.php @@ -0,0 +1,35 @@ +text); + } + + #[Test] + public function jsonConversionIsWorkingAsExpected(): void + { + $obj = new TextContent('foo'); + + self::assertSame( + ['type' => 'text', 'text' => 'foo'], + $obj->jsonSerialize(), + ); + } +} diff --git a/tests/Message/MessageBagTest.php b/tests/Message/MessageBagTest.php index 838e383a..efcb2edf 100644 --- a/tests/Message/MessageBagTest.php +++ b/tests/Message/MessageBagTest.php @@ -134,6 +134,7 @@ public function jsonSerialize(): void Message::forSystem('My amazing system prompt.'), Message::ofAssistant('It is time to sleep.'), Message::ofUser('Hello, world!'), + new AssistantMessage('Hello User!'), Message::ofUser('My hint for how to analyze an image.', new ImageUrlContent('http://image-generator.local/my-fancy-image.png')), ); @@ -145,6 +146,7 @@ public function jsonSerialize(): void ['role' => 'system', 'content' => 'My amazing system prompt.'], ['role' => 'assistant', 'content' => 'It is time to sleep.'], ['role' => 'user', 'content' => 'Hello, world!'], + ['role' => 'assistant', 'content' => 'Hello User!'], ['role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'My hint for how to analyze an image.'], ['type' => 'image_url', 'image_url' => ['url' => 'http://image-generator.local/my-fancy-image.png']], diff --git a/tests/Message/MessageTest.php b/tests/Message/MessageTest.php index edff8734..0776fe31 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Message/MessageTest.php @@ -54,6 +54,15 @@ public function createUserMessage(): void self::assertSame('Hi, my name is John.', $message->content); } + #[Test] + public function createUserMessageWithImages(): void + { + $message = Message::ofUser('Hi, my name is John.', 'http://images.local/my-image.png', 'http://images.local/my-image2.png'); + + self::assertSame('Hi, my name is John.', $message->content); + self::assertSame(['http://images.local/my-image.png', 'http://images.local/my-image2.png'], $message->images); + } + #[Test] public function createToolCallMessage(): void { diff --git a/tests/Message/SystemMessageTest.php b/tests/Message/SystemMessageTest.php new file mode 100644 index 00000000..addf89cd --- /dev/null +++ b/tests/Message/SystemMessageTest.php @@ -0,0 +1,37 @@ +getRole()); + self::assertSame('foo', $message->content); + } + + #[Test] + public function jsonConversionIsWorkingAsExpected(): void + { + $systemMessage = new SystemMessage('foo'); + + self::assertSame( + ['role' => Role::System, 'content' => 'foo'], + $systemMessage->jsonSerialize(), + ); + } +} diff --git a/tests/Message/ToolCallMessageTest.php b/tests/Message/ToolCallMessageTest.php new file mode 100644 index 00000000..378c4f3c --- /dev/null +++ b/tests/Message/ToolCallMessageTest.php @@ -0,0 +1,42 @@ +toolCall); + self::assertSame('bar', $obj->content); + } + + #[Test] + public function jsonConversionIsWorkingAsExpected(): void + { + $toolCall = new ToolCall('foo', 'bar'); + $obj = new ToolCallMessage($toolCall, 'bar'); + + self::assertSame( + ['role' => Role::ToolCall, 'content' => 'bar', 'tool_call_id' => 'foo'], + $obj->jsonSerialize(), + ); + } +} diff --git a/tests/Message/UserMessageTest.php b/tests/Message/UserMessageTest.php new file mode 100644 index 00000000..48ce1d59 --- /dev/null +++ b/tests/Message/UserMessageTest.php @@ -0,0 +1,87 @@ +content); + self::assertSame([], $obj->images); + self::assertSame(Role::User, $obj->getRole()); + } + + #[Test] + #[DataProvider('provideSerializationTests')] + public function serializationResultsAsExpected(UserMessage $message, array $expectedArray): void + { + self::assertSame($message->jsonSerialize(), $expectedArray); + } + + public static function provideSerializationTests(): \Generator + { + yield 'With only string content' => [ + new UserMessage('foo'), + ['role' => Role::User, 'content' => 'foo'], + ]; + + yield 'With only TextContent' => [ + new UserMessage(new TextContent('foo')), + ['role' => Role::User, 'content' => 'foo'], + ]; + + yield 'With single image as string' => [ + new UserMessage('foo', 'bar'), + [ + 'role' => Role::User, + 'content' => [ + ['type' => 'text', 'text' => 'foo'], + ['type' => 'image_url', 'image_url' => ['url' => 'bar']], + ], + ], + ]; + + yield 'With single image as ImageUrlContent' => [ + new UserMessage('foo', new ImageUrlContent('bar')), + [ + 'role' => Role::User, + 'content' => [ + ['type' => 'text', 'text' => 'foo'], + ['type' => 'image_url', 'image_url' => ['url' => 'bar']], + ], + ], + ]; + + yield 'With single mixed images' => [ + new UserMessage('foo', 'bar', new ImageUrlContent('baz')), + [ + 'role' => Role::User, + 'content' => [ + ['type' => 'text', 'text' => 'foo'], + ['type' => 'image_url', 'image_url' => ['url' => 'bar']], + ['type' => 'image_url', 'image_url' => ['url' => 'baz']], + ], + ], + ]; + } +} From 4e37cd2f7848e0fa27920c54faf58a1584ad6deb Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 18:45:11 +0200 Subject: [PATCH 3/8] Rename content classes --- .../Content/{ImageUrlContent.php => Image.php} | 6 +++--- src/Message/Content/{TextContent.php => Text.php} | 2 +- src/Message/Message.php | 6 +++--- src/Message/UserMessage.php | 14 +++++++------- .../{ImageUrlContentTest.php => ImageTest.php} | 12 ++++++------ tests/Message/Content/TextContentTest.php | 8 ++++---- tests/Message/MessageBagTest.php | 4 ++-- tests/Message/UserMessageTest.php | 14 +++++++------- 8 files changed, 33 insertions(+), 33 deletions(-) rename src/Message/Content/{ImageUrlContent.php => Image.php} (68%) rename src/Message/Content/{TextContent.php => Text.php} (84%) rename tests/Message/Content/{ImageUrlContentTest.php => ImageTest.php} (66%) diff --git a/src/Message/Content/ImageUrlContent.php b/src/Message/Content/Image.php similarity index 68% rename from src/Message/Content/ImageUrlContent.php rename to src/Message/Content/Image.php index 682dbbe5..e1f4d6b0 100644 --- a/src/Message/Content/ImageUrlContent.php +++ b/src/Message/Content/Image.php @@ -4,9 +4,9 @@ namespace PhpLlm\LlmChain\Message\Content; -final readonly class ImageUrlContent implements ContentInterface +final readonly class Image implements ContentInterface { - public function __construct(public string $imageUrl) + public function __construct(public string $image) { } @@ -15,6 +15,6 @@ public function __construct(public string $imageUrl) */ public function jsonSerialize(): array { - return ['type' => 'image_url', 'image_url' => ['url' => $this->imageUrl]]; + return ['type' => 'image_url', 'image_url' => ['url' => $this->image]]; } } diff --git a/src/Message/Content/TextContent.php b/src/Message/Content/Text.php similarity index 84% rename from src/Message/Content/TextContent.php rename to src/Message/Content/Text.php index c265302f..c5531c6c 100644 --- a/src/Message/Content/TextContent.php +++ b/src/Message/Content/Text.php @@ -4,7 +4,7 @@ namespace PhpLlm\LlmChain\Message\Content; -final readonly class TextContent implements ContentInterface +final readonly class Text implements ContentInterface { public function __construct(public string $text) { diff --git a/src/Message/Message.php b/src/Message/Message.php index 26202ab0..316a893a 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -4,8 +4,8 @@ namespace PhpLlm\LlmChain\Message; -use PhpLlm\LlmChain\Message\Content\ImageUrlContent; -use PhpLlm\LlmChain\Message\Content\TextContent; +use PhpLlm\LlmChain\Message\Content\Image; +use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Response\ToolCall; final readonly class Message @@ -28,7 +28,7 @@ public static function ofAssistant(?string $content = null, ?array $toolCalls = return new AssistantMessage($content, $toolCalls); } - public static function ofUser(string|TextContent $content, ImageUrlContent|string ...$images): UserMessage + public static function ofUser(string|Text $content, Image|string ...$images): UserMessage { return new UserMessage($content, ...$images); } diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php index 3ced1ee8..b40c3b37 100644 --- a/src/Message/UserMessage.php +++ b/src/Message/UserMessage.php @@ -4,19 +4,19 @@ namespace PhpLlm\LlmChain\Message; -use PhpLlm\LlmChain\Message\Content\ImageUrlContent; -use PhpLlm\LlmChain\Message\Content\TextContent; +use PhpLlm\LlmChain\Message\Content\Image; +use PhpLlm\LlmChain\Message\Content\Text; final readonly class UserMessage implements MessageInterface { /** - * @var list + * @var list */ public array $images; public function __construct( - public TextContent|string $content, - ImageUrlContent|string ...$images, + public Text|string $content, + Image|string ...$images, ) { $this->images = $images; } @@ -41,12 +41,12 @@ public function jsonSerialize(): array return $array; } - $content = \is_string($this->content) ? new TextContent($this->content) : $this->content; + $content = \is_string($this->content) ? new Text($this->content) : $this->content; $array['content'][] = $content->jsonSerialize(); foreach ($this->images as $image) { - $image = \is_string($image) ? new ImageUrlContent($image) : $image; + $image = \is_string($image) ? new Image($image) : $image; $array['content'][] = $image->jsonSerialize(); } diff --git a/tests/Message/Content/ImageUrlContentTest.php b/tests/Message/Content/ImageTest.php similarity index 66% rename from tests/Message/Content/ImageUrlContentTest.php rename to tests/Message/Content/ImageTest.php index 5e2b3071..f6d756da 100644 --- a/tests/Message/Content/ImageUrlContentTest.php +++ b/tests/Message/Content/ImageTest.php @@ -4,28 +4,28 @@ namespace PhpLlm\LlmChain\Tests\Message\Content; -use PhpLlm\LlmChain\Message\Content\ImageUrlContent; +use PhpLlm\LlmChain\Message\Content\Image; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversClass(ImageUrlContent::class)] +#[CoversClass(Image::class)] #[Small] -final class ImageUrlContentTest extends TestCase +final class ImageTest extends TestCase { #[Test] public function constructionIsPossible(): void { - $obj = new ImageUrlContent('foo'); + $obj = new Image('foo'); - self::assertSame('foo', $obj->imageUrl); + self::assertSame('foo', $obj->image); } #[Test] public function jsonConversionIsWorkingAsExpected(): void { - $obj = new ImageUrlContent('foo'); + $obj = new Image('foo'); self::assertSame( ['type' => 'image_url', 'image_url' => ['url' => 'foo']], diff --git a/tests/Message/Content/TextContentTest.php b/tests/Message/Content/TextContentTest.php index 8e230745..ac67c908 100644 --- a/tests/Message/Content/TextContentTest.php +++ b/tests/Message/Content/TextContentTest.php @@ -4,20 +4,20 @@ namespace PhpLlm\LlmChain\Tests\Message\Content; -use PhpLlm\LlmChain\Message\Content\TextContent; +use PhpLlm\LlmChain\Message\Content\Text; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -#[CoversClass(TextContent::class)] +#[CoversClass(Text::class)] #[Small] final class TextContentTest extends TestCase { #[Test] public function constructionIsPossible(): void { - $obj = new TextContent('foo'); + $obj = new Text('foo'); self::assertSame('foo', $obj->text); } @@ -25,7 +25,7 @@ public function constructionIsPossible(): void #[Test] public function jsonConversionIsWorkingAsExpected(): void { - $obj = new TextContent('foo'); + $obj = new Text('foo'); self::assertSame( ['type' => 'text', 'text' => 'foo'], diff --git a/tests/Message/MessageBagTest.php b/tests/Message/MessageBagTest.php index efcb2edf..99c79834 100644 --- a/tests/Message/MessageBagTest.php +++ b/tests/Message/MessageBagTest.php @@ -5,7 +5,7 @@ namespace PhpLlm\LlmChain\Tests\Message; use PhpLlm\LlmChain\Message\AssistantMessage; -use PhpLlm\LlmChain\Message\Content\ImageUrlContent; +use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Message; use PhpLlm\LlmChain\Message\MessageBag; use PhpLlm\LlmChain\Message\SystemMessage; @@ -135,7 +135,7 @@ public function jsonSerialize(): void Message::ofAssistant('It is time to sleep.'), Message::ofUser('Hello, world!'), new AssistantMessage('Hello User!'), - Message::ofUser('My hint for how to analyze an image.', new ImageUrlContent('http://image-generator.local/my-fancy-image.png')), + Message::ofUser('My hint for how to analyze an image.', new Image('http://image-generator.local/my-fancy-image.png')), ); $json = json_encode($messageBag); diff --git a/tests/Message/UserMessageTest.php b/tests/Message/UserMessageTest.php index 48ce1d59..96ea77ec 100644 --- a/tests/Message/UserMessageTest.php +++ b/tests/Message/UserMessageTest.php @@ -4,8 +4,8 @@ namespace PhpLlm\LlmChain\Tests\Message; -use PhpLlm\LlmChain\Message\Content\ImageUrlContent; -use PhpLlm\LlmChain\Message\Content\TextContent; +use PhpLlm\LlmChain\Message\Content\Image; +use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Message\Role; use PhpLlm\LlmChain\Message\UserMessage; use PHPUnit\Framework\Attributes\CoversClass; @@ -16,8 +16,8 @@ use PHPUnit\Framework\TestCase; #[CoversClass(UserMessage::class)] -#[UsesClass(TextContent::class)] -#[UsesClass(ImageUrlContent::class)] +#[UsesClass(Text::class)] +#[UsesClass(Image::class)] #[Small] final class UserMessageTest extends TestCase { @@ -46,7 +46,7 @@ public static function provideSerializationTests(): \Generator ]; yield 'With only TextContent' => [ - new UserMessage(new TextContent('foo')), + new UserMessage(new Text('foo')), ['role' => Role::User, 'content' => 'foo'], ]; @@ -62,7 +62,7 @@ public static function provideSerializationTests(): \Generator ]; yield 'With single image as ImageUrlContent' => [ - new UserMessage('foo', new ImageUrlContent('bar')), + new UserMessage('foo', new Image('bar')), [ 'role' => Role::User, 'content' => [ @@ -73,7 +73,7 @@ public static function provideSerializationTests(): \Generator ]; yield 'With single mixed images' => [ - new UserMessage('foo', 'bar', new ImageUrlContent('baz')), + new UserMessage('foo', 'bar', new Image('baz')), [ 'role' => Role::User, 'content' => [ From 27fee037f9001ee00b5b8bd691c37e2f78c65670 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 19:25:45 +0200 Subject: [PATCH 4/8] Move content casting of user message to message object --- src/Message/Message.php | 32 ++++++++++++++++++++++++++-- src/Message/UserMessage.php | 14 +++++-------- tests/Message/MessageTest.php | 35 ++++++++++++++++++++++++++++--- tests/Message/UserMessageTest.php | 30 +++++++------------------- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/Message/Message.php b/src/Message/Message.php index 316a893a..088b8b97 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -4,9 +4,11 @@ namespace PhpLlm\LlmChain\Message; +use PhpLlm\LlmChain\Message\Content\ContentInterface; use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Response\ToolCall; +use Webmozart\Assert\Assert; final readonly class Message { @@ -28,9 +30,35 @@ public static function ofAssistant(?string $content = null, ?array $toolCalls = return new AssistantMessage($content, $toolCalls); } - public static function ofUser(string|Text $content, Image|string ...$images): UserMessage + public static function ofUser(string|ContentInterface ...$content): UserMessage { - return new UserMessage($content, ...$images); + Assert::minCount($content, 1, 'At least a single content part must be given.'); + + $text = null; + $images = []; + foreach ($content as $index => $entry) { + if (0 === $index) { + $text = $entry; + + if (\is_string($text)) { + $text = new Text($entry); + } + + if (!$text instanceof Text) { + throw new \InvalidArgumentException('The first content piece has to be a string or Text part.'); + } + + continue; + } + + if (!is_string($entry) && !$entry instanceof Image) { + continue; + } + + $images[] = \is_string($entry) ? new Image($entry) : $entry; + } + + return new UserMessage($text, ...$images); } public static function ofToolCall(ToolCall $toolCall, string $content): ToolCallMessage diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php index b40c3b37..c746ce1b 100644 --- a/src/Message/UserMessage.php +++ b/src/Message/UserMessage.php @@ -10,13 +10,13 @@ final readonly class UserMessage implements MessageInterface { /** - * @var list + * @var list */ public array $images; public function __construct( - public Text|string $content, - Image|string ...$images, + public Text $text, + Image ...$images, ) { $this->images = $images; } @@ -36,18 +36,14 @@ public function jsonSerialize(): array { $array = ['role' => Role::User]; if ([] === $this->images) { - $array['content'] = \is_string($this->content) ? $this->content : $this->content->text; + $array['content'] = $this->text->text; return $array; } - $content = \is_string($this->content) ? new Text($this->content) : $this->content; - - $array['content'][] = $content->jsonSerialize(); + $array['content'][] = $this->text->jsonSerialize(); foreach ($this->images as $image) { - $image = \is_string($image) ? new Image($image) : $image; - $array['content'][] = $image->jsonSerialize(); } diff --git a/tests/Message/MessageTest.php b/tests/Message/MessageTest.php index 0776fe31..4a953f0c 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Message/MessageTest.php @@ -4,6 +4,8 @@ namespace PhpLlm\LlmChain\Tests\Message; +use PhpLlm\LlmChain\Message\Content\Image; +use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Message\Message; use PhpLlm\LlmChain\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; @@ -11,6 +13,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Webmozart\Assert\InvalidArgumentException; #[CoversClass(Message::class)] #[UsesClass(ToolCall::class)] @@ -46,12 +49,38 @@ public function createAssistantMessageWithToolCalls(): void self::assertTrue($message->hasToolCalls()); } + #[Test] + public function createUserMessageWithoutContentThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At least a single content part must be given.'); + + Message::ofUser(); + } + + #[Test] + public function createUserMessageWithoutTextContentInFirstPlaceIsNotPossible(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The first content piece has to be a string or Text part'); + + Message::ofUser(new Image('foo'), 'bar'); + } + #[Test] public function createUserMessage(): void { $message = Message::ofUser('Hi, my name is John.'); - self::assertSame('Hi, my name is John.', $message->content); + self::assertSame('Hi, my name is John.', $message->text->text); + } + + #[Test] + public function createUserMessageWithTextContent(): void + { + $message = Message::ofUser(new Text('Hi, my name is John.')); + + self::assertSame('Hi, my name is John.', $message->text->text); } #[Test] @@ -59,8 +88,8 @@ public function createUserMessageWithImages(): void { $message = Message::ofUser('Hi, my name is John.', 'http://images.local/my-image.png', 'http://images.local/my-image2.png'); - self::assertSame('Hi, my name is John.', $message->content); - self::assertSame(['http://images.local/my-image.png', 'http://images.local/my-image2.png'], $message->images); + self::assertSame('Hi, my name is John.', $message->text->text); + self::assertCount(2, $message->images); } #[Test] diff --git a/tests/Message/UserMessageTest.php b/tests/Message/UserMessageTest.php index 96ea77ec..d7f86544 100644 --- a/tests/Message/UserMessageTest.php +++ b/tests/Message/UserMessageTest.php @@ -24,9 +24,9 @@ final class UserMessageTest extends TestCase #[Test] public function constructionIsPossible(): void { - $obj = new UserMessage('bar'); + $obj = new UserMessage($text = new Text('foo')); - self::assertSame('bar', $obj->content); + self::assertSame($text, $obj->text); self::assertSame([], $obj->images); self::assertSame(Role::User, $obj->getRole()); } @@ -40,29 +40,13 @@ public function serializationResultsAsExpected(UserMessage $message, array $expe public static function provideSerializationTests(): \Generator { - yield 'With only string content' => [ - new UserMessage('foo'), - ['role' => Role::User, 'content' => 'foo'], - ]; - - yield 'With only TextContent' => [ + yield 'With only text' => [ new UserMessage(new Text('foo')), ['role' => Role::User, 'content' => 'foo'], ]; - yield 'With single image as string' => [ - new UserMessage('foo', 'bar'), - [ - 'role' => Role::User, - 'content' => [ - ['type' => 'text', 'text' => 'foo'], - ['type' => 'image_url', 'image_url' => ['url' => 'bar']], - ], - ], - ]; - - yield 'With single image as ImageUrlContent' => [ - new UserMessage('foo', new Image('bar')), + yield 'With single image' => [ + new UserMessage(new Text('foo'), new Image('bar')), [ 'role' => Role::User, 'content' => [ @@ -72,8 +56,8 @@ public static function provideSerializationTests(): \Generator ], ]; - yield 'With single mixed images' => [ - new UserMessage('foo', 'bar', new Image('baz')), + yield 'With single multiple images' => [ + new UserMessage(new Text('foo'), new Image('bar'), new Image('baz')), [ 'role' => Role::User, 'content' => [ From 9989b4fd187bafd6ebe5921b927639f2d3d007c3 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 21:09:28 +0200 Subject: [PATCH 5/8] Simplify message interface with content classes --- src/Message/Message.php | 33 +++----------------- src/Message/UserMessage.php | 20 ++++++------ tests/Message/MessageTest.php | 51 +++++++++++++++---------------- tests/Message/UserMessageTest.php | 6 ++-- 4 files changed, 42 insertions(+), 68 deletions(-) diff --git a/src/Message/Message.php b/src/Message/Message.php index 088b8b97..6efcbe82 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -5,10 +5,8 @@ namespace PhpLlm\LlmChain\Message; use PhpLlm\LlmChain\Message\Content\ContentInterface; -use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Response\ToolCall; -use Webmozart\Assert\Assert; final readonly class Message { @@ -32,33 +30,12 @@ public static function ofAssistant(?string $content = null, ?array $toolCalls = public static function ofUser(string|ContentInterface ...$content): UserMessage { - Assert::minCount($content, 1, 'At least a single content part must be given.'); + $content = \array_map( + static fn (string|ContentInterface $entry) => \is_string($entry) ? new Text($entry) : $entry, + $content, + ); - $text = null; - $images = []; - foreach ($content as $index => $entry) { - if (0 === $index) { - $text = $entry; - - if (\is_string($text)) { - $text = new Text($entry); - } - - if (!$text instanceof Text) { - throw new \InvalidArgumentException('The first content piece has to be a string or Text part.'); - } - - continue; - } - - if (!is_string($entry) && !$entry instanceof Image) { - continue; - } - - $images[] = \is_string($entry) ? new Image($entry) : $entry; - } - - return new UserMessage($text, ...$images); + return new UserMessage(...$content); } public static function ofToolCall(ToolCall $toolCall, string $content): ToolCallMessage diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php index c746ce1b..c1b96904 100644 --- a/src/Message/UserMessage.php +++ b/src/Message/UserMessage.php @@ -4,21 +4,21 @@ namespace PhpLlm\LlmChain\Message; +use PhpLlm\LlmChain\Message\Content\ContentInterface; use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Content\Text; final readonly class UserMessage implements MessageInterface { /** - * @var list + * @var list */ - public array $images; + public array $content; public function __construct( - public Text $text, - Image ...$images, + ContentInterface ...$images, ) { - $this->images = $images; + $this->content = $images; } public function getRole(): Role @@ -35,16 +35,14 @@ public function getRole(): Role public function jsonSerialize(): array { $array = ['role' => Role::User]; - if ([] === $this->images) { - $array['content'] = $this->text->text; + if (1 === count($this->content) && $this->content[0] instanceof Text) { + $array['content'] = $this->content[0]->text; return $array; } - $array['content'][] = $this->text->jsonSerialize(); - - foreach ($this->images as $image) { - $array['content'][] = $image->jsonSerialize(); + foreach ($this->content as $entry) { + $array['content'][] = $entry->jsonSerialize(); } return $array; diff --git a/tests/Message/MessageTest.php b/tests/Message/MessageTest.php index 4a953f0c..ebddc4f0 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Message/MessageTest.php @@ -7,15 +7,16 @@ use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Content\Text; use PhpLlm\LlmChain\Message\Message; +use PhpLlm\LlmChain\Message\Role; use PhpLlm\LlmChain\Response\ToolCall; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; -use Webmozart\Assert\InvalidArgumentException; #[CoversClass(Message::class)] +#[UsesClass(Role::class)] #[UsesClass(ToolCall::class)] #[Small] final class MessageTest extends TestCase @@ -49,47 +50,45 @@ public function createAssistantMessageWithToolCalls(): void self::assertTrue($message->hasToolCalls()); } - #[Test] - public function createUserMessageWithoutContentThrowsException(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('At least a single content part must be given.'); - - Message::ofUser(); - } - - #[Test] - public function createUserMessageWithoutTextContentInFirstPlaceIsNotPossible(): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The first content piece has to be a string or Text part'); - - Message::ofUser(new Image('foo'), 'bar'); - } - #[Test] public function createUserMessage(): void { $message = Message::ofUser('Hi, my name is John.'); - self::assertSame('Hi, my name is John.', $message->text->text); + self::assertCount(1, $message->content); + self::assertInstanceOf(Text::class, $message->content[0]); + self::assertSame('Hi, my name is John.', $message->content[0]->text); } #[Test] public function createUserMessageWithTextContent(): void { - $message = Message::ofUser(new Text('Hi, my name is John.')); + $text = new Text('Hi, my name is John.'); + $message = Message::ofUser($text); - self::assertSame('Hi, my name is John.', $message->text->text); + self::assertSame([$text], $message->content); } #[Test] public function createUserMessageWithImages(): void { - $message = Message::ofUser('Hi, my name is John.', 'http://images.local/my-image.png', 'http://images.local/my-image2.png'); - - self::assertSame('Hi, my name is John.', $message->text->text); - self::assertCount(2, $message->images); + $message = Message::ofUser( + new Text('Hi, my name is John.'), + new Image('http://images.local/my-image.png'), + 'The following image is a joke.', + new Image('http://images.local/my-image2.png'), + ); + + self::assertCount(4, $message->content); + self::assertSame([ + 'role' => Role::User, + 'content' => [ + ['type' => 'text', 'text' => 'Hi, my name is John.'], + ['type' => 'image_url', 'image_url' => ['url' => 'http://images.local/my-image.png']], + ['type' => 'text', 'text' => 'The following image is a joke.'], + ['type' => 'image_url', 'image_url' => ['url' => 'http://images.local/my-image2.png']], + ], + ], $message->jsonSerialize()); } #[Test] diff --git a/tests/Message/UserMessageTest.php b/tests/Message/UserMessageTest.php index d7f86544..9a87c04c 100644 --- a/tests/Message/UserMessageTest.php +++ b/tests/Message/UserMessageTest.php @@ -18,16 +18,16 @@ #[CoversClass(UserMessage::class)] #[UsesClass(Text::class)] #[UsesClass(Image::class)] +#[UsesClass(Role::class)] #[Small] final class UserMessageTest extends TestCase { #[Test] public function constructionIsPossible(): void { - $obj = new UserMessage($text = new Text('foo')); + $obj = new UserMessage(new Text('foo')); - self::assertSame($text, $obj->text); - self::assertSame([], $obj->images); + self::assertSame(['role' => Role::User, 'content' => 'foo'], $obj->jsonSerialize()); self::assertSame(Role::User, $obj->getRole()); } From 6a1d6fefbb6aa122cdd340da3ead42bf901713f1 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Tue, 24 Sep 2024 21:51:43 +0200 Subject: [PATCH 6/8] Rename variable --- examples/image-describer.php | 4 ++-- src/Message/UserMessage.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/image-describer.php b/examples/image-describer.php index 6fcd12f2..c8fffc41 100755 --- a/examples/image-describer.php +++ b/examples/image-describer.php @@ -15,9 +15,9 @@ $chain = new Chain($llm); $messages = new MessageBag( - Message::forSystem('You are an image analyzer that looks to images like a comedian would like.'), + 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.', + 'Describe the images as a comedian would do it.', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Webysther_20160423_-_Elephpant.svg/350px-Webysther_20160423_-_Elephpant.svg.png', 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/African_Bush_Elephant.jpg/320px-African_Bush_Elephant.jpg', ), diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php index c1b96904..69b27373 100644 --- a/src/Message/UserMessage.php +++ b/src/Message/UserMessage.php @@ -16,9 +16,9 @@ public array $content; public function __construct( - ContentInterface ...$images, + ContentInterface ...$content, ) { - $this->content = $images; + $this->content = $content; } public function getRole(): Role From bcb7e121501adaaa7ed9b76dc48736750ffe65e8 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Wed, 25 Sep 2024 16:50:52 +0200 Subject: [PATCH 7/8] Fix image describer after latest interface change --- examples/image-describer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/image-describer.php b/examples/image-describer.php index c8fffc41..5b75d627 100755 --- a/examples/image-describer.php +++ b/examples/image-describer.php @@ -7,6 +7,7 @@ use PhpLlm\LlmChain\OpenAI\Model\Gpt\Version; use PhpLlm\LlmChain\OpenAI\Runtime\OpenAI; use Symfony\Component\HttpClient\HttpClient; +use PhpLlm\LlmChain\Message\Content\Image; require_once dirname(__DIR__).'/vendor/autoload.php'; @@ -18,8 +19,8 @@ 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.', - 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Webysther_20160423_-_Elephpant.svg/350px-Webysther_20160423_-_Elephpant.svg.png', - 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/African_Bush_Elephant.jpg/320px-African_Bush_Elephant.jpg', + 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); From 96a0ffc4ac1fdeef9857f91dfb5f1d96aae0c135 Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Wed, 25 Sep 2024 17:07:25 +0200 Subject: [PATCH 8/8] Some review adjustments --- examples/image-describer.php | 4 ++-- src/Message/Content/Image.php | 7 +++++-- src/Message/UserMessage.php | 7 ++----- tests/Message/Content/ImageTest.php | 2 +- tests/Message/MessageBagTest.php | 8 ++++---- tests/Message/MessageTest.php | 4 ++-- tests/Message/UserMessageTest.php | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/image-describer.php b/examples/image-describer.php index 5b75d627..7e2e3a49 100755 --- a/examples/image-describer.php +++ b/examples/image-describer.php @@ -1,13 +1,13 @@ call($messages); diff --git a/src/Message/Content/Image.php b/src/Message/Content/Image.php index e1f4d6b0..5bbf56bc 100644 --- a/src/Message/Content/Image.php +++ b/src/Message/Content/Image.php @@ -6,7 +6,10 @@ final readonly class Image implements ContentInterface { - public function __construct(public string $image) + /** + * @param string $url An URL like "http://localhost:3000/my-image.png" or a data url like "data:image/png;base64,iVBOR[...]" + */ + public function __construct(public string $url) { } @@ -15,6 +18,6 @@ public function __construct(public string $image) */ public function jsonSerialize(): array { - return ['type' => 'image_url', 'image_url' => ['url' => $this->image]]; + return ['type' => 'image_url', 'image_url' => ['url' => $this->url]]; } } diff --git a/src/Message/UserMessage.php b/src/Message/UserMessage.php index 69b27373..700d79dc 100644 --- a/src/Message/UserMessage.php +++ b/src/Message/UserMessage.php @@ -5,7 +5,6 @@ namespace PhpLlm\LlmChain\Message; use PhpLlm\LlmChain\Message\Content\ContentInterface; -use PhpLlm\LlmChain\Message\Content\Image; use PhpLlm\LlmChain\Message\Content\Text; final readonly class UserMessage implements MessageInterface @@ -29,7 +28,7 @@ public function getRole(): Role /** * @return array{ * role: Role::User, - * content: string|list + * content: string|list * } */ public function jsonSerialize(): array @@ -41,9 +40,7 @@ public function jsonSerialize(): array return $array; } - foreach ($this->content as $entry) { - $array['content'][] = $entry->jsonSerialize(); - } + $array['content'] = $this->content; return $array; } diff --git a/tests/Message/Content/ImageTest.php b/tests/Message/Content/ImageTest.php index f6d756da..8cf7a598 100644 --- a/tests/Message/Content/ImageTest.php +++ b/tests/Message/Content/ImageTest.php @@ -19,7 +19,7 @@ public function constructionIsPossible(): void { $obj = new Image('foo'); - self::assertSame('foo', $obj->image); + self::assertSame('foo', $obj->url); } #[Test] diff --git a/tests/Message/MessageBagTest.php b/tests/Message/MessageBagTest.php index 99c79834..5b71476c 100644 --- a/tests/Message/MessageBagTest.php +++ b/tests/Message/MessageBagTest.php @@ -61,8 +61,8 @@ public function with(): void self::assertCount(4, $newMessageBag); $newMessageFromBag = $newMessageBag[3]; - assert($newMessageFromBag instanceof AssistantMessage); + self::assertInstanceOf(AssistantMessage::class, $newMessageFromBag); self::assertSame('It is time to wake up.', $newMessageFromBag->content); } @@ -82,8 +82,8 @@ public function merge(): void self::assertCount(4, $messageBag); $messageFromBag = $messageBag[3]; - assert($messageFromBag instanceof AssistantMessage); + self::assertInstanceOf(AssistantMessage::class, $messageFromBag); self::assertSame('It is time to wake up.', $messageFromBag->content); } @@ -102,8 +102,8 @@ public function withoutSystemMessage(): void self::assertCount(2, $newMessageBag); $messageFromNewBag = $newMessageBag[0]; - assert($messageFromNewBag instanceof AssistantMessage); + self::assertInstanceOf(AssistantMessage::class, $messageFromNewBag); self::assertSame('It is time to sleep.', $messageFromNewBag->content); } @@ -122,8 +122,8 @@ public function prepend(): void self::assertCount(3, $newMessageBag); $newMessageBagMessage = $newMessageBag[0]; - assert($newMessageBagMessage instanceof SystemMessage); + self::assertInstanceOf(SystemMessage::class, $newMessageBagMessage); self::assertSame('My amazing system prompt.', $newMessageBagMessage->content); } diff --git a/tests/Message/MessageTest.php b/tests/Message/MessageTest.php index ebddc4f0..7644b814 100644 --- a/tests/Message/MessageTest.php +++ b/tests/Message/MessageTest.php @@ -80,7 +80,7 @@ public function createUserMessageWithImages(): void ); self::assertCount(4, $message->content); - self::assertSame([ + self::assertSame(\json_encode([ 'role' => Role::User, 'content' => [ ['type' => 'text', 'text' => 'Hi, my name is John.'], @@ -88,7 +88,7 @@ public function createUserMessageWithImages(): void ['type' => 'text', 'text' => 'The following image is a joke.'], ['type' => 'image_url', 'image_url' => ['url' => 'http://images.local/my-image2.png']], ], - ], $message->jsonSerialize()); + ]), \json_encode($message)); } #[Test] diff --git a/tests/Message/UserMessageTest.php b/tests/Message/UserMessageTest.php index 9a87c04c..3f433b3c 100644 --- a/tests/Message/UserMessageTest.php +++ b/tests/Message/UserMessageTest.php @@ -27,7 +27,7 @@ public function constructionIsPossible(): void { $obj = new UserMessage(new Text('foo')); - self::assertSame(['role' => Role::User, 'content' => 'foo'], $obj->jsonSerialize()); + self::assertSame(\json_encode(['role' => Role::User, 'content' => 'foo']), \json_encode($obj)); self::assertSame(Role::User, $obj->getRole()); } @@ -35,7 +35,7 @@ public function constructionIsPossible(): void #[DataProvider('provideSerializationTests')] public function serializationResultsAsExpected(UserMessage $message, array $expectedArray): void { - self::assertSame($message->jsonSerialize(), $expectedArray); + self::assertSame(\json_encode($message), \json_encode($expectedArray)); } public static function provideSerializationTests(): \Generator