Skip to content

Commit 4d4a7fa

Browse files
OskarStarkclaude
andauthored
feat!: add UUID to all messages (#364)
## Summary - Added unique identifiers (UUIDv7) to all message types - Each message now automatically gets a unique ID upon instantiation - Added comprehensive tests for the new ID functionality ## Breaking Change 🚨 This is a breaking change as `MessageInterface` now requires the `getId(): Uuid` method to be implemented by all message classes. ## Implementation Details - Added `symfony/uid` package dependency (^6.4 || ^7.1) - Added `public readonly Uuid $id` property to all message classes - IDs are generated automatically in constructors using `Uuid::v7()` - Added `getId()` method to all message implementations ## Why UUID v7? UUID v7 offers significant advantages over other UUID versions: - **Time-ordered**: Natural chronological sorting without additional timestamp fields - **Millisecond precision**: Captures creation time with high accuracy - **Better database performance**: Sequential nature improves B-tree index locality - **Globally unique**: No coordination needed between distributed systems - **Extractable timestamp**: Creation time can be retrieved from the ID itself ### Practical Example ```php $message = new UserMessage(new Text('Hello')); $timestamp = $message->getId()->getDateTime(); // Returns \DateTimeImmutable echo $timestamp->format('Y-m-d H:i:s.u'); // e.g., "2025-06-29 23:45:12.123456" ``` ## Test Coverage Added tests for each message type to ensure: - ID is properly generated and accessible - ID remains consistent for the same message instance - Different message instances have different IDs - Messages with identical content still receive unique IDs Closes #77 Closes #344 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent 7a0fbc5 commit 4d4a7fa

11 files changed

+235
-0
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,32 @@ a **MessageBag** to a **Chain**, which takes care of LLM invocation and response
8989
Messages can be of different types, most importantly `UserMessage`, `SystemMessage`, or `AssistantMessage`, and can also
9090
have different content types, like `Text`, `Image` or `Audio`.
9191

92+
#### Message Unique IDs
93+
94+
Each message automatically receives a unique identifier (UUID v7) upon creation.
95+
This provides several benefits:
96+
97+
- **Traceability**: Track individual messages through your application
98+
- **Time-ordered**: UUIDs are naturally sortable by creation time
99+
- **Timestamp extraction**: Get the exact creation time from the ID
100+
- **Database-friendly**: Sequential nature improves index performance
101+
102+
```php
103+
use PhpLlm\LlmChain\Platform\Message\Message;
104+
105+
$message = Message::ofUser('Hello, AI!');
106+
107+
// Access the unique ID
108+
$id = $message->getId(); // Returns Symfony\Component\Uid\Uuid instance
109+
110+
// Extract creation timestamp
111+
$createdAt = $id->getDateTime(); // Returns \DateTimeImmutable
112+
echo $createdAt->format('Y-m-d H:i:s.u'); // e.g., "2025-06-29 15:30:45.123456"
113+
114+
// Get string representation
115+
echo $id->toRfc4122(); // e.g., "01928d1f-6f2e-7123-a456-123456789abc"
116+
```
117+
92118
#### Example Chain call with messages
93119

94120
```php

src/Platform/Message/AssistantMessage.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,35 @@
55
namespace PhpLlm\LlmChain\Platform\Message;
66

77
use PhpLlm\LlmChain\Platform\Response\ToolCall;
8+
use Symfony\Component\Uid\Uuid;
89

910
/**
1011
* @author Denis Zunke <[email protected]>
1112
*/
1213
final readonly class AssistantMessage implements MessageInterface
1314
{
15+
public Uuid $id;
16+
1417
/**
1518
* @param ?ToolCall[] $toolCalls
1619
*/
1720
public function __construct(
1821
public ?string $content = null,
1922
public ?array $toolCalls = null,
2023
) {
24+
$this->id = Uuid::v7();
2125
}
2226

2327
public function getRole(): Role
2428
{
2529
return Role::Assistant;
2630
}
2731

32+
public function getId(): Uuid
33+
{
34+
return $this->id;
35+
}
36+
2837
public function hasToolCalls(): bool
2938
{
3039
return null !== $this->toolCalls && 0 !== \count($this->toolCalls);

src/Platform/Message/MessageInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
namespace PhpLlm\LlmChain\Platform\Message;
66

7+
use Symfony\Component\Uid\Uuid;
8+
79
/**
810
* @author Denis Zunke <[email protected]>
911
*/
1012
interface MessageInterface
1113
{
1214
public function getRole(): Role;
15+
16+
public function getId(): Uuid;
1317
}

src/Platform/Message/SystemMessage.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@
44

55
namespace PhpLlm\LlmChain\Platform\Message;
66

7+
use Symfony\Component\Uid\Uuid;
8+
79
/**
810
* @author Denis Zunke <[email protected]>
911
*/
1012
final readonly class SystemMessage implements MessageInterface
1113
{
14+
public Uuid $id;
15+
1216
public function __construct(public string $content)
1317
{
18+
$this->id = Uuid::v7();
1419
}
1520

1621
public function getRole(): Role
1722
{
1823
return Role::System;
1924
}
25+
26+
public function getId(): Uuid
27+
{
28+
return $this->id;
29+
}
2030
}

src/Platform/Message/ToolCallMessage.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,29 @@
55
namespace PhpLlm\LlmChain\Platform\Message;
66

77
use PhpLlm\LlmChain\Platform\Response\ToolCall;
8+
use Symfony\Component\Uid\Uuid;
89

910
/**
1011
* @author Denis Zunke <[email protected]>
1112
*/
1213
final readonly class ToolCallMessage implements MessageInterface
1314
{
15+
public Uuid $id;
16+
1417
public function __construct(
1518
public ToolCall $toolCall,
1619
public string $content,
1720
) {
21+
$this->id = Uuid::v7();
1822
}
1923

2024
public function getRole(): Role
2125
{
2226
return Role::ToolCall;
2327
}
28+
29+
public function getId(): Uuid
30+
{
31+
return $this->id;
32+
}
2433
}

src/Platform/Message/UserMessage.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpLlm\LlmChain\Platform\Message\Content\ContentInterface;
99
use PhpLlm\LlmChain\Platform\Message\Content\Image;
1010
use PhpLlm\LlmChain\Platform\Message\Content\ImageUrl;
11+
use Symfony\Component\Uid\Uuid;
1112

1213
/**
1314
* @author Denis Zunke <[email protected]>
@@ -19,17 +20,25 @@
1920
*/
2021
public array $content;
2122

23+
public Uuid $id;
24+
2225
public function __construct(
2326
ContentInterface ...$content,
2427
) {
2528
$this->content = $content;
29+
$this->id = Uuid::v7();
2630
}
2731

2832
public function getRole(): Role
2933
{
3034
return Role::User;
3135
}
3236

37+
public function getId(): Uuid
38+
{
39+
return $this->id;
40+
}
41+
3342
public function hasAudioContent(): bool
3443
{
3544
foreach ($this->content as $content) {

tests/Helper/UuidAssertionTrait.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Tests\Helper;
6+
7+
trait UuidAssertionTrait
8+
{
9+
/**
10+
* Asserts that a value is a valid UUID v7 string.
11+
*/
12+
public static function assertIsUuidV7(mixed $actual, string $message = ''): void
13+
{
14+
self::assertIsString($actual, $message ?: 'Failed asserting that value is a string.');
15+
self::assertMatchesRegularExpression(
16+
'/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
17+
$actual,
18+
$message ?: 'Failed asserting that value is a valid UUID v7.'
19+
);
20+
}
21+
}

tests/Platform/Message/AssistantMessageTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77
use PhpLlm\LlmChain\Platform\Message\AssistantMessage;
88
use PhpLlm\LlmChain\Platform\Message\Role;
99
use PhpLlm\LlmChain\Platform\Response\ToolCall;
10+
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
1011
use PHPUnit\Framework\Attributes\CoversClass;
1112
use PHPUnit\Framework\Attributes\Small;
1213
use PHPUnit\Framework\Attributes\Test;
1314
use PHPUnit\Framework\Attributes\UsesClass;
1415
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Uid\UuidV7;
1517

1618
#[CoversClass(AssistantMessage::class)]
1719
#[UsesClass(ToolCall::class)]
1820
#[Small]
1921
final class AssistantMessageTest extends TestCase
2022
{
23+
use UuidAssertionTrait;
24+
2125
#[Test]
2226
public function theRoleOfTheMessageIsAsExpected(): void
2327
{
@@ -43,4 +47,36 @@ public function constructionWithoutContentIsPossible(): void
4347
self::assertSame([$toolCall], $message->toolCalls);
4448
self::assertTrue($message->hasToolCalls());
4549
}
50+
51+
#[Test]
52+
public function messageHasUid(): void
53+
{
54+
$message = new AssistantMessage('foo');
55+
56+
self::assertInstanceOf(UuidV7::class, $message->id);
57+
self::assertInstanceOf(UuidV7::class, $message->getId());
58+
self::assertSame($message->id, $message->getId());
59+
}
60+
61+
#[Test]
62+
public function differentMessagesHaveDifferentUids(): void
63+
{
64+
$message1 = new AssistantMessage('foo');
65+
$message2 = new AssistantMessage('bar');
66+
67+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
68+
self::assertIsUuidV7($message1->getId()->toRfc4122());
69+
self::assertIsUuidV7($message2->getId()->toRfc4122());
70+
}
71+
72+
#[Test]
73+
public function sameMessagesHaveDifferentUids(): void
74+
{
75+
$message1 = new AssistantMessage('foo');
76+
$message2 = new AssistantMessage('foo');
77+
78+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
79+
self::assertIsUuidV7($message1->getId()->toRfc4122());
80+
self::assertIsUuidV7($message2->getId()->toRfc4122());
81+
}
4682
}

tests/Platform/Message/SystemMessageTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66

77
use PhpLlm\LlmChain\Platform\Message\Role;
88
use PhpLlm\LlmChain\Platform\Message\SystemMessage;
9+
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
910
use PHPUnit\Framework\Attributes\CoversClass;
1011
use PHPUnit\Framework\Attributes\Small;
1112
use PHPUnit\Framework\Attributes\Test;
1213
use PHPUnit\Framework\TestCase;
14+
use Symfony\Component\Uid\UuidV7;
1315

1416
#[CoversClass(SystemMessage::class)]
1517
#[Small]
1618
final class SystemMessageTest extends TestCase
1719
{
20+
use UuidAssertionTrait;
21+
1822
#[Test]
1923
public function constructionIsPossible(): void
2024
{
@@ -23,4 +27,36 @@ public function constructionIsPossible(): void
2327
self::assertSame(Role::System, $message->getRole());
2428
self::assertSame('foo', $message->content);
2529
}
30+
31+
#[Test]
32+
public function messageHasUid(): void
33+
{
34+
$message = new SystemMessage('foo');
35+
36+
self::assertInstanceOf(UuidV7::class, $message->id);
37+
self::assertInstanceOf(UuidV7::class, $message->getId());
38+
self::assertSame($message->id, $message->getId());
39+
}
40+
41+
#[Test]
42+
public function differentMessagesHaveDifferentUids(): void
43+
{
44+
$message1 = new SystemMessage('foo');
45+
$message2 = new SystemMessage('bar');
46+
47+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
48+
self::assertIsUuidV7($message1->getId()->toRfc4122());
49+
self::assertIsUuidV7($message2->getId()->toRfc4122());
50+
}
51+
52+
#[Test]
53+
public function sameMessagesHaveDifferentUids(): void
54+
{
55+
$message1 = new SystemMessage('foo');
56+
$message2 = new SystemMessage('foo');
57+
58+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
59+
self::assertIsUuidV7($message1->getId()->toRfc4122());
60+
self::assertIsUuidV7($message2->getId()->toRfc4122());
61+
}
2662
}

tests/Platform/Message/ToolCallMessageTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66

77
use PhpLlm\LlmChain\Platform\Message\ToolCallMessage;
88
use PhpLlm\LlmChain\Platform\Response\ToolCall;
9+
use PhpLlm\LlmChain\Tests\Helper\UuidAssertionTrait;
910
use PHPUnit\Framework\Attributes\CoversClass;
1011
use PHPUnit\Framework\Attributes\Small;
1112
use PHPUnit\Framework\Attributes\Test;
1213
use PHPUnit\Framework\Attributes\UsesClass;
1314
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\UuidV7;
1416

1517
#[CoversClass(ToolCallMessage::class)]
1618
#[UsesClass(ToolCall::class)]
1719
#[Small]
1820
final class ToolCallMessageTest extends TestCase
1921
{
22+
use UuidAssertionTrait;
23+
2024
#[Test]
2125
public function constructionIsPossible(): void
2226
{
@@ -26,4 +30,39 @@ public function constructionIsPossible(): void
2630
self::assertSame($toolCall, $obj->toolCall);
2731
self::assertSame('bar', $obj->content);
2832
}
33+
34+
#[Test]
35+
public function messageHasUid(): void
36+
{
37+
$toolCall = new ToolCall('foo', 'bar');
38+
$message = new ToolCallMessage($toolCall, 'bar');
39+
40+
self::assertInstanceOf(UuidV7::class, $message->id);
41+
self::assertInstanceOf(UuidV7::class, $message->getId());
42+
self::assertSame($message->id, $message->getId());
43+
}
44+
45+
#[Test]
46+
public function differentMessagesHaveDifferentUids(): void
47+
{
48+
$toolCall = new ToolCall('foo', 'bar');
49+
$message1 = new ToolCallMessage($toolCall, 'bar');
50+
$message2 = new ToolCallMessage($toolCall, 'baz');
51+
52+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
53+
self::assertIsUuidV7($message1->getId()->toRfc4122());
54+
self::assertIsUuidV7($message2->getId()->toRfc4122());
55+
}
56+
57+
#[Test]
58+
public function sameMessagesHaveDifferentUids(): void
59+
{
60+
$toolCall = new ToolCall('foo', 'bar');
61+
$message1 = new ToolCallMessage($toolCall, 'bar');
62+
$message2 = new ToolCallMessage($toolCall, 'bar');
63+
64+
self::assertNotSame($message1->getId()->toRfc4122(), $message2->getId()->toRfc4122());
65+
self::assertIsUuidV7($message1->getId()->toRfc4122());
66+
self::assertIsUuidV7($message2->getId()->toRfc4122());
67+
}
2968
}

0 commit comments

Comments
 (0)