| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Nov 1 08:20:35 2026 | Cross-referenced by PHPXref |