Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

feat!: add UUID to all messages #364

Merged
merged 1 commit into from
Jun 30, 2025
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,32 @@ a **MessageBag** to a **Chain**, which takes care of LLM invocation and response
Messages can be of different types, most importantly `UserMessage`, `SystemMessage`, or `AssistantMessage`, and can also
have different content types, like `Text`, `Image` or `Audio`.

#### Message Unique IDs

Each message automatically receives a unique identifier (UUID v7) upon creation.
This provides several benefits:

- **Traceability**: Track individual messages through your application
- **Time-ordered**: UUIDs are naturally sortable by creation time
- **Timestamp extraction**: Get the exact creation time from the ID
- **Database-friendly**: Sequential nature improves index performance

```php
use PhpLlm\LlmChain\Platform\Message\Message;

$message = Message::ofUser('Hello, AI!');

// Access the unique ID
$id = $message->getId(); // Returns Symfony\Component\Uid\Uuid instance

// Extract creation timestamp
$createdAt = $id->getDateTime(); // Returns \DateTimeImmutable
echo $createdAt->format('Y-m-d H:i:s.u'); // e.g., "2025-06-29 15:30:45.123456"

// Get string representation
echo $id->toRfc4122(); // e.g., "01928d1f-6f2e-7123-a456-123456789abc"
```

#### Example Chain call with messages

