[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Builders/ -> PromptBuilder.php (source)

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Builders;
   5  
   6  use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
   7  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   8  use WordPress\AiClient\Common\Exception\RuntimeException;
   9  use WordPress\AiClient\Events\AfterGenerateResultEvent;
  10  use WordPress\AiClient\Events\BeforeGenerateResultEvent;
  11  use WordPress\AiClient\Files\DTO\File;
  12  use WordPress\AiClient\Files\Enums\FileTypeEnum;
  13  use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
  14  use WordPress\AiClient\Messages\DTO\Message;
  15  use WordPress\AiClient\Messages\DTO\MessagePart;
  16  use WordPress\AiClient\Messages\DTO\UserMessage;
  17  use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
  18  use WordPress\AiClient\Messages\Enums\ModalityEnum;
  19  use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
  20  use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
  21  use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
  22  use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
  23  use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
  24  use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
  25  use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
  26  use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
  27  use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface;
  28  use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
  29  use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
  30  use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface;
  31  use WordPress\AiClient\Providers\ProviderRegistry;
  32  use WordPress\AiClient\Results\DTO\GenerativeAiResult;
  33  use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
  34  use WordPress\AiClient\Tools\DTO\FunctionResponse;
  35  use WordPress\AiClient\Tools\DTO\WebSearch;
  36  /**
  37   * Fluent builder for constructing AI prompts.
  38   *
  39   * This class provides a fluent interface for building prompts with various
  40   * content types and model configurations. It automatically infers model
  41   * requirements based on the features used in the prompt.
  42   *
  43   * @since 0.1.0
  44   *
  45   * @phpstan-import-type MessageArrayShape from Message
  46   * @phpstan-import-type MessagePartArrayShape from MessagePart
  47   *
  48   * @phpstan-type Prompt string|MessagePart|Message|MessageArrayShape|list<string|MessagePart|MessagePartArrayShape>|list<Message>|null
  49   */
  50  class PromptBuilder
  51  {
  52      /**
  53       * @var ProviderRegistry The provider registry for finding suitable models.
  54       */
  55      private ProviderRegistry $registry;
  56      /**
  57       * @var list<Message> The messages in the conversation.
  58       */
  59      protected array $messages = [];
  60      /**
  61       * @var ModelInterface|null The model to use for generation.
  62       */
  63      protected ?ModelInterface $model = null;
  64      /**
  65       * @var list<string> Ordered list of preference keys to check when selecting a model.
  66       */
  67      protected array $modelPreferenceKeys = [];
  68      /**
  69       * @var string|null The provider ID or class name.
  70       */
  71      protected ?string $providerIdOrClassName = null;
  72      /**
  73       * @var ModelConfig The model configuration.
  74       */
  75      protected ModelConfig $modelConfig;
  76      /**
  77       * @var RequestOptions|null The request options for HTTP transport.
  78       */
  79      protected ?RequestOptions $requestOptions = null;
  80      /**
  81       * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
  82       */
  83      private ?EventDispatcherInterface $eventDispatcher = null;
  84      // phpcs:disable Generic.Files.LineLength.TooLong
  85      /**
  86       * Constructor.
  87       *
  88       * @since 0.1.0
  89       *
  90       * @param ProviderRegistry $registry The provider registry for finding suitable models.
  91       * @param Prompt $prompt Optional initial prompt content.
  92       * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events.
  93       */
  94      // phpcs:enable Generic.Files.LineLength.TooLong
  95      public function __construct(ProviderRegistry $registry, $prompt = null, ?EventDispatcherInterface $eventDispatcher = null)
  96      {
  97          $this->registry = $registry;
  98          $this->modelConfig = new ModelConfig();
  99          $this->eventDispatcher = $eventDispatcher;
 100          if ($prompt === null) {
 101              return;
 102          }
 103          // Check if it's a list of Messages - set as messages
 104          if ($this->isMessagesList($prompt)) {
 105              $this->messages = $prompt;
 106              return;
 107          }
 108          // Parse it as a user message
 109          $userMessage = $this->parseMessage($prompt, MessageRoleEnum::user());
 110          $this->messages[] = $userMessage;
 111      }
 112      /**
 113       * Creates a deep clone of this builder.
 114       *
 115       * Clones all mutable state including messages, model configuration, and request options.
 116       * Service objects (registry, model, event dispatcher) are intentionally NOT cloned
 117       * as they are shared dependencies.
 118       *
 119       * @since 0.4.2
 120       */
 121      public function __clone()
 122      {
 123          // Deep clone messages array (Message has __clone)
 124          $clonedMessages = [];
 125          foreach ($this->messages as $message) {
 126              $clonedMessages[] = clone $message;
 127          }
 128          $this->messages = $clonedMessages;
 129          // Clone model config (ModelConfig has __clone)
 130          $this->modelConfig = clone $this->modelConfig;
 131          // Clone request options if set (contains only primitives)
 132          if ($this->requestOptions !== null) {
 133              $this->requestOptions = clone $this->requestOptions;
 134          }
 135          // Note: $registry, $model, and $eventDispatcher are service objects
 136          // and are intentionally NOT cloned - they should be shared references.
 137      }
 138      /**
 139       * Adds text to the current message.
 140       *
 141       * @since 0.1.0
 142       *
 143       * @param string $text The text to add.
 144       * @return self
 145       */
 146      public function withText(string $text): self
 147      {
 148          $part = new MessagePart($text);
 149          $this->appendPartToMessages($part);
 150          return $this;
 151      }
 152      /**
 153       * Adds a file to the current message.
 154       *
 155       * Accepts:
 156       * - File object
 157       * - URL string (remote file)
 158       * - Base64-encoded data string
 159       * - Data URI string (data:mime/type;base64,data)
 160       * - Local file path string
 161       *
 162       * @since 0.1.0
 163       *
 164       * @param string|File $file The file (File object or string representation).
 165       * @param string|null $mimeType The MIME type (optional, ignored if File object provided).
 166       * @return self
 167       * @throws InvalidArgumentException If the file is invalid or MIME type cannot be determined.
 168       */
 169      public function withFile($file, ?string $mimeType = null): self
 170      {
 171          $file = $file instanceof File ? $file : new File($file, $mimeType);
 172          $part = new MessagePart($file);
 173          $this->appendPartToMessages($part);
 174          return $this;
 175      }
 176      /**
 177       * Adds a function response to the current message.
 178       *
 179       * @since 0.1.0
 180       *
 181       * @param FunctionResponse $functionResponse The function response.
 182       * @return self
 183       */
 184      public function withFunctionResponse(FunctionResponse $functionResponse): self
 185      {
 186          $part = new MessagePart($functionResponse);
 187          $this->appendPartToMessages($part);
 188          return $this;
 189      }
 190      /**
 191       * Adds message parts to the current message.
 192       *
 193       * @since 0.1.0
 194       *
 195       * @param MessagePart ...$parts The message parts to add.
 196       * @return self
 197       */
 198      public function withMessageParts(MessagePart ...$parts): self
 199      {
 200          foreach ($parts as $part) {
 201              $this->appendPartToMessages($part);
 202          }
 203          return $this;
 204      }
 205      /**
 206       * Adds conversation history messages.
 207       *
 208       * Historical messages are prepended to the beginning of the message list,
 209       * before the current message being built.
 210       *
 211       * @since 0.1.0
 212       *
 213       * @param Message ...$messages The messages to add to history.
 214       * @return self
 215       */
 216      public function withHistory(Message ...$messages): self
 217      {
 218          // Prepend the history messages to the beginning of the messages array
 219          $this->messages = array_merge($messages, $this->messages);
 220          return $this;
 221      }
 222      /**
 223       * Sets the model to use for generation.
 224       *
 225       * The model's configuration will be merged with the builder's configuration,
 226       * with the builder's configuration taking precedence for any overlapping settings.
 227       *
 228       * @since 0.1.0
 229       *
 230       * @param ModelInterface $model The model to use.
 231       * @return self
 232       */
 233      public function usingModel(ModelInterface $model): self
 234      {
 235          $this->model = $model;
 236          // Merge model's config with builder's config, with builder's config taking precedence
 237          $modelConfigArray = $model->getConfig()->toArray();
 238          $builderConfigArray = $this->modelConfig->toArray();
 239          $mergedConfigArray = array_merge($modelConfigArray, $builderConfigArray);
 240          $this->modelConfig = ModelConfig::fromArray($mergedConfigArray);
 241          return $this;
 242      }
 243      /**
 244       * Sets preferred models to evaluate in order.
 245       *
 246       * @since 0.2.0
 247       *
 248       * @param string|ModelInterface|array{0:string,1:string} ...$preferredModels The preferred models as model IDs,
 249       * model instances, or [provider ID, model ID] tuples. For broader compatibility, it is recommended you specify
 250       * only model IDs or model instances, as that will allow for different providers that expose the same model to be
 251       * considered.
 252       * @return self
 253       *
 254       * @throws InvalidArgumentException When a preferred model has an invalid type or identifier.
 255       */
 256      public function usingModelPreference(...$preferredModels): self
 257      {
 258          if ($preferredModels === []) {
 259              throw new InvalidArgumentException('At least one model preference must be provided.');
 260          }
 261          $preferenceKeys = [];
 262          foreach ($preferredModels as $preferredModel) {
 263              if (is_array($preferredModel)) {
 264                  // [model identifier, provider ID] tuple
 265                  if (!array_is_list($preferredModel) || count($preferredModel) !== 2) {
 266                      throw new InvalidArgumentException('Model preference tuple must contain model identifier and provider ID.');
 267                  }
 268                  [$providerId, $modelId] = $preferredModel;
 269                  $modelId = $this->normalizePreferenceIdentifier($modelId);
 270                  $providerId = $this->normalizePreferenceIdentifier($providerId, 'Model preference provider identifiers cannot be empty.');
 271                  $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
 272              } elseif ($preferredModel instanceof ModelInterface) {
 273                  // Model instance
 274                  $modelId = $preferredModel->metadata()->getId();
 275                  $providerId = $preferredModel->providerMetadata()->getId();
 276                  $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
 277              } elseif (is_string($preferredModel)) {
 278                  // Model ID
 279                  $modelId = $this->normalizePreferenceIdentifier($preferredModel);
 280                  $preferenceKey = $this->createModelPreferenceKey($modelId);
 281              } else {
 282                  // Invalid type
 283                  throw new InvalidArgumentException('Model preferences must be model identifiers, instances of ModelInterface, ' . 'or provider/model tuples.');
 284              }
 285              $preferenceKeys[] = $preferenceKey;
 286          }
 287          $this->modelPreferenceKeys = $preferenceKeys;
 288          return $this;
 289      }
 290      /**
 291       * Sets the model configuration.
 292       *
 293       * Merges the provided configuration with the builder's configuration,
 294       * with builder configuration taking precedence.
 295       *
 296       * @since 0.1.0
 297       *
 298       * @param ModelConfig $config The model configuration to merge.
 299       * @return self
 300       */
 301      public function usingModelConfig(ModelConfig $config): self
 302      {
 303          // Convert both configs to arrays
 304          $builderConfigArray = $this->modelConfig->toArray();
 305          $providedConfigArray = $config->toArray();
 306          // Merge arrays with builder config taking precedence
 307          $mergedArray = array_merge($providedConfigArray, $builderConfigArray);
 308          // Create new config from merged array
 309          $this->modelConfig = ModelConfig::fromArray($mergedArray);
 310          return $this;
 311      }
 312      /**
 313       * Sets the provider to use for generation.
 314       *
 315       * @since 0.1.0
 316       *
 317       * @param string $providerIdOrClassName The provider ID or class name.
 318       * @return self
 319       */
 320      public function usingProvider(string $providerIdOrClassName): self
 321      {
 322          $this->providerIdOrClassName = $providerIdOrClassName;
 323          return $this;
 324      }
 325      /**
 326       * Sets the system instruction.
 327       *
 328       * System instructions are stored in the model configuration and guide
 329       * the AI model's behavior throughout the conversation.
 330       *
 331       * @since 0.1.0
 332       *
 333       * @param string $systemInstruction The system instruction text.
 334       * @return self
 335       */
 336      public function usingSystemInstruction(string $systemInstruction): self
 337      {
 338          $this->modelConfig->setSystemInstruction($systemInstruction);
 339          return $this;
 340      }
 341      /**
 342       * Sets the maximum number of tokens to generate.
 343       *
 344       * @since 0.1.0
 345       *
 346       * @param int $maxTokens The maximum number of tokens.
 347       * @return self
 348       */
 349      public function usingMaxTokens(int $maxTokens): self
 350      {
 351          $this->modelConfig->setMaxTokens($maxTokens);
 352          return $this;
 353      }
 354      /**
 355       * Sets the temperature for generation.
 356       *
 357       * @since 0.1.0
 358       *
 359       * @param float $temperature The temperature value.
 360       * @return self
 361       */
 362      public function usingTemperature(float $temperature): self
 363      {
 364          $this->modelConfig->setTemperature($temperature);
 365          return $this;
 366      }
 367      /**
 368       * Sets the top-p value for generation.
 369       *
 370       * @since 0.1.0
 371       *
 372       * @param float $topP The top-p value.
 373       * @return self
 374       */
 375      public function usingTopP(float $topP): self
 376      {
 377          $this->modelConfig->setTopP($topP);
 378          return $this;
 379      }
 380      /**
 381       * Sets the top-k value for generation.
 382       *
 383       * @since 0.1.0
 384       *
 385       * @param int $topK The top-k value.
 386       * @return self
 387       */
 388      public function usingTopK(int $topK): self
 389      {
 390          $this->modelConfig->setTopK($topK);
 391          return $this;
 392      }
 393      /**
 394       * Sets stop sequences for generation.
 395       *
 396       * @since 0.1.0
 397       *
 398       * @param string ...$stopSequences The stop sequences.
 399       * @return self
 400       */
 401      public function usingStopSequences(string ...$stopSequences): self
 402      {
 403          $this->modelConfig->setStopSequences($stopSequences);
 404          return $this;
 405      }
 406      /**
 407       * Sets the number of candidates to generate.
 408       *
 409       * @since 0.1.0
 410       *
 411       * @param int $candidateCount The number of candidates.
 412       * @return self
 413       */
 414      public function usingCandidateCount(int $candidateCount): self
 415      {
 416          $this->modelConfig->setCandidateCount($candidateCount);
 417          return $this;
 418      }
 419      /**
 420       * Sets the function declarations available to the model.
 421       *
 422       * @since 0.1.0
 423       *
 424       * @param FunctionDeclaration ...$functionDeclarations The function declarations.
 425       * @return self
 426       */
 427      public function usingFunctionDeclarations(FunctionDeclaration ...$functionDeclarations): self
 428      {
 429          $this->modelConfig->setFunctionDeclarations($functionDeclarations);
 430          return $this;
 431      }
 432      /**
 433       * Sets the presence penalty for generation.
 434       *
 435       * @since 0.1.0
 436       *
 437       * @param float $presencePenalty The presence penalty value.
 438       * @return self
 439       */
 440      public function usingPresencePenalty(float $presencePenalty): self
 441      {
 442          $this->modelConfig->setPresencePenalty($presencePenalty);
 443          return $this;
 444      }
 445      /**
 446       * Sets the frequency penalty for generation.
 447       *
 448       * @since 0.1.0
 449       *
 450       * @param float $frequencyPenalty The frequency penalty value.
 451       * @return self
 452       */
 453      public function usingFrequencyPenalty(float $frequencyPenalty): self
 454      {
 455          $this->modelConfig->setFrequencyPenalty($frequencyPenalty);
 456          return $this;
 457      }
 458      /**
 459       * Sets the web search configuration.
 460       *
 461       * @since 0.1.0
 462       *
 463       * @param WebSearch $webSearch The web search configuration.
 464       * @return self
 465       */
 466      public function usingWebSearch(WebSearch $webSearch): self
 467      {
 468          $this->modelConfig->setWebSearch($webSearch);
 469          return $this;
 470      }
 471      /**
 472       * Sets the request options for HTTP transport.
 473       *
 474       * @since 0.3.0
 475       *
 476       * @param RequestOptions $requestOptions The request options.
 477       * @return self
 478       */
 479      public function usingRequestOptions(RequestOptions $requestOptions): self
 480      {
 481          $this->requestOptions = $requestOptions;
 482          return $this;
 483      }
 484      /**
 485       * Sets the top log probabilities configuration.
 486       *
 487       * If $topLogprobs is null, enables log probabilities.
 488       * If $topLogprobs has a value, enables log probabilities and sets the number of top log probabilities to return.
 489       *
 490       * @since 0.1.0
 491       *
 492       * @param int|null $topLogprobs The number of top log probabilities to return, or null to enable log probabilities.
 493       * @return self
 494       */
 495      public function usingTopLogprobs(?int $topLogprobs = null): self
 496      {
 497          // Always enable log probabilities
 498          $this->modelConfig->setLogprobs(\true);
 499          // If a specific number is provided, set it
 500          if ($topLogprobs !== null) {
 501              $this->modelConfig->setTopLogprobs($topLogprobs);
 502          }
 503          return $this;
 504      }
 505      /**
 506       * Sets the output MIME type.
 507       *
 508       * @since 0.1.0
 509       *
 510       * @param string $mimeType The MIME type.
 511       * @return self
 512       */
 513      public function asOutputMimeType(string $mimeType): self
 514      {
 515          $this->modelConfig->setOutputMimeType($mimeType);
 516          return $this;
 517      }
 518      /**
 519       * Sets the output schema.
 520       *
 521       * @since 0.1.0
 522       *
 523       * @param array<string, mixed> $schema The output schema.
 524       * @return self
 525       */
 526      public function asOutputSchema(array $schema): self
 527      {
 528          $this->modelConfig->setOutputSchema($schema);
 529          return $this;
 530      }
 531      /**
 532       * Sets the output modalities.
 533       *
 534       * @since 0.1.0
 535       *
 536       * @param ModalityEnum ...$modalities The output modalities.
 537       * @return self
 538       */
 539      public function asOutputModalities(ModalityEnum ...$modalities): self
 540      {
 541          $this->modelConfig->setOutputModalities($modalities);
 542          return $this;
 543      }
 544      /**
 545       * Sets the output file type.
 546       *
 547       * @since 0.1.0
 548       *
 549       * @param FileTypeEnum $fileType The output file type.
 550       * @return self
 551       */
 552      public function asOutputFileType(FileTypeEnum $fileType): self
 553      {
 554          $this->modelConfig->setOutputFileType($fileType);
 555          return $this;
 556      }
 557      /**
 558       * Sets the output media orientation.
 559       *
 560       * @since 1.3.0
 561       *
 562       * @param MediaOrientationEnum $orientation The output media orientation.
 563       * @return self
 564       */
 565      public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self
 566      {
 567          $this->modelConfig->setOutputMediaOrientation($orientation);
 568          return $this;
 569      }
 570      /**
 571       * Sets the output media aspect ratio.
 572       *
 573       * If set, this supersedes the output media orientation, as it is a more
 574       * specific configuration.
 575       *
 576       * @since 1.3.0
 577       *
 578       * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2").
 579       * @return self
 580       */
 581      public function asOutputMediaAspectRatio(string $aspectRatio): self
 582      {
 583          $this->modelConfig->setOutputMediaAspectRatio($aspectRatio);
 584          return $this;
 585      }
 586      /**
 587       * Sets the output speech voice.
 588       *
 589       * @since 1.3.0
 590       *
 591       * @param string $voice The output speech voice.
 592       * @return self
 593       */
 594      public function asOutputSpeechVoice(string $voice): self
 595      {
 596          $this->modelConfig->setOutputSpeechVoice($voice);
 597          return $this;
 598      }
 599      /**
 600       * Configures the prompt for JSON response output.
 601       *
 602       * @since 0.1.0
 603       *
 604       * @param array<string, mixed>|null $schema Optional JSON schema.
 605       * @return self
 606       */
 607      public function asJsonResponse(?array $schema = null): self
 608      {
 609          $this->asOutputMimeType('application/json');
 610          if ($schema !== null) {
 611              $this->asOutputSchema($schema);
 612          }
 613          return $this;
 614      }
 615      /**
 616       * Infers the capability from configured output modalities.
 617       *
 618       * @since 0.1.0
 619       *
 620       * @return CapabilityEnum The inferred capability.
 621       * @throws RuntimeException If the output modality is not supported.
 622       */
 623      private function inferCapabilityFromOutputModalities(): CapabilityEnum
 624      {
 625          // Get the configured output modalities
 626          $outputModalities = $this->modelConfig->getOutputModalities();
 627          // Default to text if no output modality is specified
 628          if ($outputModalities === null || empty($outputModalities)) {
 629              return CapabilityEnum::textGeneration();
 630          }
 631          // Multi-modal output (multiple modalities) defaults to text generation. This is temporary
 632          // as a multi-modal interface will be implemented in the future.
 633          if (count($outputModalities) > 1) {
 634              return CapabilityEnum::textGeneration();
 635          }
 636          // Infer capability from single output modality
 637          $outputModality = $outputModalities[0];
 638          if ($outputModality->isText()) {
 639              return CapabilityEnum::textGeneration();
 640          } elseif ($outputModality->isImage()) {
 641              return CapabilityEnum::imageGeneration();
 642          } elseif ($outputModality->isAudio()) {
 643              return CapabilityEnum::speechGeneration();
 644          } elseif ($outputModality->isVideo()) {
 645              return CapabilityEnum::videoGeneration();
 646          } else {
 647              // For unsupported modalities, provide a clear error message
 648              throw new RuntimeException(sprintf('Output modality "%s" is not yet supported.', $outputModality->value));
 649          }
 650      }
 651      /**
 652       * Infers the capability from a model's implemented interfaces.
 653       *
 654       * @since 0.1.0
 655       *
 656       * @param ModelInterface $model The model to infer capability from.
 657       * @return CapabilityEnum|null The inferred capability, or null if none can be inferred.
 658       */
 659      private function inferCapabilityFromModelInterfaces(ModelInterface $model): ?CapabilityEnum
 660      {
 661          // Check model interfaces in order of preference
 662          if ($model instanceof TextGenerationModelInterface) {
 663              return CapabilityEnum::textGeneration();
 664          }
 665          if ($model instanceof ImageGenerationModelInterface) {
 666              return CapabilityEnum::imageGeneration();
 667          }
 668          if ($model instanceof TextToSpeechConversionModelInterface) {
 669              return CapabilityEnum::textToSpeechConversion();
 670          }
 671          if ($model instanceof SpeechGenerationModelInterface) {
 672              return CapabilityEnum::speechGeneration();
 673          }
 674          if ($model instanceof VideoGenerationModelInterface) {
 675              return CapabilityEnum::videoGeneration();
 676          }
 677          // No supported interface found
 678          return null;
 679      }
 680      /**
 681       * Checks if the current prompt is supported by the selected model.
 682       *
 683       * @since 0.1.0
 684       * @since 0.3.0 Method visibility changed to public.
 685       *
 686       * @param CapabilityEnum|null $capability Optional capability to check support for.
 687       * @return bool True if supported, false otherwise.
 688       */
 689      public function isSupported(?CapabilityEnum $capability = null): bool
 690      {
 691          // If no intended capability provided, infer from output modalities
 692          if ($capability === null) {
 693              // First try to infer from a specific model if one is set
 694              if ($this->model !== null) {
 695                  $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
 696                  if ($inferredCapability !== null) {
 697                      $capability = $inferredCapability;
 698                  }
 699              }
 700              // If still no capability, infer from output modalities
 701              if ($capability === null) {
 702                  $capability = $this->inferCapabilityFromOutputModalities();
 703              }
 704          }
 705          // Build requirements with the specified capability
 706          $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
 707          // If the model has been set, check if it meets the requirements
 708          if ($this->model !== null) {
 709              return $requirements->areMetBy($this->model->metadata());
 710          }
 711          try {
 712              // Check if any models support these requirements
 713              $models = $this->registry->findModelsMetadataForSupport($requirements);
 714              return !empty($models);
 715          } catch (InvalidArgumentException $e) {
 716              // No models support the requirements
 717              return \false;
 718          }
 719      }
 720      /**
 721       * Checks if the prompt is supported for text generation.
 722       *
 723       * @since 0.1.0
 724       *
 725       * @return bool True if text generation is supported.
 726       */
 727      public function isSupportedForTextGeneration(): bool
 728      {
 729          return $this->isSupported(CapabilityEnum::textGeneration());
 730      }
 731      /**
 732       * Checks if the prompt is supported for image generation.
 733       *
 734       * @since 0.1.0
 735       *
 736       * @return bool True if image generation is supported.
 737       */
 738      public function isSupportedForImageGeneration(): bool
 739      {
 740          return $this->isSupported(CapabilityEnum::imageGeneration());
 741      }
 742      /**
 743       * Checks if the prompt is supported for text to speech conversion.
 744       *
 745       * @since 0.1.0
 746       *
 747       * @return bool True if text to speech conversion is supported.
 748       */
 749      public function isSupportedForTextToSpeechConversion(): bool
 750      {
 751          return $this->isSupported(CapabilityEnum::textToSpeechConversion());
 752      }
 753      /**
 754       * Checks if the prompt is supported for video generation.
 755       *
 756       * @since 0.1.0
 757       *
 758       * @return bool True if video generation is supported.
 759       */
 760      public function isSupportedForVideoGeneration(): bool
 761      {
 762          return $this->isSupported(CapabilityEnum::videoGeneration());
 763      }
 764      /**
 765       * Checks if the prompt is supported for speech generation.
 766       *
 767       * @since 0.1.0
 768       *
 769       * @return bool True if speech generation is supported.
 770       */
 771      public function isSupportedForSpeechGeneration(): bool
 772      {
 773          return $this->isSupported(CapabilityEnum::speechGeneration());
 774      }
 775      /**
 776       * Checks if the prompt is supported for music generation.
 777       *
 778       * @since 0.1.0
 779       *
 780       * @return bool True if music generation is supported.
 781       */
 782      public function isSupportedForMusicGeneration(): bool
 783      {
 784          return $this->isSupported(CapabilityEnum::musicGeneration());
 785      }
 786      /**
 787       * Checks if the prompt is supported for embedding generation.
 788       *
 789       * @since 0.1.0
 790       *
 791       * @return bool True if embedding generation is supported.
 792       */
 793      public function isSupportedForEmbeddingGeneration(): bool
 794      {
 795          return $this->isSupported(CapabilityEnum::embeddingGeneration());
 796      }
 797      /**
 798       * Generates a result from the prompt.
 799       *
 800       * This is the primary execution method that generates a result (containing
 801       * potentially multiple candidates) based on the specified capability or
 802       * the configured output modality.
 803       *
 804       * @since 0.1.0
 805       *
 806       * @param CapabilityEnum|null $capability Optional capability to use for generation.
 807       *                                        If null, capability is inferred from output modality.
 808       * @return GenerativeAiResult The generated result containing candidates.
 809       * @throws InvalidArgumentException If the prompt or model validation fails.
 810       * @throws RuntimeException If the model doesn't support the required capability.
 811       */
 812      public function generateResult(?CapabilityEnum $capability = null): GenerativeAiResult
 813      {
 814          $this->validateMessages();
 815          // If capability is not provided, infer it
 816          if ($capability === null) {
 817              // First try to infer from a specific model if one is set
 818              if ($this->model !== null) {
 819                  $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
 820                  if ($inferredCapability !== null) {
 821                      $capability = $inferredCapability;
 822                  }
 823              }
 824              // If still no capability, infer from output modalities
 825              if ($capability === null) {
 826                  $capability = $this->inferCapabilityFromOutputModalities();
 827              }
 828          }
 829          $model = $this->getConfiguredModel($capability);
 830          // Dispatch BeforeGenerateResultEvent
 831          $this->dispatchEvent(new BeforeGenerateResultEvent($this->messages, $model, $capability));
 832          // Route to the appropriate generation method based on capability
 833          $result = $this->executeModelGeneration($model, $capability, $this->messages);
 834          // Dispatch AfterGenerateResultEvent
 835          $this->dispatchEvent(new AfterGenerateResultEvent($this->messages, $model, $capability, $result));
 836          return $result;
 837      }
 838      /**
 839       * Executes the model generation based on capability.
 840       *
 841       * @since 0.4.0
 842       *
 843       * @param ModelInterface $model The model to use for generation.
 844       * @param CapabilityEnum $capability The capability to use.
 845       * @param list<Message> $messages The messages to send.
 846       * @return GenerativeAiResult The generated result.
 847       * @throws RuntimeException If the model doesn't support the required capability.
 848       */
 849      private function executeModelGeneration(ModelInterface $model, CapabilityEnum $capability, array $messages): GenerativeAiResult
 850      {
 851          if ($capability->isTextGeneration()) {
 852              if (!$model instanceof TextGenerationModelInterface) {
 853                  throw new RuntimeException(sprintf('Model "%s" does not support text generation.', $model->metadata()->getId()));
 854              }
 855              return $model->generateTextResult($messages);
 856          }
 857          if ($capability->isImageGeneration()) {
 858              if (!$model instanceof ImageGenerationModelInterface) {
 859                  throw new RuntimeException(sprintf('Model "%s" does not support image generation.', $model->metadata()->getId()));
 860              }
 861              return $model->generateImageResult($messages);
 862          }
 863          if ($capability->isTextToSpeechConversion()) {
 864              if (!$model instanceof TextToSpeechConversionModelInterface) {
 865                  throw new RuntimeException(sprintf('Model "%s" does not support text-to-speech conversion.', $model->metadata()->getId()));
 866              }
 867              return $model->convertTextToSpeechResult($messages);
 868          }
 869          if ($capability->isSpeechGeneration()) {
 870              if (!$model instanceof SpeechGenerationModelInterface) {
 871                  throw new RuntimeException(sprintf('Model "%s" does not support speech generation.', $model->metadata()->getId()));
 872              }
 873              return $model->generateSpeechResult($messages);
 874          }
 875          if ($capability->isVideoGeneration()) {
 876              if (!$model instanceof VideoGenerationModelInterface) {
 877                  throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId()));
 878              }
 879              return $model->generateVideoResult($messages);
 880          }
 881          // TODO: Add support for other capabilities when interfaces are available
 882          throw new RuntimeException(sprintf('Capability "%s" is not yet supported for generation.', $capability->value));
 883      }
 884      /**
 885       * Generates a text result from the prompt.
 886       *
 887       * @since 0.1.0
 888       *
 889       * @return GenerativeAiResult The generated result containing text candidates.
 890       * @throws InvalidArgumentException If the prompt or model validation fails.
 891       * @throws RuntimeException If the model doesn't support text generation.
 892       */
 893      public function generateTextResult(): GenerativeAiResult
 894      {
 895          // Include text in output modalities
 896          $this->includeOutputModalities(ModalityEnum::text());
 897          // Generate and return the result with text generation capability
 898          return $this->generateResult(CapabilityEnum::textGeneration());
 899      }
 900      /**
 901       * Generates an image result from the prompt.
 902       *
 903       * @since 0.1.0
 904       *
 905       * @return GenerativeAiResult The generated result containing image candidates.
 906       * @throws InvalidArgumentException If the prompt or model validation fails.
 907       * @throws RuntimeException If the model doesn't support image generation.
 908       */
 909      public function generateImageResult(): GenerativeAiResult
 910      {
 911          // Include image in output modalities
 912          $this->includeOutputModalities(ModalityEnum::image());
 913          // Generate and return the result with image generation capability
 914          return $this->generateResult(CapabilityEnum::imageGeneration());
 915      }
 916      /**
 917       * Generates a speech result from the prompt.
 918       *
 919       * @since 0.1.0
 920       *
 921       * @return GenerativeAiResult The generated result containing speech audio candidates.
 922       * @throws InvalidArgumentException If the prompt or model validation fails.
 923       * @throws RuntimeException If the model doesn't support speech generation.
 924       */
 925      public function generateSpeechResult(): GenerativeAiResult
 926      {
 927          // Include audio in output modalities
 928          $this->includeOutputModalities(ModalityEnum::audio());
 929          // Generate and return the result with speech generation capability
 930          return $this->generateResult(CapabilityEnum::speechGeneration());
 931      }
 932      /**
 933       * Converts text to speech and returns the result.
 934       *
 935       * @since 0.1.0
 936       *
 937       * @return GenerativeAiResult The generated result containing speech audio candidates.
 938       * @throws InvalidArgumentException If the prompt or model validation fails.
 939       * @throws RuntimeException If the model doesn't support text-to-speech conversion.
 940       */
 941      public function convertTextToSpeechResult(): GenerativeAiResult
 942      {
 943          // Include audio in output modalities
 944          $this->includeOutputModalities(ModalityEnum::audio());
 945          // Generate and return the result with text-to-speech conversion capability
 946          return $this->generateResult(CapabilityEnum::textToSpeechConversion());
 947      }
 948      /**
 949       * Generates a video result from the prompt.
 950       *
 951       * @since 1.3.0
 952       *
 953       * @return GenerativeAiResult The generated result containing video candidates.
 954       * @throws InvalidArgumentException If the prompt or model validation fails.
 955       * @throws RuntimeException If the model doesn't support video generation.
 956       */
 957      public function generateVideoResult(): GenerativeAiResult
 958      {
 959          // Include video in output modalities
 960          $this->includeOutputModalities(ModalityEnum::video());
 961          // Generate and return the result with video generation capability
 962          return $this->generateResult(CapabilityEnum::videoGeneration());
 963      }
 964      /**
 965       * Generates text from the prompt.
 966       *
 967       * @since 0.1.0
 968       *
 969       * @return string The generated text.
 970       * @throws InvalidArgumentException If the prompt or model validation fails.
 971       */
 972      public function generateText(): string
 973      {
 974          return $this->generateTextResult()->toText();
 975      }
 976      /**
 977       * Generates multiple text candidates from the prompt.
 978       *
 979       * @since 0.1.0
 980       *
 981       * @param int|null $candidateCount The number of candidates to generate.
 982       * @return list<string> The generated texts.
 983       * @throws InvalidArgumentException If the prompt or model validation fails.
 984       */
 985      public function generateTexts(?int $candidateCount = null): array
 986      {
 987          if ($candidateCount !== null) {
 988              $this->usingCandidateCount($candidateCount);
 989          }
 990          // Generate text result
 991          return $this->generateTextResult()->toTexts();
 992      }
 993      /**
 994       * Generates an image from the prompt.
 995       *
 996       * @since 0.1.0
 997       *
 998       * @return File The generated image file.
 999       * @throws InvalidArgumentException If the prompt or model validation fails.
1000       * @throws RuntimeException If no image is generated.
1001       */
1002      public function generateImage(): File
1003      {
1004          return $this->generateImageResult()->toFile();
1005      }
1006      /**
1007       * Generates multiple images from the prompt.
1008       *
1009       * @since 0.1.0
1010       *
1011       * @param int|null $candidateCount The number of images to generate.
1012       * @return list<File> The generated image files.
1013       * @throws InvalidArgumentException If the prompt or model validation fails.
1014       * @throws RuntimeException If no images are generated.
1015       */
1016      public function generateImages(?int $candidateCount = null): array
1017      {
1018          if ($candidateCount !== null) {
1019              $this->usingCandidateCount($candidateCount);
1020          }
1021          return $this->generateImageResult()->toFiles();
1022      }
1023      /**
1024       * Converts text to speech.
1025       *
1026       * @since 0.1.0
1027       *
1028       * @return File The generated speech audio file.
1029       * @throws InvalidArgumentException If the prompt or model validation fails.
1030       * @throws RuntimeException If no audio is generated.
1031       */
1032      public function convertTextToSpeech(): File
1033      {
1034          return $this->convertTextToSpeechResult()->toFile();
1035      }
1036      /**
1037       * Converts text to multiple speech outputs.
1038       *
1039       * @since 0.1.0
1040       *
1041       * @param int|null $candidateCount The number of speech outputs to generate.
1042       * @return list<File> The generated speech audio files.
1043       * @throws InvalidArgumentException If the prompt or model validation fails.
1044       * @throws RuntimeException If no audio is generated.
1045       */
1046      public function convertTextToSpeeches(?int $candidateCount = null): array
1047      {
1048          if ($candidateCount !== null) {
1049              $this->usingCandidateCount($candidateCount);
1050          }
1051          return $this->convertTextToSpeechResult()->toFiles();
1052      }
1053      /**
1054       * Generates speech from the prompt.
1055       *
1056       * @since 0.1.0
1057       *
1058       * @return File The generated speech audio file.
1059       * @throws InvalidArgumentException If the prompt or model validation fails.
1060       * @throws RuntimeException If no audio is generated.
1061       */
1062      public function generateSpeech(): File
1063      {
1064          return $this->generateSpeechResult()->toFile();
1065      }
1066      /**
1067       * Generates multiple speech outputs from the prompt.
1068       *
1069       * @since 0.1.0
1070       *
1071       * @param int|null $candidateCount The number of speech outputs to generate.
1072       * @return list<File> The generated speech audio files.
1073       * @throws InvalidArgumentException If the prompt or model validation fails.
1074       * @throws RuntimeException If no audio is generated.
1075       */
1076      public function generateSpeeches(?int $candidateCount = null): array
1077      {
1078          if ($candidateCount !== null) {
1079              $this->usingCandidateCount($candidateCount);
1080          }
1081          return $this->generateSpeechResult()->toFiles();
1082      }
1083      /**
1084       * Generates a video from the prompt.
1085       *
1086       * @since 1.3.0
1087       *
1088       * @return File The generated video file.
1089       * @throws InvalidArgumentException If the prompt or model validation fails.
1090       * @throws RuntimeException If no video is generated.
1091       */
1092      public function generateVideo(): File
1093      {
1094          return $this->generateVideoResult()->toFile();
1095      }
1096      /**
1097       * Generates multiple videos from the prompt.
1098       *
1099       * @since 1.3.0
1100       *
1101       * @param int|null $candidateCount The number of videos to generate.
1102       * @return list<File> The generated video files.
1103       * @throws InvalidArgumentException If the prompt or model validation fails.
1104       * @throws RuntimeException If no videos are generated.
1105       */
1106      public function generateVideos(?int $candidateCount = null): array
1107      {
1108          if ($candidateCount !== null) {
1109              $this->usingCandidateCount($candidateCount);
1110          }
1111          return $this->generateVideoResult()->toFiles();
1112      }
1113      /**
1114       * Appends a MessagePart to the messages array.
1115       *
1116       * If the last message has a user role, the part is added to it.
1117       * Otherwise, a new UserMessage is created with the part.
1118       *
1119       * @since 0.1.0
1120       *
1121       * @param MessagePart $part The part to append.
1122       * @return void
1123       */
1124      protected function appendPartToMessages(MessagePart $part): void
1125      {
1126          $lastMessage = end($this->messages);
1127          if ($lastMessage instanceof Message && $lastMessage->getRole()->isUser()) {
1128              // Replace the last message with a new one containing the appended part
1129              array_pop($this->messages);
1130              $this->messages[] = $lastMessage->withPart($part);
1131              return;
1132          }
1133          // Create new UserMessage with the part
1134          $this->messages[] = new UserMessage([$part]);
1135      }
1136      /**
1137       * Gets the model to use for generation.
1138       *
1139       * If a model has been explicitly set, validates it meets requirements and returns it.
1140       * Otherwise, finds a suitable model based on the prompt requirements.
1141       *
1142       * @since 0.1.0
1143       *
1144       * @param CapabilityEnum $capability The capability the model will be using.
1145       * @return ModelInterface The model to use.
1146       * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements.
1147       */
1148      private function getConfiguredModel(CapabilityEnum $capability): ModelInterface
1149      {
1150          $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
1151          if ($this->model !== null) {
1152              // Explicit model was provided via usingModel(); just update config and bind dependencies.
1153              $model = $this->model;
1154              $model->setConfig($this->modelConfig);
1155              $this->registry->bindModelDependencies($model);
1156              $this->bindModelRequestOptions($model);
1157              return $model;
1158          }
1159          // Retrieve the candidate models map which satisfies the requirements.
1160          $candidateMap = $this->getCandidateModelsMap($requirements);
1161          if (empty($candidateMap)) {
1162              $message = sprintf('No models found that support %s for this prompt.', $capability->value);
1163              if ($this->providerIdOrClassName !== null) {
1164                  $message = sprintf('No models found for provider "%s" that support %s for this prompt.', $this->providerIdOrClassName, $capability->value);
1165              }
1166              throw new InvalidArgumentException($message);
1167          }
1168          // Check if any preferred models match the candidates, in priority order.
1169          if (!empty($this->modelPreferenceKeys)) {
1170              // Find preferences that match available candidates, preserving preference order.
1171              $matchingPreferences = array_intersect_key(array_flip($this->modelPreferenceKeys), $candidateMap);
1172              if (!empty($matchingPreferences)) {
1173                  // Get the first matching preference key
1174                  $firstMatchKey = key($matchingPreferences);
1175                  [$providerId, $modelId] = $candidateMap[$firstMatchKey];
1176                  $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
1177                  $this->bindModelRequestOptions($model);
1178                  return $model;
1179              }
1180          }
1181          // No preference matched; fall back to the first candidate discovered.
1182          [$providerId, $modelId] = reset($candidateMap);
1183          $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
1184          $this->bindModelRequestOptions($model);
1185          return $model;
1186      }
1187      /**
1188       * Binds configured request options to the model if present and supported.
1189       *
1190       * Request options are only applicable to API-based models that make HTTP requests.
1191       *
1192       * @since 0.3.0
1193       *
1194       * @param ModelInterface $model The model to bind request options to.
1195       * @return void
1196       */
1197      private function bindModelRequestOptions(ModelInterface $model): void
1198      {
1199          if ($this->requestOptions !== null && $model instanceof ApiBasedModelInterface) {
1200              $model->setRequestOptions($this->requestOptions);
1201          }
1202      }
1203      /**
1204       * Builds a map of candidate models that satisfy the requirements for efficient lookup.
1205       *
1206       * @since 0.2.0
1207       *
1208       * @param ModelRequirements $requirements The requirements derived from the prompt.
1209       * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
1210       */
1211      private function getCandidateModelsMap(ModelRequirements $requirements): array
1212      {
1213          if ($this->providerIdOrClassName === null) {
1214              // No provider locked in, gather all models across providers that meet requirements.
1215              $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements);
1216              $candidateMap = [];
1217              foreach ($providerModelsMetadata as $providerModels) {
1218                  $providerId = $providerModels->getProvider()->getId();
1219                  $providerMap = $this->generateMapFromCandidates($providerId, $providerModels->getModels());
1220                  // Use + operator to merge, preserving keys from $candidateMap (first provider wins for model-only keys)
1221                  $candidateMap = $candidateMap + $providerMap;
1222              }
1223              return $candidateMap;
1224          }
1225          // Provider set, only consider models from that provider.
1226          $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport($this->providerIdOrClassName, $requirements);
1227          // Ensure we pass the provider ID, not the class name
1228          $providerId = $this->registry->getProviderId($this->providerIdOrClassName);
1229          return $this->generateMapFromCandidates($providerId, $modelsMetadata);
1230      }
1231      /**
1232       * Generates a candidate map from model metadata with both provider-specific and model-only keys.
1233       *
1234       * @since 0.2.0
1235       *
1236       * @param string $providerId The provider ID.
1237       * @param list<ModelMetadata> $modelsMetadata The models metadata to map.
1238       * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
1239       */
1240      private function generateMapFromCandidates(string $providerId, array $modelsMetadata): array
1241      {
1242          $map = [];
1243          foreach ($modelsMetadata as $modelMetadata) {
1244              $modelId = $modelMetadata->getId();
1245              // Add provider-specific key
1246              $providerModelKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
1247              $map[$providerModelKey] = [$providerId, $modelId];
1248              // Add model-only key
1249              $modelKey = $this->createModelPreferenceKey($modelId);
1250              $map[$modelKey] = [$providerId, $modelId];
1251          }
1252          return $map;
1253      }
1254      /**
1255       * Normalizes and validates a preference identifier string.
1256       *
1257       * @since 0.2.0
1258       *
1259       * @param mixed $value The value to normalize.
1260       * @param string $emptyMessage The message for empty or invalid values.
1261       * @return string The normalized identifier.
1262       *
1263       * @throws InvalidArgumentException If the value is not a non-empty string.
1264       */
1265      private function normalizePreferenceIdentifier($value, string $emptyMessage = 'Model preference identifiers cannot be empty.'): string
1266      {
1267          if (!is_string($value)) {
1268              throw new InvalidArgumentException($emptyMessage);
1269          }
1270          $trimmed = trim($value);
1271          if ($trimmed === '') {
1272              throw new InvalidArgumentException($emptyMessage);
1273          }
1274          return $trimmed;
1275      }
1276      /**
1277       * Creates a preference key for a provider/model combination.
1278       *
1279       * @since 0.2.0
1280       *
1281       * @param string $providerId The provider identifier.
1282       * @param string $modelId The model identifier.
1283       * @return string The generated preference key.
1284       */
1285      private function createProviderModelPreferenceKey(string $providerId, string $modelId): string
1286      {
1287          return 'providerModel::' . $providerId . '::' . $modelId;
1288      }
1289      /**
1290       * Creates a preference key for a model identifier.
1291       *
1292       * @since 0.2.0
1293       *
1294       * @param string $modelId The model identifier.
1295       * @return string The generated preference key.
1296       */
1297      private function createModelPreferenceKey(string $modelId): string
1298      {
1299          return 'model::' . $modelId;
1300      }
1301      /**
1302       * Parses various input types into a Message with the given role.
1303       *
1304       * @since 0.1.0
1305       *
1306       * @param mixed $input The input to parse.
1307       * @param MessageRoleEnum $defaultRole The role for the message if not specified by input.
1308       * @return Message The parsed message.
1309       * @throws InvalidArgumentException If the input type is not supported or results in empty message.
1310       */
1311      private function parseMessage($input, MessageRoleEnum $defaultRole): Message
1312      {
1313          // Handle Message input directly
1314          if ($input instanceof Message) {
1315              return $input;
1316          }
1317          // Handle single MessagePart
1318          if ($input instanceof MessagePart) {
1319              return new Message($defaultRole, [$input]);
1320          }
1321          // Handle string input
1322          if (is_string($input)) {
1323              if (trim($input) === '') {
1324                  throw new InvalidArgumentException('Cannot create a message from an empty string.');
1325              }
1326              return new Message($defaultRole, [new MessagePart($input)]);
1327          }
1328          // Handle array input
1329          if (!is_array($input)) {
1330              throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, ' . 'a list of string|MessagePart|MessagePartArrayShape, or a Message instance.');
1331          }
1332          // Handle MessageArrayShape input
1333          if (Message::isArrayShape($input)) {
1334              return Message::fromArray($input);
1335          }
1336          // Check if it's a MessagePartArrayShape
1337          if (MessagePart::isArrayShape($input)) {
1338              return new Message($defaultRole, [MessagePart::fromArray($input)]);
1339          }
1340          // It should be a list of string|MessagePart|MessagePartArrayShape
1341          if (!array_is_list($input)) {
1342              throw new InvalidArgumentException('Array input must be a list array.');
1343          }
1344          // Empty array check
1345          if (empty($input)) {
1346              throw new InvalidArgumentException('Cannot create a message from an empty array.');
1347          }
1348          $parts = [];
1349          foreach ($input as $item) {
1350              if (is_string($item)) {
1351                  $parts[] = new MessagePart($item);
1352              } elseif ($item instanceof MessagePart) {
1353                  $parts[] = $item;
1354              } elseif (is_array($item) && MessagePart::isArrayShape($item)) {
1355                  $parts[] = MessagePart::fromArray($item);
1356              } else {
1357                  throw new InvalidArgumentException('Array items must be strings, MessagePart instances, or MessagePartArrayShape.');
1358              }
1359          }
1360          return new Message($defaultRole, $parts);
1361      }
1362      /**
1363       * Validates the messages array for prompt generation.
1364       *
1365       * Ensures that:
1366       * - The first message is a user message
1367       * - The last message is a user message
1368       * - The last message has parts
1369       *
1370       * @since 0.1.0
1371       *
1372       * @return void
1373       * @throws InvalidArgumentException If validation fails.
1374       */
1375      private function validateMessages(): void
1376      {
1377          if (empty($this->messages)) {
1378              throw new InvalidArgumentException('Cannot generate from an empty prompt. Add content using withText() or similar methods.');
1379          }
1380          $firstMessage = reset($this->messages);
1381          if (!$firstMessage->getRole()->isUser()) {
1382              throw new InvalidArgumentException('The first message must be from a user role, not from ' . $firstMessage->getRole()->value);
1383          }
1384          $lastMessage = end($this->messages);
1385          if (!$lastMessage->getRole()->isUser()) {
1386              throw new InvalidArgumentException('The last message must be from a user role, not from ' . $lastMessage->getRole()->value);
1387          }
1388          if (empty($lastMessage->getParts())) {
1389              throw new InvalidArgumentException('The last message must have content parts. Add content using withText() or similar methods.');
1390          }
1391      }
1392      /**
1393       * Checks if the value is a list of Message objects.
1394       *
1395       * @since 0.1.0
1396       *
1397       * @param mixed $value The value to check.
1398       * @return bool True if the value is a list of Message objects.
1399       *
1400       * @phpstan-assert-if-true list<Message> $value
1401       */
1402      private function isMessagesList($value): bool
1403      {
1404          if (!is_array($value) || empty($value) || !array_is_list($value)) {
1405              return \false;
1406          }
1407          // Check if all items are Messages
1408          foreach ($value as $item) {
1409              if (!$item instanceof Message) {
1410                  return \false;
1411              }
1412          }
1413          return \true;
1414      }
1415      /**
1416       * Includes output modalities if not already present.
1417       *
1418       * Adds the given modalities to the output modalities list if they're not
1419       * already included. If output modalities is null, initializes it with
1420       * the given modalities.
1421       *
1422       * @since 0.1.0
1423       *
1424       * @param ModalityEnum ...$modalities The modalities to include.
1425       * @return void
1426       */
1427      private function includeOutputModalities(ModalityEnum ...$modalities): void
1428      {
1429          $existing = $this->modelConfig->getOutputModalities();
1430          // Initialize if null
1431          if ($existing === null) {
1432              $this->modelConfig->setOutputModalities($modalities);
1433              return;
1434          }
1435          // Build a set of existing modality values for O(1) lookup
1436          $existingValues = [];
1437          foreach ($existing as $existingModality) {
1438              $existingValues[$existingModality->value] = \true;
1439          }
1440          // Add new modalities that don't exist
1441          $toAdd = [];
1442          foreach ($modalities as $modality) {
1443              if (!isset($existingValues[$modality->value])) {
1444                  $toAdd[] = $modality;
1445              }
1446          }
1447          // Update if we have new modalities to add
1448          if (!empty($toAdd)) {
1449              $this->modelConfig->setOutputModalities(array_merge($existing, $toAdd));
1450          }
1451      }
1452      /**
1453       * Dispatches an event if an event dispatcher is registered.
1454       *
1455       * @since 0.4.0
1456       *
1457       * @param object $event The event to dispatch.
1458       * @return void
1459       */
1460      private function dispatchEvent(object $event): void
1461      {
1462          if ($this->eventDispatcher !== null) {
1463              $this->eventDispatcher->dispatch($event);
1464          }
1465      }
1466  }


Generated : Sat Jun 13 09:38:55 2026 Cross-referenced by PHPXref