| [ 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\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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |