diff --git a/composer.json b/composer.json index 5ca727a0..a1994147 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", + "psr/event-dispatcher": "^1.0", "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index d6a5af90..519aef77 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "893f04e6854aa23afd7dcd0cabfe1def", + "content-hash": "c290e23e54f97989b8d04bb33bf137fc", "packages": [ { "name": "php-http/discovery", @@ -249,6 +249,56 @@ }, "time": "2024-03-15T13:55:21+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.3", diff --git a/src/AiClient.php b/src/AiClient.php index ab9ceadd..df742c24 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -4,6 +4,7 @@ namespace WordPress\AiClient; +use Psr\EventDispatcher\EventDispatcherInterface; use WordPress\AiClient\Builders\PromptBuilder; use WordPress\AiClient\Common\Exception\InvalidArgumentException; use WordPress\AiClient\Common\Exception\RuntimeException; @@ -90,6 +91,11 @@ class AiClient */ private static ?ProviderRegistry $defaultRegistry = null; + /** + * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events. + */ + private static ?EventDispatcherInterface $eventDispatcher = null; + /** * Gets the default provider registry instance. * @@ -114,6 +120,56 @@ public static function defaultRegistry(): ProviderRegistry return self::$defaultRegistry; } + /** + * Sets the event dispatcher for prompt lifecycle events. + * + * The event dispatcher will be used to dispatch BeforePromptSentEvent and + * AfterPromptSentEvent during prompt generation. + * + * @since n.e.x.t + * + * @param EventDispatcherInterface|null $dispatcher The event dispatcher, or null to disable. + * @return void + */ + public static function setEventDispatcher(?EventDispatcherInterface $dispatcher): void + { + self::$eventDispatcher = $dispatcher; + } + + /** + * Gets the event dispatcher for prompt lifecycle events. + * + * @since n.e.x.t + * + * @return EventDispatcherInterface|null The event dispatcher, or null if not set. + */ + public static function getEventDispatcher(): ?EventDispatcherInterface + { + return self::$eventDispatcher; + } + + /** + * Dispatches an event if an event dispatcher is registered. + * + * This is a convenience method that handles the null check internally, + * only dispatching if a dispatcher has been set via setEventDispatcher(). + * + * @since n.e.x.t + * + * @template T of object + * @param T $event The event to dispatch. + * @return T The event (potentially modified by listeners). + */ + public static function dispatchEvent(object $event): object + { + if (self::$eventDispatcher !== null) { + /** @var T */ + return self::$eventDispatcher->dispatch($event); + } + + return $event; + } + /** * Checks if a provider is configured and available for use. * diff --git a/src/Builders/PromptBuilder.php b/src/Builders/PromptBuilder.php index 758aeeda..317d053e 100644 --- a/src/Builders/PromptBuilder.php +++ b/src/Builders/PromptBuilder.php @@ -4,8 +4,11 @@ namespace WordPress\AiClient\Builders; +use WordPress\AiClient\AiClient; use WordPress\AiClient\Common\Exception\InvalidArgumentException; use WordPress\AiClient\Common\Exception\RuntimeException; +use WordPress\AiClient\Events\AfterPromptSentEvent; +use WordPress\AiClient\Events\BeforePromptSentEvent; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Messages\DTO\Message; @@ -826,7 +829,39 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi $model = $this->getConfiguredModel($capability); + // Dispatch BeforePromptSentEvent (allows message modification) + $beforeEvent = AiClient::dispatchEvent( + new BeforePromptSentEvent($this->messages, $model, $capability) + ); + $messages = $beforeEvent->getMessages(); + // Route to the appropriate generation method based on capability + $result = $this->executeModelGeneration($model, $capability, $messages); + + // Dispatch AfterPromptSentEvent + AiClient::dispatchEvent( + new AfterPromptSentEvent($messages, $model, $capability, $result) + ); + + return $result; + } + + /** + * Executes the model generation based on capability. + * + * @since n.e.x.t + * + * @param ModelInterface $model The model to use for generation. + * @param CapabilityEnum $capability The capability to use. + * @param list $messages The messages to send. + * @return GenerativeAiResult The generated result. + * @throws RuntimeException If the model doesn't support the required capability. + */ + private function executeModelGeneration( + ModelInterface $model, + CapabilityEnum $capability, + array $messages + ): GenerativeAiResult { if ($capability->isTextGeneration()) { if (!$model instanceof TextGenerationModelInterface) { throw new RuntimeException( @@ -836,7 +871,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi ) ); } - return $model->generateTextResult($this->messages); + return $model->generateTextResult($messages); } if ($capability->isImageGeneration()) { @@ -848,7 +883,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi ) ); } - return $model->generateImageResult($this->messages); + return $model->generateImageResult($messages); } if ($capability->isTextToSpeechConversion()) { @@ -860,7 +895,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi ) ); } - return $model->convertTextToSpeechResult($this->messages); + return $model->convertTextToSpeechResult($messages); } if ($capability->isSpeechGeneration()) { @@ -872,15 +907,13 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi ) ); } - return $model->generateSpeechResult($this->messages); + return $model->generateSpeechResult($messages); } if ($capability->isVideoGeneration()) { - // Video generation is not yet implemented throw new RuntimeException('Output modality "video" is not yet supported.'); } - // TODO: Add support for other capabilities when interfaces are available throw new RuntimeException( sprintf('Capability "%s" is not yet supported for generation.', $capability->value) ); diff --git a/src/Events/AfterPromptSentEvent.php b/src/Events/AfterPromptSentEvent.php new file mode 100644 index 00000000..53a04ae5 --- /dev/null +++ b/src/Events/AfterPromptSentEvent.php @@ -0,0 +1,111 @@ + The messages that were sent to the model. + */ + private array $messages; + + /** + * @var ModelInterface The model that processed the prompt. + */ + private ModelInterface $model; + + /** + * @var CapabilityEnum|null The capability that was used for generation. + */ + private ?CapabilityEnum $capability; + + /** + * @var GenerativeAiResult The result from the model. + */ + private GenerativeAiResult $result; + + /** + * Constructor. + * + * @since n.e.x.t + * + * @param list $messages The messages that were sent to the model. + * @param ModelInterface $model The model that processed the prompt. + * @param CapabilityEnum|null $capability The capability that was used for generation. + * @param GenerativeAiResult $result The result from the model. + */ + public function __construct( + array $messages, + ModelInterface $model, + ?CapabilityEnum $capability, + GenerativeAiResult $result + ) { + $this->messages = $messages; + $this->model = $model; + $this->capability = $capability; + $this->result = $result; + } + + /** + * Gets the messages that were sent to the model. + * + * @since n.e.x.t + * + * @return list The messages. + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * Gets the model that processed the prompt. + * + * @since n.e.x.t + * + * @return ModelInterface The model. + */ + public function getModel(): ModelInterface + { + return $this->model; + } + + /** + * Gets the capability that was used for generation. + * + * @since n.e.x.t + * + * @return CapabilityEnum|null The capability, or null if not specified. + */ + public function getCapability(): ?CapabilityEnum + { + return $this->capability; + } + + /** + * Gets the result from the model. + * + * @since n.e.x.t + * + * @return GenerativeAiResult The result. + */ + public function getResult(): GenerativeAiResult + { + return $this->result; + } +} diff --git a/src/Events/BeforePromptSentEvent.php b/src/Events/BeforePromptSentEvent.php new file mode 100644 index 00000000..5b213275 --- /dev/null +++ b/src/Events/BeforePromptSentEvent.php @@ -0,0 +1,103 @@ + The messages to be sent to the model. + */ + private array $messages; + + /** + * @var ModelInterface The model that will process the prompt. + */ + private ModelInterface $model; + + /** + * @var CapabilityEnum|null The capability being used for generation. + */ + private ?CapabilityEnum $capability; + + /** + * Constructor. + * + * @since n.e.x.t + * + * @param list $messages The messages to be sent to the model. + * @param ModelInterface $model The model that will process the prompt. + * @param CapabilityEnum|null $capability The capability being used for generation. + */ + public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability) + { + $this->messages = $messages; + $this->model = $model; + $this->capability = $capability; + } + + /** + * Gets the messages to be sent to the model. + * + * @since n.e.x.t + * + * @return list The messages. + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * Sets the messages to be sent to the model. + * + * This allows listeners to modify the messages before they are sent. + * + * @since n.e.x.t + * + * @param list $messages The modified messages. + * @return void + */ + public function setMessages(array $messages): void + { + $this->messages = $messages; + } + + /** + * Gets the model that will process the prompt. + * + * @since n.e.x.t + * + * @return ModelInterface The model. + */ + public function getModel(): ModelInterface + { + return $this->model; + } + + /** + * Gets the capability being used for generation. + * + * @since n.e.x.t + * + * @return CapabilityEnum|null The capability, or null if not specified. + */ + public function getCapability(): ?CapabilityEnum + { + return $this->capability; + } +} diff --git a/tests/mocks/MockEventDispatcher.php b/tests/mocks/MockEventDispatcher.php new file mode 100644 index 00000000..f7e5507c --- /dev/null +++ b/tests/mocks/MockEventDispatcher.php @@ -0,0 +1,98 @@ + The list of dispatched events. + */ + private array $dispatchedEvents = []; + + /** + * @var array> The registered listeners keyed by event class name. + */ + private array $listeners = []; + + /** + * {@inheritDoc} + * + * @param object $event The event to dispatch. + * @return object The event after being processed by listeners. + */ + public function dispatch(object $event): object + { + $this->dispatchedEvents[] = $event; + + $eventClass = get_class($event); + if (isset($this->listeners[$eventClass])) { + foreach ($this->listeners[$eventClass] as $listener) { + $listener($event); + } + } + + return $event; + } + + /** + * Registers a listener for a specific event class. + * + * @param string $eventClass The event class name. + * @param callable $listener The listener callback. + * @return void + */ + public function addListener(string $eventClass, callable $listener): void + { + if (!isset($this->listeners[$eventClass])) { + $this->listeners[$eventClass] = []; + } + $this->listeners[$eventClass][] = $listener; + } + + /** + * Gets all dispatched events. + * + * @return list The dispatched events. + */ + public function getDispatchedEvents(): array + { + return $this->dispatchedEvents; + } + + /** + * Gets dispatched events of a specific type. + * + * @template T of object + * @param class-string $eventClass The event class to filter by. + * @return list The filtered events. + */ + public function getDispatchedEventsOfType(string $eventClass): array + { + return array_values(array_filter( + $this->dispatchedEvents, + static function (object $event) use ($eventClass): bool { + return $event instanceof $eventClass; + } + )); + } + + /** + * Clears all dispatched events. + * + * @return void + */ + public function clearDispatchedEvents(): void + { + $this->dispatchedEvents = []; + } +} diff --git a/tests/unit/AiClientTest.php b/tests/unit/AiClientTest.php index 8aef3f0b..d75c592b 100644 --- a/tests/unit/AiClientTest.php +++ b/tests/unit/AiClientTest.php @@ -7,12 +7,15 @@ use PHPUnit\Framework\TestCase; use RuntimeException; use WordPress\AiClient\AiClient; +use WordPress\AiClient\Events\AfterPromptSentEvent; +use WordPress\AiClient\Events\BeforePromptSentEvent; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\UserMessage; use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; use WordPress\AiClient\Providers\ProviderRegistry; +use WordPress\AiClient\Tests\mocks\MockEventDispatcher; use WordPress\AiClient\Tests\traits\MockModelCreationTrait; /** @@ -30,7 +33,8 @@ protected function setUp(): void protected function tearDown(): void { - // Tests use dependency injection - registry instances passed directly to methods + // Clean up static event dispatcher after each test + AiClient::setEventDispatcher(null); } /** @@ -742,4 +746,69 @@ public function testGetConfiguredPromptBuilderHelperIntegration(): void $this->expectExceptionMessageMatches('/No models found that support/'); AiClient::generateResult($prompt, null, $this->createMockEmptyRegistry()); } + + /** + * Tests setEventDispatcher and getEventDispatcher methods. + */ + public function testEventDispatcherGetterAndSetter(): void + { + // Initially null + $this->assertNull(AiClient::getEventDispatcher()); + + // Set a dispatcher + $dispatcher = new MockEventDispatcher(); + AiClient::setEventDispatcher($dispatcher); + + $this->assertSame($dispatcher, AiClient::getEventDispatcher()); + + // Set to null + AiClient::setEventDispatcher(null); + $this->assertNull(AiClient::getEventDispatcher()); + } + + /** + * Tests that event dispatcher is passed to PromptBuilder via prompt() method. + */ + public function testEventDispatcherIsPassedToPromptBuilder(): void + { + $dispatcher = new MockEventDispatcher(); + AiClient::setEventDispatcher($dispatcher); + + $expectedResult = $this->createTestResult(); + $mockModel = $this->createMockTextGenerationModel($expectedResult); + $registry = $this->createRegistryWithMockProvider(); + + $result = AiClient::generateTextResult('Test prompt', $mockModel, $registry); + + $this->assertSame($expectedResult, $result); + + // Verify events were dispatched + $beforeEvents = $dispatcher->getDispatchedEventsOfType(BeforePromptSentEvent::class); + $afterEvents = $dispatcher->getDispatchedEventsOfType(AfterPromptSentEvent::class); + + $this->assertCount(1, $beforeEvents); + $this->assertCount(1, $afterEvents); + } + + /** + * Tests that prompt() method creates builder with event dispatcher. + */ + public function testPromptMethodPassesEventDispatcher(): void + { + $dispatcher = new MockEventDispatcher(); + AiClient::setEventDispatcher($dispatcher); + + $expectedResult = $this->createTestResult(); + $mockModel = $this->createMockTextGenerationModel($expectedResult); + $registry = $this->createRegistryWithMockProvider(); + + $result = AiClient::prompt('Test prompt', $registry) + ->usingModel($mockModel) + ->generateTextResult(); + + $this->assertSame($expectedResult, $result); + + // Verify events were dispatched + $this->assertCount(2, $dispatcher->getDispatchedEvents()); + } } diff --git a/tests/unit/Builders/PromptBuilderEventDispatchingTest.php b/tests/unit/Builders/PromptBuilderEventDispatchingTest.php new file mode 100644 index 00000000..ad1f50eb --- /dev/null +++ b/tests/unit/Builders/PromptBuilderEventDispatchingTest.php @@ -0,0 +1,218 @@ +registry = new ProviderRegistry(); + $this->registry->registerProvider(MockProvider::class); + $this->dispatcher = new MockEventDispatcher(); + } + + /** + * Cleans up after each test. + * + * @return void + */ + protected function tearDown(): void + { + // Clean up global event dispatcher + AiClient::setEventDispatcher(null); + } + + /** + * Tests that events are dispatched when a dispatcher is set globally. + * + * @return void + */ + public function testEventsAreDispatchedWhenDispatcherIsSet(): void + { + AiClient::setEventDispatcher($this->dispatcher); + + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $builder = new PromptBuilder($this->registry, 'Hello, world!'); + $builder->usingModel($model); + + $builder->generateTextResult(); + + $beforeEvents = $this->dispatcher->getDispatchedEventsOfType(BeforePromptSentEvent::class); + $afterEvents = $this->dispatcher->getDispatchedEventsOfType(AfterPromptSentEvent::class); + + $this->assertCount(1, $beforeEvents); + $this->assertCount(1, $afterEvents); + } + + /** + * Tests that no events are dispatched when dispatcher is not set. + * + * @return void + */ + public function testNoEventsDispatchedWithoutDispatcher(): void + { + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $builder = new PromptBuilder($this->registry, 'Hello, world!'); + $builder->usingModel($model); + + $builder->generateTextResult(); + + // No dispatcher set, so no events should be dispatched + $this->assertCount(0, $this->dispatcher->getDispatchedEvents()); + } + + /** + * Tests that BeforePromptSentEvent contains correct data. + * + * @return void + */ + public function testBeforePromptSentEventContainsCorrectData(): void + { + AiClient::setEventDispatcher($this->dispatcher); + + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $builder = new PromptBuilder($this->registry, 'Test prompt'); + $builder->usingModel($model); + + $builder->generateTextResult(); + + $beforeEvents = $this->dispatcher->getDispatchedEventsOfType(BeforePromptSentEvent::class); + $this->assertCount(1, $beforeEvents); + + $event = $beforeEvents[0]; + $this->assertCount(1, $event->getMessages()); + $this->assertSame($model, $event->getModel()); + $this->assertEquals(CapabilityEnum::textGeneration(), $event->getCapability()); + } + + /** + * Tests that AfterPromptSentEvent contains correct data. + * + * @return void + */ + public function testAfterPromptSentEventContainsCorrectData(): void + { + AiClient::setEventDispatcher($this->dispatcher); + + $result = $this->createTestResult('Generated response'); + $model = $this->createMockTextGenerationModel($result); + + $builder = new PromptBuilder($this->registry, 'Test prompt'); + $builder->usingModel($model); + + $returnedResult = $builder->generateTextResult(); + + $afterEvents = $this->dispatcher->getDispatchedEventsOfType(AfterPromptSentEvent::class); + $this->assertCount(1, $afterEvents); + + $event = $afterEvents[0]; + $this->assertCount(1, $event->getMessages()); + $this->assertSame($model, $event->getModel()); + $this->assertEquals(CapabilityEnum::textGeneration(), $event->getCapability()); + $this->assertSame($result, $event->getResult()); + $this->assertSame($returnedResult, $event->getResult()); + } + + /** + * Tests that BeforePromptSentEvent can modify messages. + * + * @return void + */ + public function testBeforePromptSentEventCanModifyMessages(): void + { + AiClient::setEventDispatcher($this->dispatcher); + + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + // Register a listener that modifies the messages + $modifiedMessages = [ + new UserMessage([new MessagePart('Modified message')]) + ]; + + $this->dispatcher->addListener( + BeforePromptSentEvent::class, + static function (BeforePromptSentEvent $event) use ($modifiedMessages): void { + $event->setMessages($modifiedMessages); + } + ); + + $builder = new PromptBuilder($this->registry, 'Original message'); + $builder->usingModel($model); + + $builder->generateTextResult(); + + // Verify the modification happened + $afterEvents = $this->dispatcher->getDispatchedEventsOfType(AfterPromptSentEvent::class); + $this->assertCount(1, $afterEvents); + + $afterEvent = $afterEvents[0]; + $this->assertSame($modifiedMessages, $afterEvent->getMessages()); + } + + /** + * Tests that events are dispatched in correct order. + * + * @return void + */ + public function testEventsDispatchedInCorrectOrder(): void + { + AiClient::setEventDispatcher($this->dispatcher); + + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $builder = new PromptBuilder($this->registry, 'Hello'); + $builder->usingModel($model); + + $builder->generateTextResult(); + + $events = $this->dispatcher->getDispatchedEvents(); + $this->assertCount(2, $events); + $this->assertInstanceOf(BeforePromptSentEvent::class, $events[0]); + $this->assertInstanceOf(AfterPromptSentEvent::class, $events[1]); + } +} diff --git a/tests/unit/Events/AfterPromptSentEventTest.php b/tests/unit/Events/AfterPromptSentEventTest.php new file mode 100644 index 00000000..063b9591 --- /dev/null +++ b/tests/unit/Events/AfterPromptSentEventTest.php @@ -0,0 +1,84 @@ +createTestResult('Hello!'); + $model = $this->createMockTextGenerationModel($result); + $capability = CapabilityEnum::textGeneration(); + + $event = new AfterPromptSentEvent($messages, $model, $capability, $result); + + $this->assertSame($messages, $event->getMessages()); + $this->assertSame($model, $event->getModel()); + $this->assertSame($capability, $event->getCapability()); + $this->assertSame($result, $event->getResult()); + } + + /** + * Tests event construction with null capability. + * + * @return void + */ + public function testConstructionWithNullCapability(): void + { + $messages = [ + new UserMessage([new MessagePart('Hello')]) + ]; + $result = $this->createTestResult('Response'); + $model = $this->createMockTextGenerationModel($result); + + $event = new AfterPromptSentEvent($messages, $model, null, $result); + + $this->assertNull($event->getCapability()); + } + + /** + * Tests that result is accessible. + * + * @return void + */ + public function testGetResult(): void + { + $messages = [ + new UserMessage([new MessagePart('Test prompt')]) + ]; + $result = $this->createTestResult('Test response'); + $model = $this->createMockTextGenerationModel($result); + + $event = new AfterPromptSentEvent( + $messages, + $model, + CapabilityEnum::textGeneration(), + $result + ); + + $this->assertSame($result, $event->getResult()); + $this->assertCount(1, $event->getResult()->getCandidates()); + } +} diff --git a/tests/unit/Events/BeforePromptSentEventTest.php b/tests/unit/Events/BeforePromptSentEventTest.php new file mode 100644 index 00000000..d99afd88 --- /dev/null +++ b/tests/unit/Events/BeforePromptSentEventTest.php @@ -0,0 +1,103 @@ +createTestResult(); + $model = $this->createMockTextGenerationModel($result); + $capability = CapabilityEnum::textGeneration(); + + $event = new BeforePromptSentEvent($messages, $model, $capability); + + $this->assertSame($messages, $event->getMessages()); + $this->assertSame($model, $event->getModel()); + $this->assertSame($capability, $event->getCapability()); + } + + /** + * Tests event construction with null capability. + * + * @return void + */ + public function testConstructionWithNullCapability(): void + { + $messages = [ + new UserMessage([new MessagePart('Hello')]) + ]; + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $event = new BeforePromptSentEvent($messages, $model, null); + + $this->assertNull($event->getCapability()); + } + + /** + * Tests message modification. + * + * @return void + */ + public function testSetMessages(): void + { + $originalMessages = [ + new UserMessage([new MessagePart('Original message')]) + ]; + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $event = new BeforePromptSentEvent($originalMessages, $model, null); + + $newMessages = [ + new UserMessage([new MessagePart('Modified message')]) + ]; + $event->setMessages($newMessages); + + $this->assertSame($newMessages, $event->getMessages()); + $this->assertNotSame($originalMessages, $event->getMessages()); + } + + /** + * Tests that the event can hold multiple messages. + * + * @return void + */ + public function testMultipleMessages(): void + { + $messages = [ + new UserMessage([new MessagePart('First message')]), + new UserMessage([new MessagePart('Second message')]), + new UserMessage([new MessagePart('Third message')]) + ]; + $result = $this->createTestResult(); + $model = $this->createMockTextGenerationModel($result); + + $event = new BeforePromptSentEvent($messages, $model, CapabilityEnum::textGeneration()); + + $this->assertCount(3, $event->getMessages()); + } +}