```php
Expand Down
9 changes: 9 additions & 0 deletions src/Platform/Message/AssistantMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@
namespace PhpLlm\LlmChain\Platform\Message;

use PhpLlm\LlmChain\Platform\Response\ToolCall;
use Symfony\Component\Uid\Uuid;

/**
* @author Denis Zunke <denis.zunke@gmail.com>
*/
final readonly class AssistantMessage implements MessageInterface
{
public Uuid $id;

/**
* @param ?ToolCall[] $toolCalls
*/
public function __construct(
public ?string $content = null,
public ?array $toolCalls = null,
) {
$this->id = Uuid::v7();
}

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

public function getId(): Uuid
{
return $this->id;
}

public function hasToolCalls(): bool
{
return null !== $this->toolCalls && 0 !== \count($this->toolCalls);
Expand Down
4 changes: 4 additions & 0 deletions src/Platform/Message/MessageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

namespace PhpLlm\LlmChain\Platform\Message;

use Symfony\Component\Uid\Uuid;

/**
* @author Denis Zunke <denis.zunke@gmail.com>
*/
interface MessageInterface
{
public function getRole(): Role;

public function getId(): Uuid;
}
10 changes: 10 additions & 0 deletions src/Platform/Message/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@

namespace PhpLlm\LlmChain\Platform\Message;

use Symfony\Component\Uid\Uuid;

/**
* @author Denis Zunke <denis.zunke@gmail.com>
*/
final readonly class SystemMessage implements MessageInterface
{
public Uuid $id;

public function __construct(public string $content)
{
$this->id = Uuid::v7();
}

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

public function getId(): Uuid
{
return $this->id;
}
}
9 changes: 9 additions & 0 deletions src/Platform/Message/ToolCallMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@
namespace PhpLlm\LlmChain\Platform\Message;

use PhpLlm\LlmChain\Platform\Response\ToolCall;
use Symfony\Component\Uid\Uuid;

/**
* @author Denis Zunke <denis.zunke@gmail.com>
*/
final readonly class ToolCallMessage implements MessageInterface
{
public Uuid $id;

public function __construct(
public ToolCall $toolCall,
public string $content,
) {
$this->id = Uuid::v7();
}

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

public function getId(): Uuid
{
return $this->id;
}
}
9 changes: 9 additions & 0 deletions src/Platform/Message/UserMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpLlm\LlmChain\Platform\Message\Content\ContentInterface;
use PhpLlm\LlmChain\Platform\Message\Content\Image;
use PhpLlm\LlmChain\Platform\Message\Content\ImageUrl;
use Symfony\Component\Uid\Uuid;

/**
* @author Denis Zunke <denis.zunke@gmail.com>
Expand All @@ -19,17 +20,25 @@
*/
public array $content;

public Uuid $id;

public function __construct(
ContentInterface ...$content,
) {
$this->content = $content;
$this->id = Uuid::v7();
}

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

public function getId(): Uuid
{
return $this->id;
}

public function hasAudioContent(): bool
{
foreach ($this->content as $content) {
Expand Down
21 changes: 21 additions & 0 deletions tests/Helper/UuidAssertionTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Helper;

trait UuidAssertionTrait
{
/**
* Asserts that a value is a valid UUID v7 string.
*/
public static function assertIsUuidV7(mixed $actual, string $message = ''): void
{
self::assertIsString($actual, $message ?: 'Failed asserting that value is a string.');
self::assertMatchesRegularExpression(
'/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
$actual,
$message ?: 'Failed asserting that value is a valid UUID v7.'
);
}
}
36 changes: 36 additions & 0 deletions tests/Platform/Message/AssistantMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@
use PhpLlm\LlmChain\Platform\Message\AssistantMessage;
use PhpLlm\LlmChain\Platform\Message\Role;
use PhpLlm\LlmChain\Platform\Response\ToolCall;
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
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 Symfony\Component\Uid\UuidV7;

#[CoversClass(AssistantMessage::class)]
#[UsesClass(ToolCall::class)]
#[Small]
final class AssistantMessageTest extends TestCase
{
use UuidAssertionTrait;

#[Test]
public function theRoleOfTheMessageIsAsExpected(): void
{
Expand All @@ -43,4 +47,36 @@ public function constructionWithoutContentIsPossible(): void
self::assertSame([$toolCall], $message->toolCalls);
self::assertTrue($message->hasToolCalls());
}

#[Test]
public function messageHasUid(): void
{
$message = new AssistantMessage('foo');

self::assertInstanceOf(UuidV7::class, $message->id);
self::assertInstanceOf(UuidV7::class, $message->getId());
self::assertSame($message->id, $message->getId());
}

#[Test]
public function differentMessagesHaveDifferentUids(): void
{
$message1 = new AssistantMessage('foo');
$message2 = new AssistantMessage('bar');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}

#[Test]
public function sameMessagesHaveDifferentUids(): void
{
$message1 = new AssistantMessage('foo');
$message2 = new AssistantMessage('foo');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}
}
36 changes: 36 additions & 0 deletions tests/Platform/Message/SystemMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@

use PhpLlm\LlmChain\Platform\Message\Role;
use PhpLlm\LlmChain\Platform\Message\SystemMessage;
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\UuidV7;

#[CoversClass(SystemMessage::class)]
#[Small]
final class SystemMessageTest extends TestCase
{
use UuidAssertionTrait;

#[Test]
public function constructionIsPossible(): void
{
Expand All @@ -23,4 +27,36 @@ public function constructionIsPossible(): void
self::assertSame(Role::System, $message->getRole());
self::assertSame('foo', $message->content);
}

#[Test]
public function messageHasUid(): void
{
$message = new SystemMessage('foo');

self::assertInstanceOf(UuidV7::class, $message->id);
self::assertInstanceOf(UuidV7::class, $message->getId());
self::assertSame($message->id, $message->getId());
}

#[Test]
public function differentMessagesHaveDifferentUids(): void
{
$message1 = new SystemMessage('foo');
$message2 = new SystemMessage('bar');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}

#[Test]
public function sameMessagesHaveDifferentUids(): void
{
$message1 = new SystemMessage('foo');
$message2 = new SystemMessage('foo');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}
}
39 changes: 39 additions & 0 deletions tests/Platform/Message/ToolCallMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@

use PhpLlm\LlmChain\Platform\Message\ToolCallMessage;
use PhpLlm\LlmChain\Platform\Response\ToolCall;
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
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 Symfony\Component\Uid\UuidV7;

#[CoversClass(ToolCallMessage::class)]
#[UsesClass(ToolCall::class)]
#[Small]
final class ToolCallMessageTest extends TestCase
{
use UuidAssertionTrait;

#[Test]
public function constructionIsPossible(): void
{
Expand All @@ -26,4 +30,39 @@ public function constructionIsPossible(): void
self::assertSame($toolCall, $obj->toolCall);
self::assertSame('bar', $obj->content);
}

#[Test]
public function messageHasUid(): void
{
$toolCall = new ToolCall('foo', 'bar');
$message = new ToolCallMessage($toolCall, 'bar');

self::assertInstanceOf(UuidV7::class, $message->id);
self::assertInstanceOf(UuidV7::class, $message->getId());
self::assertSame($message->id, $message->getId());
}

#[Test]
public function differentMessagesHaveDifferentUids(): void
{
$toolCall = new ToolCall('foo', 'bar');
$message1 = new ToolCallMessage($toolCall, 'bar');
$message2 = new ToolCallMessage($toolCall, 'baz');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}

#[Test]
public function sameMessagesHaveDifferentUids(): void
{
$toolCall = new ToolCall('foo', 'bar');
$message1 = new ToolCallMessage($toolCall, 'bar');
$message2 = new ToolCallMessage($toolCall, 'bar');

self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
self::assertIsUuidV7($message1->getId()->toRfc4122());
self::assertIsUuidV7($message2->getId()->toRfc4122());
}
}
Loading