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 4 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
27 changes: 27 additions & 0 deletions examples/image-describer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use PhpLlm\LlmChain\Chain;
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\Runtime\OpenAI;
use Symfony\Component\HttpClient\HttpClient;

require_once dirname(__DIR__).'/vendor/autoload.php';

$runtime = new OpenAI(HttpClient::create(), getenv('OPENAI_API_KEY'));
$llm = new Gpt($runtime, Version::gpt4oMini());

$chain = new Chain($llm);
$messages = new MessageBag(
Message::forSystem('You are an image analyzer that looks to images like a comedian would like.'),
Message::ofUser(
'Describe the image 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',
),
);
$response = $chain->call($messages);

echo $response.PHP_EOL;
53 changes: 53 additions & 0 deletions src/Message/AssistantMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message;

use PhpLlm\LlmChain\Response\ToolCall;

final readonly class AssistantMessage implements MessageInterface
{
/**
* @param ?ToolCall[] $toolCalls
*/
public function __construct(
public ?string $content = null,
public ?array $toolCalls = null,
) {
}

public function getRole(): Role
{
return Role::Assistant;
}

public function hasToolCalls(): bool
{
return null !== $this->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;
}
}
9 changes: 9 additions & 0 deletions src/Message/Content/ContentInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message\Content;

interface ContentInterface extends \JsonSerializable
{
}
20 changes: 20 additions & 0 deletions src/Message/Content/Image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message\Content;

final readonly class Image implements ContentInterface
{
public function __construct(public string $image)
{
}

/**
* @return array{type: 'image_url', image_url: array{url: string}}
*/
public function jsonSerialize(): array
{
return ['type' => 'image_url', 'image_url' => ['url' => $this->image]];
}
}
20 changes: 20 additions & 0 deletions src/Message/Content/Text.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message\Content;

final readonly class Text implements ContentInterface
{
public function __construct(public string $text)
{
}

/**
* @return array{type: 'text', text: string}
*/
public function jsonSerialize(): array
{
return ['type' => 'text', 'text' => $this->text];
}
}
101 changes: 36 additions & 65 deletions src/Message/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,94 +4,65 @@

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 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
public static function ofAssistant(?string $content = null, ?array $toolCalls = null): AssistantMessage
{
return new self($content, Role::Assistant, $toolCalls);
return new AssistantMessage($content, $toolCalls);
}

public static function ofUser(string $content): self
public static function ofUser(string|ContentInterface ...$content): UserMessage
{
return new self($content, Role::User);
}
Assert::minCount($content, 1, 'At least a single content part must be given.');

public static function ofToolCall(ToolCall $toolCall, string $content): self
{
return new self($content, Role::ToolCall, [$toolCall]);
}
$text = null;
$images = [];
foreach ($content as $index => $entry) {
if (0 === $index) {
$text = $entry;

public function isSystem(): bool
{
return Role::System === $this->role;
}
if (\is_string($text)) {
$text = new Text($entry);
}

public function isAssistant(): bool
{
return Role::Assistant === $this->role;
}
if (!$text instanceof Text) {
throw new \InvalidArgumentException('The first content piece has to be a string or Text part.');
}

public function isUser(): bool
{
return Role::User === $this->role;
}
continue;
}

public function isToolCall(): bool
{
return Role::ToolCall === $this->role;
}

public function hasToolCalls(): bool
{
return null !== $this->toolCalls && 0 !== count($this->toolCalls);
}
if (!is_string($entry) && !$entry instanceof Image) {
continue;
}

/**
* @return array{
* role: 'system'|'assistant'|'user'|'tool',
* content: ?string,
* tool_calls?: ToolCall[],
* tool_call_id?: string
* }
*/
public function jsonSerialize(): array
{
$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;
$images[] = \is_string($entry) ? new Image($entry) : $entry;
}

if ($this->hasToolCalls() && $this->isAssistant()) {
$array['tool_calls'] = $this->toolCalls;
}
return new UserMessage($text, ...$images);
}

return $array;
public static function ofToolCall(ToolCall $toolCall, string $content): ToolCallMessage
{
return new ToolCallMessage($toolCall, $content);
}
}
19 changes: 11 additions & 8 deletions src/Message/MessageBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@
namespace PhpLlm\LlmChain\Message;

/**
* @template-extends \ArrayObject<int, Message>
* @template-extends \ArrayObject<int, MessageInterface>
*/
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;
}
}

return null;
}

public function with(Message $message): self
public function with(MessageInterface $message): self
{
$messages = clone $this;
$messages->append($message);
Expand All @@ -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()));
Expand All @@ -60,7 +63,7 @@ public function prepend(Message $message): self
}

/**
* @return Message[]
* @return MessageInterface[]
*/
public function jsonSerialize(): array
{
Expand Down
10 changes: 10 additions & 0 deletions src/Message/MessageInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message;

interface MessageInterface extends \JsonSerializable
{
public function getRole(): Role;
}
31 changes: 31 additions & 0 deletions src/Message/SystemMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Message;

final readonly class SystemMessage implements MessageInterface
{
public function __construct(public string $content)
{
}

public function getRole(): Role
{
return Role::System;
}

/**
* @return array{
* role: Role::System,
* content: string
* }
*/
public function jsonSerialize(): array
{
return [
'role' => Role::System,
'content' => $this->content,
];
}
}
Loading