[ 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\Builders\Traits\ModelResolutionTrait;
   8  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   9  use WordPress\AiClient\Common\Exception\RuntimeException;
  10  use WordPress\AiClient\Events\AfterGenerateResultEvent;
  11  use WordPress\AiClient\Events\BeforeGenerateResultEvent;
  12  use WordPress\AiClient\Files\DTO\File;
  13  use WordPress\AiClient\Files\Enums\FileTypeEnum;
  14  use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
  15  use WordPress\AiClient\Files\Util\FileInputUtil;
  16  use WordPress\AiClient\Messages\DTO\Message;
  17  use WordPress\AiClient\Messages\DTO\MessagePart;
  18  use WordPress\AiClient\Messages\DTO\UserMessage;
  19  use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
  20  use WordPress\AiClient\Messages\Enums\ModalityEnum;
  21  use WordPress\AiClient\Providers\ModelResolver;
  22  use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
  23  use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
  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      use ModelResolutionTrait;
  53      /**
  54       * @var list<Message> The messages in the conversation.
  55       */
  56      protected array $messages = [];
  57      /**
  58       * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
  59       */
  60      private ?EventDispatcherInterface $eventDispatcher = null;
  61      // phpcs:disable Generic.Files.LineLength.TooLong
  62      /**
  63       * Constructor.
  64       *
  65       * @since 0.1.0
  66       *
  67       * @param ProviderRegistry $registry The provider registry for finding suitable models.
  68       * @param Prompt $prompt Optional initial prompt content.
  69       * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events.
  70       */
  71      // phpcs:enable Generic.Files.LineLength.TooLong
  72      public function __construct(ProviderRegistry $registry, $prompt = null, ?EventDispatcherInterface $eventDispatcher = null)
  73      {
  74          $this->modelConfig = new ModelConfig();
  75          $this->modelResolver = new ModelResolver($registry);
  76          $this->eventDispatcher = $eventDispatcher;
  77          if ($prompt === null) {
  78              return;
  79          }
  80          // Check if it's a list of Messages - set as messages
  81          if ($this->isMessagesList($prompt)) {
  82              $this->messages = $prompt;
  83              return;
  84          }
  85          // Parse it as a user message
  86          $userMessage = $this->parseMessage($prompt, MessageRoleEnum::user());
  87          $this->messages[] = $userMessage;
  88      }
  89      /**
  90       * Creates a deep clone of this builder.
  91       *
  92       * Clones all mutable state including messages, model configuration, and request options.
  93       * Service objects (registry, model, event dispatcher) are intentionally NOT cloned
  94       * as they are shared dependencies.
  95       *
  96       * @since 0.4.2
  97       */
  98      public function __clone()
  99      {
 100          // Deep clone messages array (Message has __clone)
 101          $clonedMessages = [];
 102          foreach ($this->messages as $message) {
 103              $clonedMessages[] = clone $message;
 104          }
 105          $this->messages = $clonedMessages;
 106          // Clone model config (ModelConfig has __clone)
 107          $this->modelConfig = clone $this->modelConfig;
 108          // Clone model resolver (ModelResolver has __clone; clones request options)
 109          $this->modelResolver = clone $this->modelResolver;
 110          // Note: $eventDispatcher is a service object and is intentionally NOT
 111          // cloned - it should be a shared reference.
 112      }
 113      /**
 114       * Adds text to the current message.
 115       *
 116       * @since 0.1.0
 117       *
 118       * @param string $text The text to add.
 119       * @return self
 120       */
 121      public function withText(string $text): self
 122      {
 123          $part = new MessagePart($text);
 124          $this->appendPartToMessages($part);
 125          return $this;
 126      }
 127      /**
 128       * Adds a file to the current message.
 129       *
 130       * Accepts:
 131       * - File object
 132       * - URL string (remote file)
 133       * - Base64-encoded data string
 134       * - Data URI string (data:mime/type;base64,data)
 135       * - Local file path string
 136       *
 137       * Resolving a local file path reads from the filesystem. Only pass strings you control; for untrusted
 138       * input, construct a File via File::fromUrl(), File::fromDataUri() or File::fromBase64() instead.
 139       *
 140       * @since 0.1.0
 141       *
 142       * @param string|File $file The file (File object or string representation).
 143       * @param string|null $mimeType The MIME type (optional, ignored if File object provided).
 144       * @return self
 145       * @throws InvalidArgumentException If the file is invalid or MIME type cannot be determined.
 146       */
 147      public function withFile($file, ?string $mimeType = null): self
 148      {
 149          $file = $file instanceof File ? $file : FileInputUtil::fromString($file, $mimeType);
 150          $part = new MessagePart($file);
 151          $this->appendPartToMessages($part);
 152          return $this;
 153      }
 154      /**
 155       * Adds a function response to the current message.
 156       *
 157       * @since 0.1.0
 158       *
 159       * @param FunctionResponse $functionResponse The function response.
 160       * @return self
 161       */
 162      public function withFunctionResponse(FunctionResponse $functionResponse): self
 163      {
 164          $part = new MessagePart($functionResponse);
 165          $this->appendPartToMessages($part);
 166          return $this;
 167      }
 168      /**
 169       * Adds message parts to the current message.
 170       *
 171       * @since 0.1.0
 172       *
 173       * @param MessagePart ...$parts The message parts to add.
 174       * @return self
 175       */
 176      public function withMessageParts(MessagePart ...$parts): self
 177      {
 178          foreach ($parts as $part) {
 179              $this->appendPartToMessages($part);
 180          }
 181          return $this;
 182      }
 183      /**
 184       * Adds conversation history messages.
 185       *
 186       * Historical messages are prepended to the beginning of the message list,
 187       * before the current message being built.
 188       *
 189       * @since 0.1.0
 190       *
 191       * @param Message ...$messages The messages to add to history.
 192       * @return self
 193       */
 194      public function withHistory(Message ...$messages): self
 195      {
 196          // Prepend the history messages to the beginning of the messages array
 197          $this->messages = array_merge($messages, $this->messages);
 198          return $this;
 199      }
 200      /**
 201       * Sets the system instruction.
 202       *
 203       * System instructions are stored in the model configuration and guide
 204       * the AI model's behavior throughout the conversation.
 205       *
 206       * @since 0.1.0
 207       *
 208       * @param string $systemInstruction The system instruction text.
 209       * @return self
 210       */
 211      public function usingSystemInstruction(string $systemInstruction): self
 212      {
 213          $this->modelConfig->setSystemInstruction($systemInstruction);
 214          return $this;
 215      }
 216      /**
 217       * Sets the maximum number of tokens to generate.
 218       *
 219       * @since 0.1.0
 220       *
 221       * @param int $maxTokens The maximum number of tokens.
 222       * @return self
 223       */
 224      public function usingMaxTokens(int $maxTokens): self
 225      {
 226          $this->modelConfig->setMaxTokens($maxTokens);
 227          return $this;
 228      }
 229      /**
 230       * Sets the temperature for generation.
 231       *
 232       * @since 0.1.0
 233       *
 234       * @param float $temperature The temperature value.
 235       * @return self
 236       */
 237      public function usingTemperature(float $temperature): self
 238      {
 239          $this->modelConfig->setTemperature($temperature);
 240          return $this;
 241      }
 242      /**
 243       * Sets the top-p value for generation.
 244       *
 245       * @since 0.1.0
 246       *
 247       * @param float $topP The top-p value.
 248       * @return self
 249       */
 250      public function usingTopP(float $topP): self
 251      {
 252          $this->modelConfig->setTopP($topP);
 253          return $this;
 254      }
 255      /**
 256       * Sets the top-k value for generation.
 257       *
 258       * @since 0.1.0
 259       *
 260       * @param int $topK The top-k value.
 261       * @return self
 262       */
 263      public function usingTopK(int $topK): self
 264      {
 265          $this->modelConfig->setTopK($topK);
 266          return $this;
 267      }
 268      /**
 269       * Sets stop sequences for generation.
 270       *
 271       * @since 0.1.0
 272       *
 273       * @param string ...$stopSequences The stop sequences.
 274       * @return self
 275       */
 276      public function usingStopSequences(string ...$stopSequences): self
 277      {
 278          $this->modelConfig->setStopSequences($stopSequences);
 279          return $this;
 280      }
 281      /**
 282       * Sets the number of candidates to generate.
 283       *
 284       * @since 0.1.0
 285       *
 286       * @param int $candidateCount The number of candidates.
 287       * @return self
 288       */
 289      public function usingCandidateCount(int $candidateCount): self
 290      {
 291          $this->modelConfig->setCandidateCount($candidateCount);
 292          return $this;
 293      }
 294      /**
 295       * Sets the function declarations available to the model.
 296       *
 297       * @since 0.1.0
 298       *
 299       * @param FunctionDeclaration ...$functionDeclarations The function declarations.
 300       * @return self
 301       */
 302      public function usingFunctionDeclarations(FunctionDeclaration ...$functionDeclarations): self
 303      {
 304          $this->modelConfig->setFunctionDeclarations($functionDeclarations);
 305          return $this;
 306      }
 307      /**
 308       * Sets the presence penalty for generation.
 309       *
 310       * @since 0.1.0
 311       *
 312       * @param float $presencePenalty The presence penalty value.
 313       * @return self
 314       */
 315      public function usingPresencePenalty(float $presencePenalty): self
 316      {
 317          $this->modelConfig->setPresencePenalty($presencePenalty);
 318          return $this;
 319      }
 320      /**
 321       * Sets the frequency penalty for generation.
 322       *
 323       * @since 0.1.0
 324       *
 325       * @param float $frequencyPenalty The frequency penalty value.
 326       * @return self
 327       */
 328      public function usingFrequencyPenalty(float $frequencyPenalty): self
 329      {
 330          $this->modelConfig->setFrequencyPenalty($frequencyPenalty);
 331          return $this;
 332      }
 333      /**
 334       * Sets the web search configuration.
 335       *
 336       * @since 0.1.0
 337       *
 338       * @param WebSearch $webSearch The web search configuration.
 339       * @return self
 340       */
 341      public function usingWebSearch(WebSearch $webSearch): self
 342      {
 343          $this->modelConfig->setWebSearch($webSearch);
 344          return $this;
 345      }
 346      /**
 347       * Sets the top log probabilities configuration.
 348       *
 349       * If $topLogprobs is null, enables log probabilities.
 350       * If $topLogprobs has a value, enables log probabilities and sets the number of top log probabilities to return.
 351       *
 352       * @since 0.1.0
 353       *
 354       * @param int|null $topLogprobs The number of top log probabilities to return, or null to enable log probabilities.
 355       * @return self
 356       */
 357      public function usingTopLogprobs(?int $topLogprobs = null): self
 358      {
 359          // Always enable log probabilities
 360          $this->modelConfig->setLogprobs(\true);
 361          // If a specific number is provided, set it
 362          if ($topLogprobs !== null) {
 363              $this->modelConfig->setTopLogprobs($topLogprobs);
 364          }
 365          return $this;
 366      }
 367      /**
 368       * Sets the output MIME type.
 369       *
 370       * @since 0.1.0
 371       *
 372       * @param string $mimeType The MIME type.
 373       * @return self
 374       */
 375      public function asOutputMimeType(string $mimeType): self
 376      {
 377          $this->modelConfig->setOutputMimeType($mimeType);
 378          return $this;
 379      }
 380      /**
 381       * Sets the output schema.
 382       *
 383       * @since 0.1.0
 384       *
 385       * @param array<string, mixed> $schema The output schema.
 386       * @return self
 387       */
 388      public function asOutputSchema(array $schema): self
 389      {
 390          $this->modelConfig->setOutputSchema($schema);
 391          return $this;
 392      }
 393      /**
 394       * Sets the output modalities.
 395       *
 396       * @since 0.1.0
 397       *
 398       * @param ModalityEnum ...$modalities The output modalities.
 399       * @return self
 400       */
 401      public function asOutputModalities(ModalityEnum ...$modalities): self
 402      {
 403          $this->modelConfig->setOutputModalities($modalities);
 404          return $this;
 405      }
 406      /**
 407       * Sets the output file type.
 408       *
 409       * @since 0.1.0
 410       *
 411       * @param FileTypeEnum $fileType The output file type.
 412       * @return self
 413       */
 414      public function asOutputFileType(FileTypeEnum $fileType): self
 415      {
 416          $this->modelConfig->setOutputFileType($fileType);
 417          return $this;
 418      }
 419      /**
 420       * Sets the output media orientation.
 421       *
 422       * @since 1.3.0
 423       *
 424       * @param MediaOrientationEnum $orientation The output media orientation.
 425       * @return self
 426       */
 427      public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self
 428      {
 429          $this->modelConfig->setOutputMediaOrientation($orientation);
 430          return $this;
 431      }
 432      /**
 433       * Sets the output media aspect ratio.
 434       *
 435       * If set, this supersedes the output media orientation, as it is a more
 436       * specific configuration.
 437       *
 438       * @since 1.3.0
 439       *
 440       * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2").
 441       * @return self
 442       */
 443      public function asOutputMediaAspectRatio(string $aspectRatio): self
 444      {
 445          $this->modelConfig->setOutputMediaAspectRatio($aspectRatio);
 446          return $this;
 447      }
 448      /**
 449       * Sets the output speech voice.
 450       *
 451       * @since 1.3.0
 452       *
 453       * @param string $voice The output speech voice.
 454       * @return self
 455       */
 456      public function asOutputSpeechVoice(string $voice): self
 457      {
 458          $this->modelConfig->setOutputSpeechVoice($voice);
 459          return $this;
 460      }
 461      /**
 462       * Configures the prompt for JSON response output.
 463       *
 464       * @since 0.1.0
 465       *
 466       * @param array<string, mixed>|null $schema Optional JSON schema.
 467       * @return self
 468       */
 469      public function asJsonResponse(?array $schema = null): self
 470      {
 471          $this->asOutputMimeType('application/json');
 472          if ($schema !== null) {
 473              $this->asOutputSchema($schema);
 474          }
 475          return $this;
 476      }
 477      /**
 478       * Infers the capability from configured output modalities.
 479       *
 480       * @since 0.1.0
 481       *
 482       * @return CapabilityEnum The inferred capability.
 483       * @throws RuntimeException If the output modality is not supported.
 484       */
 485      private function inferCapabilityFromOutputModalities(): CapabilityEnum
 486      {
 487          // Get the configured output modalities
 488          $outputModalities = $this->modelConfig->getOutputModalities();
 489          // Default to text if no output modality is specified
 490          if ($outputModalities === null || empty($outputModalities)) {
 491              return CapabilityEnum::textGeneration();
 492          }
 493          // Multi-modal output (multiple modalities) defaults to text generation. This is temporary
 494          // as a multi-modal interface will be implemented in the future.
 495          if (count($outputModalities) > 1) {
 496              return CapabilityEnum::textGeneration();
 497          }
 498          // Infer capability from single output modality
 499          $outputModality = $outputModalities[0];
 500          if ($outputModality->isText()) {
 501              return CapabilityEnum::textGeneration();
 502          } elseif ($outputModality->isImage()) {
 503              return CapabilityEnum::imageGeneration();
 504          } elseif ($outputModality->isAudio()) {
 505              return CapabilityEnum::speechGeneration();
 506          } elseif ($outputModality->isVideo()) {
 507              return CapabilityEnum::videoGeneration();
 508          } else {
 509              // For unsupported modalities, provide a clear error message
 510              throw new RuntimeException(sprintf('Output modality "%s" is not yet supported.', $outputModality->value));
 511          }
 512      }
 513      /**
 514       * Infers the capability from a model's implemented interfaces.
 515       *
 516       * @since 0.1.0
 517       *
 518       * @param ModelInterface $model The model to infer capability from.
 519       * @return CapabilityEnum|null The inferred capability, or null if none can be inferred.
 520       */
 521      private function inferCapabilityFromModelInterfaces(ModelInterface $model): ?CapabilityEnum
 522      {
 523          // Check model interfaces in order of preference
 524          if ($model instanceof TextGenerationModelInterface) {
 525              return CapabilityEnum::textGeneration();
 526          }
 527          if ($model instanceof ImageGenerationModelInterface) {
 528              return CapabilityEnum::imageGeneration();
 529          }
 530          if ($model instanceof TextToSpeechConversionModelInterface) {
 531              return CapabilityEnum::textToSpeechConversion();
 532          }
 533          if ($model instanceof SpeechGenerationModelInterface) {
 534              return CapabilityEnum::speechGeneration();
 535          }
 536          if ($model instanceof VideoGenerationModelInterface) {
 537              return CapabilityEnum::videoGeneration();
 538          }
 539          // No supported interface found
 540          return null;
 541      }
 542      /**
 543       * Checks if the current prompt is supported by the selected model.
 544       *
 545       * @since 0.1.0
 546       * @since 0.3.0 Method visibility changed to public.
 547       *
 548       * @param CapabilityEnum|null $capability Optional capability to check support for.
 549       * @return bool True if supported, false otherwise.
 550       */
 551      public function isSupported(?CapabilityEnum $capability = null): bool
 552      {
 553          // If no intended capability provided, infer from output modalities
 554          if ($capability === null) {
 555              // First try to infer from a specific model if one is set
 556              $model = $this->modelResolver->getModel();
 557              if ($model !== null) {
 558                  $inferredCapability = $this->inferCapabilityFromModelInterfaces($model);
 559                  if ($inferredCapability !== null) {
 560                      $capability = $inferredCapability;
 561                  }
 562              }
 563              // If still no capability, infer from output modalities
 564              if ($capability === null) {
 565                  $capability = $this->inferCapabilityFromOutputModalities();
 566              }
 567          }
 568          // Build requirements with the specified capability
 569          $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
 570          return $this->modelResolver->isSupported($requirements);
 571      }
 572      /**
 573       * Checks if the prompt is supported for text generation.
 574       *
 575       * @since 0.1.0
 576       *
 577       * @return bool True if text generation is supported.
 578       */
 579      public function isSupportedForTextGeneration(): bool
 580      {
 581          return $this->isSupported(CapabilityEnum::textGeneration());
 582      }
 583      /**
 584       * Checks if the prompt is supported for image generation.
 585       *
 586       * @since 0.1.0
 587       *
 588       * @return bool True if image generation is supported.
 589       */
 590      public function isSupportedForImageGeneration(): bool
 591      {
 592          return $this->isSupported(CapabilityEnum::imageGeneration());
 593      }
 594      /**
 595       * Checks if the prompt is supported for text to speech conversion.
 596       *
 597       * @since 0.1.0
 598       *
 599       * @return bool True if text to speech conversion is supported.
 600       */
 601      public function isSupportedForTextToSpeechConversion(): bool
 602      {
 603          return $this->isSupported(CapabilityEnum::textToSpeechConversion());
 604      }
 605      /**
 606       * Checks if the prompt is supported for video generation.
 607       *
 608       * @since 0.1.0
 609       *
 610       * @return bool True if video generation is supported.
 611       */
 612      public function isSupportedForVideoGeneration(): bool
 613      {
 614          return $this->isSupported(CapabilityEnum::videoGeneration());
 615      }
 616      /**
 617       * Checks if the prompt is supported for speech generation.
 618       *
 619       * @since 0.1.0
 620       *
 621       * @return bool True if speech generation is supported.
 622       */
 623      public function isSupportedForSpeechGeneration(): bool
 624      {
 625          return $this->isSupported(CapabilityEnum::speechGeneration());
 626      }
 627      /**
 628       * Checks if the prompt is supported for music generation.
 629       *
 630       * @since 0.1.0
 631       *
 632       * @return bool True if music generation is supported.
 633       */
 634      public function isSupportedForMusicGeneration(): bool
 635      {
 636          return $this->isSupported(CapabilityEnum::musicGeneration());
 637      }
 638      /**
 639       * Checks if the prompt is supported for embedding generation.
 640       *
 641       * @since 0.1.0
 642       *
 643       * @return bool True if embedding generation is supported.
 644       */
 645      public function isSupportedForEmbeddingGeneration(): bool
 646      {
 647          return $this->isSupported(CapabilityEnum::embeddingGeneration());
 648      }
 649      /**
 650       * Generates a result from the prompt.
 651       *
 652       * This is the primary execution method that generates a result (containing
 653       * potentially multiple candidates) based on the specified capability or
 654       * the configured output modality.
 655       *
 656       * @since 0.1.0
 657       *
 658       * @param CapabilityEnum|null $capability Optional capability to use for generation.
 659       *                                        If null, capability is inferred from output modality.
 660       * @return GenerativeAiResult The generated result containing candidates.
 661       * @throws InvalidArgumentException If the prompt or model validation fails.
 662       * @throws RuntimeException If the model doesn't support the required capability.
 663       */
 664      public function generateResult(?CapabilityEnum $capability = null): GenerativeAiResult
 665      {
 666          $this->validateMessages();
 667          // If capability is not provided, infer it
 668          if ($capability === null) {
 669              // First try to infer from a specific model if one is set
 670              $setModel = $this->modelResolver->getModel();
 671              if ($setModel !== null) {
 672                  $inferredCapability = $this->inferCapabilityFromModelInterfaces($setModel);
 673                  if ($inferredCapability !== null) {
 674                      $capability = $inferredCapability;
 675                  }
 676              }
 677              // If still no capability, infer from output modalities
 678              if ($capability === null) {
 679                  $capability = $this->inferCapabilityFromOutputModalities();
 680              }
 681          }
 682          $model = $this->getConfiguredModel($capability);
 683          // Dispatch BeforeGenerateResultEvent
 684          $this->dispatchEvent(new BeforeGenerateResultEvent($this->messages, $model, $capability));
 685          // Route to the appropriate generation method based on capability
 686          $result = $this->executeModelGeneration($model, $capability, $this->messages);
 687          // Dispatch AfterGenerateResultEvent
 688          $this->dispatchEvent(new AfterGenerateResultEvent($this->messages, $model, $capability, $result));
 689          return $result;
 690      }
 691      /**
 692       * Executes the model generation based on capability.
 693       *
 694       * @since 0.4.0
 695       *
 696       * @param ModelInterface $model The model to use for generation.
 697       * @param CapabilityEnum $capability The capability to use.
 698       * @param list<Message> $messages The messages to send.
 699       * @return GenerativeAiResult The generated result.
 700       * @throws RuntimeException If the model doesn't support the required capability.
 701       */
 702      private function executeModelGeneration(ModelInterface $model, CapabilityEnum $capability, array $messages): GenerativeAiResult
 703      {
 704          if ($capability->isTextGeneration()) {
 705              if (!$model instanceof TextGenerationModelInterface) {
 706                  throw new RuntimeException(sprintf('Model "%s" does not support text generation.', $model->metadata()->getId()));
 707              }
 708              return $model->generateTextResult($messages);
 709          }
 710          if ($capability->isImageGeneration()) {
 711              if (!$model instanceof ImageGenerationModelInterface) {
 712                  throw new RuntimeException(sprintf('Model "%s" does not support image generation.', $model->metadata()->getId()));
 713              }
 714              return $model->generateImageResult($messages);
 715          }
 716          if ($capability->isTextToSpeechConversion()) {
 717              if (!$model instanceof TextToSpeechConversionModelInterface) {
 718                  throw new RuntimeException(sprintf('Model "%s" does not support text-to-speech conversion.', $model->metadata()->getId()));
 719              }
 720              return $model->convertTextToSpeechResult($messages);
 721          }
 722          if ($capability->isSpeechGeneration()) {
 723              if (!$model instanceof SpeechGenerationModelInterface) {
 724                  throw new RuntimeException(sprintf('Model "%s" does not support speech generation.', $model->metadata()->getId()));
 725              }
 726              return $model->generateSpeechResult($messages);
 727          }
 728          if ($capability->isVideoGeneration()) {
 729              if (!$model instanceof VideoGenerationModelInterface) {
 730                  throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId()));
 731              }
 732              return $model->generateVideoResult($messages);
 733          }
 734          // TODO: Add support for other capabilities when interfaces are available
 735          throw new RuntimeException(sprintf('Capability "%s" is not yet supported for generation.', $capability->value));
 736      }
 737      /**
 738       * Generates a text result from the prompt.
 739       *
 740       * @since 0.1.0
 741       *
 742       * @return GenerativeAiResult The generated result containing text candidates.
 743       * @throws InvalidArgumentException If the prompt or model validation fails.
 744       * @throws RuntimeException If the model doesn't support text generation.
 745       */
 746      public function generateTextResult(): GenerativeAiResult
 747      {
 748          // Include text in output modalities
 749          $this->includeOutputModalities(ModalityEnum::text());
 750          // Generate and return the result with text generation capability
 751          return $this->generateResult(CapabilityEnum::textGeneration());
 752      }
 753      /**
 754       * Generates an image result from the prompt.
 755       *
 756       * @since 0.1.0
 757       *
 758       * @return GenerativeAiResult The generated result containing image candidates.
 759       * @throws InvalidArgumentException If the prompt or model validation fails.
 760       * @throws RuntimeException If the model doesn't support image generation.
 761       */
 762      public function generateImageResult(): GenerativeAiResult
 763      {
 764          // Include image in output modalities
 765          $this->includeOutputModalities(ModalityEnum::image());
 766          // Generate and return the result with image generation capability
 767          return $this->generateResult(CapabilityEnum::imageGeneration());
 768      }
 769      /**
 770       * Generates a speech result from the prompt.
 771       *
 772       * @since 0.1.0
 773       *
 774       * @return GenerativeAiResult The generated result containing speech audio candidates.
 775       * @throws InvalidArgumentException If the prompt or model validation fails.
 776       * @throws RuntimeException If the model doesn't support speech generation.
 777       */
 778      public function generateSpeechResult(): GenerativeAiResult
 779      {
 780          // Include audio in output modalities
 781          $this->includeOutputModalities(ModalityEnum::audio());
 782          // Generate and return the result with speech generation capability
 783          return $this->generateResult(CapabilityEnum::speechGeneration());
 784      }
 785      /**
 786       * Converts text to speech and returns the result.
 787       *
 788       * @since 0.1.0
 789       *
 790       * @return GenerativeAiResult The generated result containing speech audio candidates.
 791       * @throws InvalidArgumentException If the prompt or model validation fails.
 792       * @throws RuntimeException If the model doesn't support text-to-speech conversion.
 793       */
 794      public function convertTextToSpeechResult(): GenerativeAiResult
 795      {
 796          // Include audio in output modalities
 797          $this->includeOutputModalities(ModalityEnum::audio());
 798          // Generate and return the result with text-to-speech conversion capability
 799          return $this->generateResult(CapabilityEnum::textToSpeechConversion());
 800      }
 801      /**
 802       * Generates a video result from the prompt.
 803       *
 804       * @since 1.3.0
 805       *
 806       * @return GenerativeAiResult The generated result containing video candidates.
 807       * @throws InvalidArgumentException If the prompt or model validation fails.
 808       * @throws RuntimeException If the model doesn't support video generation.
 809       */
 810      public function generateVideoResult(): GenerativeAiResult
 811      {
 812          // Include video in output modalities
 813          $this->includeOutputModalities(ModalityEnum::video());
 814          // Generate and return the result with video generation capability
 815          return $this->generateResult(CapabilityEnum::videoGeneration());
 816      }
 817      /**
 818       * Generates text from the prompt.
 819       *
 820       * @since 0.1.0
 821       *
 822       * @return string The generated text.
 823       * @throws InvalidArgumentException If the prompt or model validation fails.
 824       */
 825      public function generateText(): string
 826      {
 827          return $this->generateTextResult()->toText();
 828      }
 829      /**
 830       * Generates multiple text candidates from the prompt.
 831       *
 832       * @since 0.1.0
 833       *
 834       * @param int|null $candidateCount The number of candidates to generate.
 835       * @return list<string> The generated texts.
 836       * @throws InvalidArgumentException If the prompt or model validation fails.
 837       */
 838      public function generateTexts(?int $candidateCount = null): array
 839      {
 840          if ($candidateCount !== null) {
 841              $this->usingCandidateCount($candidateCount);
 842          }
 843          // Generate text result
 844          return $this->generateTextResult()->toTexts();
 845      }
 846      /**
 847       * Generates an image from the prompt.
 848       *
 849       * @since 0.1.0
 850       *
 851       * @return File The generated image file.
 852       * @throws InvalidArgumentException If the prompt or model validation fails.
 853       * @throws RuntimeException If no image is generated.
 854       */
 855      public function generateImage(): File
 856      {
 857          return $this->generateImageResult()->toFile();
 858      }
 859      /**
 860       * Generates multiple images from the prompt.
 861       *
 862       * @since 0.1.0
 863       *
 864       * @param int|null $candidateCount The number of images to generate.
 865       * @return list<File> The generated image files.
 866       * @throws InvalidArgumentException If the prompt or model validation fails.
 867       * @throws RuntimeException If no images are generated.
 868       */
 869      public function generateImages(?int $candidateCount = null): array
 870      {
 871          if ($candidateCount !== null) {
 872              $this->usingCandidateCount($candidateCount);
 873          }
 874          return $this->generateImageResult()->toFiles();
 875      }
 876      /**
 877       * Converts text to speech.
 878       *
 879       * @since 0.1.0
 880       *
 881       * @return File The generated speech audio file.
 882       * @throws InvalidArgumentException If the prompt or model validation fails.
 883       * @throws RuntimeException If no audio is generated.
 884       */
 885      public function convertTextToSpeech(): File
 886      {
 887          return $this->convertTextToSpeechResult()->toFile();
 888      }
 889      /**
 890       * Converts text to multiple speech outputs.
 891       *
 892       * @since 0.1.0
 893       *
 894       * @param int|null $candidateCount The number of speech outputs to generate.
 895       * @return list<File> The generated speech audio files.
 896       * @throws InvalidArgumentException If the prompt or model validation fails.
 897       * @throws RuntimeException If no audio is generated.
 898       */
 899      public function convertTextToSpeeches(?int $candidateCount = null): array
 900      {
 901          if ($candidateCount !== null) {
 902              $this->usingCandidateCount($candidateCount);
 903          }
 904          return $this->convertTextToSpeechResult()->toFiles();
 905      }
 906      /**
 907       * Generates speech from the prompt.
 908       *
 909       * @since 0.1.0
 910       *
 911       * @return File The generated speech audio file.
 912       * @throws InvalidArgumentException If the prompt or model validation fails.
 913       * @throws RuntimeException If no audio is generated.
 914       */
 915      public function generateSpeech(): File
 916      {
 917          return $this->generateSpeechResult()->toFile();
 918      }
 919      /**
 920       * Generates multiple speech outputs from the prompt.
 921       *
 922       * @since 0.1.0
 923       *
 924       * @param int|null $candidateCount The number of speech outputs to generate.
 925       * @return list<File> The generated speech audio files.
 926       * @throws InvalidArgumentException If the prompt or model validation fails.
 927       * @throws RuntimeException If no audio is generated.
 928       */
 929      public function generateSpeeches(?int $candidateCount = null): array
 930      {
 931          if ($candidateCount !== null) {
 932              $this->usingCandidateCount($candidateCount);
 933          }
 934          return $this->generateSpeechResult()->toFiles();
 935      }
 936      /**
 937       * Generates a video from the prompt.
 938       *
 939       * @since 1.3.0
 940       *
 941       * @return File The generated video file.
 942       * @throws InvalidArgumentException If the prompt or model validation fails.
 943       * @throws RuntimeException If no video is generated.
 944       */
 945      public function generateVideo(): File
 946      {
 947          return $this->generateVideoResult()->toFile();
 948      }
 949      /**
 950       * Generates multiple videos from the prompt.
 951       *
 952       * @since 1.3.0
 953       *
 954       * @param int|null $candidateCount The number of videos to generate.
 955       * @return list<File> The generated video files.
 956       * @throws InvalidArgumentException If the prompt or model validation fails.
 957       * @throws RuntimeException If no videos are generated.
 958       */
 959      public function generateVideos(?int $candidateCount = null): array
 960      {
 961          if ($candidateCount !== null) {
 962              $this->usingCandidateCount($candidateCount);
 963          }
 964          return $this->generateVideoResult()->toFiles();
 965      }
 966      /**
 967       * Appends a MessagePart to the messages array.
 968       *
 969       * If the last message has a user role, the part is added to it.
 970       * Otherwise, a new UserMessage is created with the part.
 971       *
 972       * @since 0.1.0
 973       *
 974       * @param MessagePart $part The part to append.
 975       * @return void
 976       */
 977      protected function appendPartToMessages(MessagePart $part): void
 978      {
 979          $lastMessage = end($this->messages);
 980          if ($lastMessage instanceof Message && $lastMessage->getRole()->isUser()) {
 981              // Replace the last message with a new one containing the appended part
 982              array_pop($this->messages);
 983              $this->messages[] = $lastMessage->withPart($part);
 984              return;
 985          }
 986          // Create new UserMessage with the part
 987          $this->messages[] = new UserMessage([$part]);
 988      }
 989      /**
 990       * Gets the model to use for generation.
 991       *
 992       * If a model has been explicitly set, validates it meets requirements and returns it.
 993       * Otherwise, finds a suitable model based on the prompt requirements.
 994       *
 995       * @since 0.1.0
 996       *
 997       * @param CapabilityEnum $capability The capability the model will be using.
 998       * @return ModelInterface The model to use.
 999       * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements.
1000       */
1001      private function getConfiguredModel(CapabilityEnum $capability): ModelInterface
1002      {
1003          $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
1004          return $this->modelResolver->resolve($requirements, $this->modelConfig, 'prompt');
1005      }
1006      /**
1007       * Parses various input types into a Message with the given role.
1008       *
1009       * @since 0.1.0
1010       *
1011       * @param mixed $input The input to parse.
1012       * @param MessageRoleEnum $defaultRole The role for the message if not specified by input.
1013       * @return Message The parsed message.
1014       * @throws InvalidArgumentException If the input type is not supported or results in empty message.
1015       */
1016      private function parseMessage($input, MessageRoleEnum $defaultRole): Message
1017      {
1018          // Handle Message input directly
1019          if ($input instanceof Message) {
1020              return $input;
1021          }
1022          // Handle single MessagePart
1023          if ($input instanceof MessagePart) {
1024              return new Message($defaultRole, [$input]);
1025          }
1026          // Handle string input
1027          if (is_string($input)) {
1028              if (trim($input) === '') {
1029                  throw new InvalidArgumentException('Cannot create a message from an empty string.');
1030              }
1031              return new Message($defaultRole, [new MessagePart($input)]);
1032          }
1033          // Handle array input
1034          if (!is_array($input)) {
1035              throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, ' . 'a list of string|MessagePart|MessagePartArrayShape, or a Message instance.');
1036          }
1037          // Handle MessageArrayShape input
1038          if (Message::isArrayShape($input)) {
1039              return Message::fromArray($input);
1040          }
1041          // Check if it's a MessagePartArrayShape
1042          if (MessagePart::isArrayShape($input)) {
1043              return new Message($defaultRole, [MessagePart::fromArray($input)]);
1044          }
1045          // It should be a list of string|MessagePart|MessagePartArrayShape
1046          if (!array_is_list($input)) {
1047              throw new InvalidArgumentException('Array input must be a list array.');
1048          }
1049          // Empty array check
1050          if (empty($input)) {
1051              throw new InvalidArgumentException('Cannot create a message from an empty array.');
1052          }
1053          $parts = [];
1054          foreach ($input as $item) {
1055              if (is_string($item)) {
1056                  $parts[] = new MessagePart($item);
1057              } elseif ($item instanceof MessagePart) {
1058                  $parts[] = $item;
1059              } elseif (is_array($item) && MessagePart::isArrayShape($item)) {
1060                  $parts[] = MessagePart::fromArray($item);
1061              } else {
1062                  throw new InvalidArgumentException('Array items must be strings, MessagePart instances, or MessagePartArrayShape.');
1063              }
1064          }
1065          return new Message($defaultRole, $parts);
1066      }
1067      /**
1068       * Validates the messages array for prompt generation.
1069       *
1070       * Ensures that:
1071       * - The first message is a user message
1072       * - The last message is a user message
1073       * - The last message has parts
1074       *
1075       * @since 0.1.0
1076       *
1077       * @return void
1078       * @throws InvalidArgumentException If validation fails.
1079       */
1080      private function validateMessages(): void
1081      {
1082          // Work on a copy so reset() and end() do not change the property array's internal pointer.
1083          $messages = $this->messages;
1084          if (empty($messages)) {
1085              throw new InvalidArgumentException('Cannot generate from an empty prompt. Add content using withText() or similar methods.');
1086          }
1087          $firstMessage = reset($messages);
1088          if (!$firstMessage->getRole()->isUser()) {
1089              throw new InvalidArgumentException('The first message must be from a user role, not from ' . $firstMessage->getRole()->value);
1090          }
1091          $lastMessage = end($messages);
1092          if (!$lastMessage->getRole()->isUser()) {
1093              throw new InvalidArgumentException('The last message must be from a user role, not from ' . $lastMessage->getRole()->value);
1094          }
1095          if (empty($lastMessage->getParts())) {
1096              throw new InvalidArgumentException('The last message must have content parts. Add content using withText() or similar methods.');
1097          }
1098      }
1099      /**
1100       * Checks if the value is a list of Message objects.
1101       *
1102       * @since 0.1.0
1103       *
1104       * @param mixed $value The value to check.
1105       * @return bool True if the value is a list of Message objects.
1106       *
1107       * @phpstan-assert-if-true list<Message> $value
1108       */
1109      private function isMessagesList($value): bool
1110      {
1111          if (!is_array($value) || empty($value) || !array_is_list($value)) {
1112              return \false;
1113          }
1114          // Check if all items are Messages
1115          foreach ($value as $item) {
1116              if (!$item instanceof Message) {
1117                  return \false;
1118              }
1119          }
1120          return \true;
1121      }
1122      /**
1123       * Includes output modalities if not already present.
1124       *
1125       * Adds the given modalities to the output modalities list if they're not
1126       * already included. If output modalities is null, initializes it with
1127       * the given modalities.
1128       *
1129       * @since 0.1.0
1130       *
1131       * @param ModalityEnum ...$modalities The modalities to include.
1132       * @return void
1133       */
1134      private function includeOutputModalities(ModalityEnum ...$modalities): void
1135      {
1136          $existing = $this->modelConfig->getOutputModalities();
1137          // Initialize if null
1138          if ($existing === null) {
1139              $this->modelConfig->setOutputModalities($modalities);
1140              return;
1141          }
1142          // Build a set of existing modality values for O(1) lookup
1143          $existingValues = [];
1144          foreach ($existing as $existingModality) {
1145              $existingValues[$existingModality->value] = \true;
1146          }
1147          // Add new modalities that don't exist
1148          $toAdd = [];
1149          foreach ($modalities as $modality) {
1150              if (!isset($existingValues[$modality->value])) {
1151                  $toAdd[] = $modality;
1152              }
1153          }
1154          // Update if we have new modalities to add
1155          if (!empty($toAdd)) {
1156              $this->modelConfig->setOutputModalities(array_merge($existing, $toAdd));
1157          }
1158      }
1159      /**
1160       * Dispatches an event if an event dispatcher is registered.
1161       *
1162       * @since 0.4.0
1163       *
1164       * @param object $event The event to dispatch.
1165       * @return void
1166       */
1167      private function dispatchEvent(object $event): void
1168      {
1169          if ($this->eventDispatcher !== null) {
1170              $this->eventDispatcher->dispatch($event);
1171          }
1172      }
1173  }


Generated : Sun Nov 1 08:20:35 2026 Cross-referenced by PHPXref