Skip to content
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,59 @@ See the [`PromptBuilder` class](https://github.com/WordPress/php-ai-client/blob/

**More documentation is coming soon.**

## Event Dispatching

The AI Client supports PSR-14 event dispatching for prompt lifecycle events. This allows you to hook into the generation process for logging, monitoring, or other integrations.

### Available Events

- `BeforeGenerateResultEvent` - Dispatched before a prompt is sent to the model
- `AfterGenerateResultEvent` - Dispatched after a result is received from the model

**Important:** Event listeners should not return a value, as they will be ignored. In order to modify data that is passed with the event object, you need to rely on setters on the event object. Any event data for which there are no setters on the event object is meant to be immutable or, in other words, read-only for the event listener.

### Connecting Your Event Dispatcher

To enable event dispatching, pass any PSR-14 compatible `EventDispatcherInterface` to the client:

```php
use WordPress\AiClient\AiClient;

// Set your PSR-14 event dispatcher
AiClient::setEventDispatcher($yourEventDispatcher);

// Events will now be dispatched during generation
$text = AiClient::prompt('Hello, world!')
->generateText();
```

### Example: Logging Events

```php
use WordPress\AiClient\Events\BeforeGenerateResultEvent;
use WordPress\AiClient\Events\AfterGenerateResultEvent;

// In your event listener/subscriber
class AiEventListener
{
public function onBeforeGenerate(BeforeGenerateResultEvent $event): void
{
$model = $event->getModel();
$messages = $event->getMessages();
$capability = $event->getCapability();

// Log, monitor, or perform other actions
}

public function onAfterGenerate(AfterGenerateResultEvent $event): void
{
$result = $event->getResult();

// Log the result, track usage, etc.
}
}
```

## Further reading

For more information on the requirements and guiding principles, please review:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
52 changes: 51 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 39 additions & 2 deletions src/AiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand All @@ -114,6 +120,34 @@ public static function defaultRegistry(): ProviderRegistry
return self::$defaultRegistry;
}

/**
* Sets the event dispatcher for prompt lifecycle events.
*
* The event dispatcher will be used to dispatch BeforeGenerateResultEvent and
* AfterGenerateResultEvent 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;
}

/**
* Checks if a provider is configured and available for use.
*
Expand Down Expand Up @@ -171,7 +205,11 @@ public static function isConfigured($availabilityOrIdOrClassName): bool
*/
public static function prompt($prompt = null, ?ProviderRegistry $registry = null): PromptBuilder
{
return new PromptBuilder($registry ?? self::defaultRegistry(), $prompt);
return new PromptBuilder(
$registry ?? self::defaultRegistry(),
$prompt,
self::$eventDispatcher
);
}

/**
Expand Down Expand Up @@ -225,7 +263,6 @@ public static function generateTextResult(
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateTextResult();
}


/**
* Generates an image using the traditional API approach.
*
Expand Down
73 changes: 66 additions & 7 deletions src/Builders/PromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

namespace WordPress\AiClient\Builders;

use Psr\EventDispatcher\EventDispatcherInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Events\AfterGenerateResultEvent;
use WordPress\AiClient\Events\BeforeGenerateResultEvent;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Messages\DTO\Message;
Expand Down Expand Up @@ -82,6 +85,11 @@ class PromptBuilder
*/
protected ?RequestOptions $requestOptions = null;

/**
* @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
*/
private ?EventDispatcherInterface $eventDispatcher = null;

// phpcs:disable Generic.Files.LineLength.TooLong
/**
* Constructor.
Expand All @@ -90,12 +98,17 @@ class PromptBuilder
*
* @param ProviderRegistry $registry The provider registry for finding suitable models.
* @param Prompt $prompt Optional initial prompt content.
* @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events.
*/
// phpcs:enable Generic.Files.LineLength.TooLong
public function __construct(ProviderRegistry $registry, $prompt = null)
{
public function __construct(
ProviderRegistry $registry,
$prompt = null,
?EventDispatcherInterface $eventDispatcher = null
) {
$this->registry = $registry;
$this->modelConfig = new ModelConfig();
$this->eventDispatcher = $eventDispatcher;

if ($prompt === null) {
return;
Expand Down Expand Up @@ -826,7 +839,38 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi

$model = $this->getConfiguredModel($capability);

// Dispatch BeforeGenerateResultEvent
$this->dispatchEvent(
new BeforeGenerateResultEvent($this->messages, $model, $capability)
);

// Route to the appropriate generation method based on capability
$result = $this->executeModelGeneration($model, $capability, $this->messages);

// Dispatch AfterGenerateResultEvent
$this->dispatchEvent(
new AfterGenerateResultEvent($this->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<Message> $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(
Expand All @@ -836,7 +880,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi
)
);
}
return $model->generateTextResult($this->messages);
return $model->generateTextResult($messages);
}

if ($capability->isImageGeneration()) {
Expand All @@ -848,7 +892,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi
)
);
}
return $model->generateImageResult($this->messages);
return $model->generateImageResult($messages);
}

if ($capability->isTextToSpeechConversion()) {
Expand All @@ -860,7 +904,7 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi
)
);
}
return $model->convertTextToSpeechResult($this->messages);
return $model->convertTextToSpeechResult($messages);
}

if ($capability->isSpeechGeneration()) {
Expand All @@ -872,11 +916,11 @@ public function generateResult(?CapabilityEnum $capability = null): GenerativeAi
)
);
}
return $model->generateSpeechResult($this->messages);
return $model->generateSpeechResult($messages);
}

// Video generation is not yet implemented
if ($capability->isVideoGeneration()) {
// Video generation is not yet implemented
throw new RuntimeException('Output modality "video" is not yet supported.');
}

Expand Down Expand Up @@ -1522,4 +1566,19 @@ private function includeOutputModalities(ModalityEnum ...$modalities): void
$this->modelConfig->setOutputModalities(array_merge($existing, $toAdd));
}
}

/**
* Dispatches an event if an event dispatcher is registered.
*
* @since n.e.x.t
*
* @param object $event The event to dispatch.
* @return void
*/
private function dispatchEvent(object $event): void
{
if ($this->eventDispatcher !== null) {
$this->eventDispatcher->dispatch($event);
}
}
}
Loading