| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WP AI Client: WP_AI_Client_Prompt_Builder class 4 * 5 * @package WordPress 6 * @subpackage AI 7 * @since 7.0.0 8 */ 9 10 use WordPress\AiClient\AiClient; 11 use WordPress\AiClient\Builders\PromptBuilder; 12 use WordPress\AiClient\Common\Exception\InvalidArgumentException; 13 use WordPress\AiClient\Common\Exception\TokenLimitReachedException; 14 use WordPress\AiClient\Files\DTO\File; 15 use WordPress\AiClient\Files\Enums\FileTypeEnum; 16 use WordPress\AiClient\Files\Enums\MediaOrientationEnum; 17 use WordPress\AiClient\Messages\DTO\Message; 18 use WordPress\AiClient\Messages\DTO\MessagePart; 19 use WordPress\AiClient\Messages\Enums\ModalityEnum; 20 use WordPress\AiClient\Providers\Http\DTO\RequestOptions; 21 use WordPress\AiClient\Providers\Http\Exception\ClientException; 22 use WordPress\AiClient\Providers\Http\Exception\NetworkException; 23 use WordPress\AiClient\Providers\Http\Exception\ServerException; 24 use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; 25 use WordPress\AiClient\Providers\Models\DTO\ModelConfig; 26 use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; 27 use WordPress\AiClient\Providers\ProviderRegistry; 28 use WordPress\AiClient\Results\DTO\GenerativeAiResult; 29 use WordPress\AiClient\Tools\DTO\FunctionDeclaration; 30 use WordPress\AiClient\Tools\DTO\FunctionResponse; 31 use WordPress\AiClient\Tools\DTO\WebSearch; 32 33 /** 34 * Fluent builder for constructing AI prompts, returning WP_Error on failure. 35 * 36 * This class provides a fluent interface for building prompts with various 37 * content types and model configurations. It wraps the PHP AI Client SDK's 38 * PromptBuilder and adds WordPress-specific behavior including WP_Error 39 * handling instead of exceptions, snake_case method naming, and integration 40 * with the Abilities API. 41 * 42 * Only the generating methods will return a WP_Error, to not break the fluent 43 * interface. As soon as any exception is caught in a chain of method calls, 44 * the returned instance will be in an error state, and all subsequent method 45 * calls will be no-ops that just return the same error state instance. Only 46 * when a generating method is called, the WP_Error will be returned. The 47 * support check methods are the exception to the no-op behavior: they return 48 * false rather than the error state instance. 49 * 50 * @since 7.0.0 51 * 52 * @phpstan-import-type Prompt from PromptBuilder 53 * 54 * @method self with_text(string $text) Adds text to the current message. 55 * @method self with_file($file, ?string $mimeType = null) Adds a file to the current message. 56 * @method self with_function_response(FunctionResponse $functionResponse) Adds a function response to the current message. 57 * @method self with_message_parts(MessagePart ...$parts) Adds message parts to the current message. 58 * @method self with_history(Message ...$messages) Adds conversation history messages. 59 * @method self using_model(ModelInterface $model) Sets the model to use for generation. 60 * @method self using_model_preference(...$preferredModels) Sets preferred models to evaluate in order. 61 * @method self using_model_config(ModelConfig $config) Sets the model configuration. 62 * @method self using_provider(string $providerIdOrClassName) Sets the provider to use for generation. 63 * @method self using_system_instruction(string $systemInstruction) Sets the system instruction. 64 * @method self using_max_tokens(int $maxTokens) Sets the maximum number of tokens to generate. 65 * @method self using_temperature(float $temperature) Sets the temperature for generation. 66 * @method self using_top_p(float $topP) Sets the top-p value for generation. 67 * @method self using_top_k(int $topK) Sets the top-k value for generation. 68 * @method self using_stop_sequences(string ...$stopSequences) Sets stop sequences for generation. 69 * @method self using_candidate_count(int $candidateCount) Sets the number of candidates to generate. 70 * @method self using_function_declarations(FunctionDeclaration ...$functionDeclarations) Sets the function declarations available to the model. 71 * @method self using_presence_penalty(float $presencePenalty) Sets the presence penalty for generation. 72 * @method self using_frequency_penalty(float $frequencyPenalty) Sets the frequency penalty for generation. 73 * @method self using_web_search(WebSearch $webSearch) Sets the web search configuration. 74 * @method self using_request_options(RequestOptions $options) Sets the request options for HTTP transport. 75 * @method self using_top_logprobs(?int $topLogprobs = null) Sets the top log probabilities configuration. 76 * @method self as_output_mime_type(string $mimeType) Sets the output MIME type. 77 * @method self as_output_schema(array<string, mixed> $schema) Sets the output schema. 78 * @method self as_output_modalities(ModalityEnum ...$modalities) Sets the output modalities. 79 * @method self as_output_file_type(FileTypeEnum $fileType) Sets the output file type. 80 * @method self as_output_media_orientation(MediaOrientationEnum $orientation) Sets the output media orientation. 81 * @method self as_output_media_aspect_ratio(string $aspectRatio) Sets the output media aspect ratio. 82 * @method self as_output_speech_voice(string $voice) Sets the output speech voice. 83 * @method self as_json_response(?array<string, mixed> $schema = null) Configures the prompt for JSON response output. 84 * @method bool is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability. 85 * @method bool is_supported_for_text_generation() Checks if the prompt is supported for text generation. 86 * @method bool is_supported_for_image_generation() Checks if the prompt is supported for image generation. 87 * @method bool is_supported_for_text_to_speech_conversion() Checks if the prompt is supported for text to speech conversion. 88 * @method bool is_supported_for_video_generation() Checks if the prompt is supported for video generation. 89 * @method bool is_supported_for_speech_generation() Checks if the prompt is supported for speech generation. 90 * @method bool is_supported_for_music_generation() Checks if the prompt is supported for music generation. 91 * @method bool is_supported_for_embedding_generation() Checks if the prompt is supported for embedding generation. 92 * @method GenerativeAiResult|WP_Error generate_result(?CapabilityEnum $capability = null) Generates a result from the prompt. 93 * @method GenerativeAiResult|WP_Error generate_text_result() Generates a text result from the prompt. 94 * @method GenerativeAiResult|WP_Error generate_image_result() Generates an image result from the prompt. 95 * @method GenerativeAiResult|WP_Error generate_speech_result() Generates a speech result from the prompt. 96 * @method GenerativeAiResult|WP_Error convert_text_to_speech_result() Converts text to speech and returns the result. 97 * @method GenerativeAiResult|WP_Error generate_video_result() Generates a video result from the prompt. 98 * @method string|WP_Error generate_text() Generates text from the prompt. 99 * @method list<string>|WP_Error generate_texts(?int $candidateCount = null) Generates multiple text candidates from the prompt. 100 * @method File|WP_Error generate_image() Generates an image from the prompt. 101 * @method list<File>|WP_Error generate_images(?int $candidateCount = null) Generates multiple images from the prompt. 102 * @method File|WP_Error convert_text_to_speech() Converts text to speech. 103 * @method list<File>|WP_Error convert_text_to_speeches(?int $candidateCount = null) Converts text to multiple speech outputs. 104 * @method File|WP_Error generate_speech() Generates speech from the prompt. 105 * @method list<File>|WP_Error generate_speeches(?int $candidateCount = null) Generates multiple speech outputs from the prompt. 106 * @method File|WP_Error generate_video() Generates a video from the prompt. 107 * @method list<File>|WP_Error generate_videos(?int $candidateCount = null) Generates multiple videos from the prompt. 108 */ 109 class WP_AI_Client_Prompt_Builder { 110 111 /** 112 * Wrapped prompt builder instance from the PHP AI Client SDK. 113 * 114 * @since 7.0.0 115 * @var PromptBuilder 116 */ 117 private PromptBuilder $builder; 118 119 /** 120 * WordPress error instance, if any error occurred during method calls. 121 * 122 * @since 7.0.0 123 * @var WP_Error|null 124 */ 125 private ?WP_Error $error = null; 126 127 /** 128 * List of methods that generate a result from the prompt. 129 * 130 * Structured as a map for faster lookups. 131 * 132 * @since 7.0.0 133 * @var array<string, bool> 134 */ 135 private static array $generating_methods = array( 136 'generate_result' => true, 137 'generate_text_result' => true, 138 'generate_image_result' => true, 139 'generate_speech_result' => true, 140 'convert_text_to_speech_result' => true, 141 'generate_video_result' => true, 142 'generate_text' => true, 143 'generate_texts' => true, 144 'generate_image' => true, 145 'generate_images' => true, 146 'convert_text_to_speech' => true, 147 'convert_text_to_speeches' => true, 148 'generate_speech' => true, 149 'generate_speeches' => true, 150 'generate_video' => true, 151 'generate_videos' => true, 152 ); 153 154 /** 155 * List of methods that check whether the prompt is supported. 156 * 157 * Structured as a map for faster lookups. 158 * 159 * @since 7.0.0 160 * @var array<string, bool> 161 */ 162 private static array $support_check_methods = array( 163 'is_supported' => true, 164 'is_supported_for_text_generation' => true, 165 'is_supported_for_image_generation' => true, 166 'is_supported_for_text_to_speech_conversion' => true, 167 'is_supported_for_video_generation' => true, 168 'is_supported_for_speech_generation' => true, 169 'is_supported_for_music_generation' => true, 170 'is_supported_for_embedding_generation' => true, 171 ); 172 173 /** 174 * Constructor. 175 * 176 * @since 7.0.0 177 * 178 * @param ProviderRegistry $registry The provider registry for finding suitable models. 179 * @param Prompt $prompt Optional. Initial prompt content. 180 * A string for simple text prompts, 181 * a MessagePart or Message object for 182 * structured content, an array for a 183 * message array shape, or a list of 184 * parts or messages for multi-turn 185 * conversations. Default null. 186 */ 187 public function __construct( ProviderRegistry $registry, $prompt = null ) { 188 try { 189 $this->builder = new PromptBuilder( $registry, $prompt, AiClient::getEventDispatcher() ); 190 } catch ( Throwable $e ) { 191 $this->builder = new PromptBuilder( $registry, null, AiClient::getEventDispatcher() ); 192 $this->error = $this->throwable_to_wp_error( $e ); 193 } 194 195 $default_timeout = 30.0; 196 197 /** 198 * Filters the default request timeout in seconds for AI Client HTTP requests. 199 * 200 * @since 7.0.0 201 * 202 * @param float $default_timeout The default timeout in seconds. 203 */ 204 $filtered_default_timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout ); 205 if ( is_numeric( $filtered_default_timeout ) && (float) $filtered_default_timeout >= 0.0 ) { 206 $default_timeout = (float) $filtered_default_timeout; 207 } else { 208 _doing_it_wrong( 209 __METHOD__, 210 sprintf( 211 /* translators: %s: wp_ai_client_default_request_timeout */ 212 __( 'The %s filter must return a non-negative number.' ), 213 '<code>wp_ai_client_default_request_timeout</code>' 214 ), 215 '7.0.0' 216 ); 217 } 218 219 $this->builder->usingRequestOptions( 220 RequestOptions::fromArray( 221 array( 222 RequestOptions::KEY_TIMEOUT => $default_timeout, 223 ) 224 ) 225 ); 226 } 227 228 /** 229 * Clones the wrapped prompt builder alongside this instance. 230 * 231 * The wrapped builder mutates its own state, so without this a clone would 232 * share that state with the original and any change made to one would be 233 * visible in the other. 234 * 235 * @since 7.2.0 236 */ 237 public function __clone() { 238 $this->builder = clone $this->builder; 239 240 if ( null !== $this->error ) { 241 $this->error = clone $this->error; 242 } 243 } 244 245 /** 246 * Registers WordPress abilities as function declarations for the AI model. 247 * 248 * Converts each WP_Ability to a FunctionDeclaration using the wpab__ prefix 249 * naming convention and passes them to the underlying prompt builder. 250 * 251 * @since 7.0.0 252 * 253 * @param WP_Ability|string ...$abilities The abilities to register, either as WP_Ability objects or ability name strings. 254 * @return self The current instance for method chaining. 255 */ 256 public function using_abilities( ...$abilities ): self { 257 $declarations = array(); 258 259 foreach ( $abilities as $ability ) { 260 if ( is_string( $ability ) ) { 261 $ability_name = $ability; 262 $ability = wp_get_ability( $ability ); 263 if ( ! $ability ) { 264 _doing_it_wrong( 265 __METHOD__, 266 sprintf( 267 /* translators: %s: string value of the ability name. */ 268 __( 'The ability %s was not found.' ), 269 '<code>' . esc_html( $ability_name ) . '</code>' 270 ), 271 '7.0.0' 272 ); 273 continue; 274 } 275 } 276 277 // This is only here as a sanity check, the method signature should ensure this already. 278 if ( ! $ability instanceof WP_Ability ) { 279 continue; 280 } 281 282 $function_name = WP_AI_Client_Ability_Function_Resolver::ability_name_to_function_name( $ability->get_name() ); 283 $input_schema = wp_prepare_json_schema_for_client( $ability->get_input_schema() ); 284 285 $declarations[] = new FunctionDeclaration( 286 $function_name, 287 $ability->get_description(), 288 ! empty( $input_schema ) ? $input_schema : null 289 ); 290 } 291 292 if ( ! empty( $declarations ) ) { 293 return $this->using_function_declarations( ...$declarations ); 294 } 295 296 return $this; 297 } 298 299 /** 300 * Magic method to proxy snake_case method calls to their PHP AI Client camelCase counterparts. 301 * 302 * This allows WordPress developers to use snake_case naming conventions. It catches 303 * any exceptions thrown, stores them, and returns a WP_Error when a terminate method 304 * is called, or false when a support check method is called. 305 * 306 * @since 7.0.0 307 * 308 * @param string $name The method name in snake_case. 309 * @param array<int, mixed> $arguments The method arguments. 310 * @return mixed The result of the method call. 311 */ 312 public function __call( string $name, array $arguments ) { 313 /* 314 * If an error occurred in a previous method call, either return the error for terminate methods, 315 * or return the same instance for other methods to maintain the fluent interface. 316 */ 317 if ( null !== $this->error ) { 318 if ( self::is_generating_method( $name ) ) { 319 return $this->error; 320 } 321 if ( self::is_support_check_method( $name ) ) { 322 return false; 323 } 324 return $this; 325 } 326 327 // Check if the prompt should be prevented for is_supported* and generate_*/convert_text_to_speech* methods. 328 if ( self::is_support_check_method( $name ) || self::is_generating_method( $name ) ) { 329 // If AI is not supported, then there's no need to apply the filter as the prompt will be prevented anyway. 330 $is_ai_disabled = ! wp_supports_ai(); 331 $prevent = $is_ai_disabled; 332 if ( ! $prevent ) { 333 /** 334 * Filters whether to prevent the prompt from being executed. 335 * 336 * @since 7.0.0 337 * 338 * @param bool $prevent Whether to prevent the prompt. Default false. 339 * @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only). 340 */ 341 $prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this ); 342 } 343 344 if ( $prevent ) { 345 // For is_supported* methods, return false. 346 if ( self::is_support_check_method( $name ) ) { 347 return false; 348 } 349 350 $error_message = $is_ai_disabled 351 ? __( 'AI features are not supported in this environment.' ) 352 : __( 'Prompt execution was prevented by a filter.' ); 353 354 // For generate_* and convert_text_to_speech* methods, create a WP_Error. 355 $this->error = new WP_Error( 356 'prompt_prevented', 357 $error_message, 358 array( 359 'status' => 503, 360 ) 361 ); 362 363 if ( self::is_generating_method( $name ) ) { 364 return $this->error; 365 } 366 return $this; 367 } 368 } 369 370 try { 371 $callable = $this->get_builder_callable( $name ); 372 $result = $callable( ...$arguments ); 373 374 // If the result is a PromptBuilder, return the current instance to allow method chaining. 375 if ( $result instanceof PromptBuilder ) { 376 return $this; 377 } 378 379 return $result; 380 } catch ( Throwable $e ) { 381 $this->error = $this->throwable_to_wp_error( $e ); 382 383 if ( self::is_generating_method( $name ) ) { 384 return $this->error; 385 } 386 if ( self::is_support_check_method( $name ) ) { 387 return false; 388 } 389 return $this; 390 } 391 } 392 393 /** 394 * Converts a throwable into a WP_Error with a structured error code and message. 395 * 396 * This method maps different throwable types to specific WP_Error codes and HTTP status codes. 397 * The presence of the status codes means these WP_Error objects can be easily used in REST API responses 398 * or other contexts where HTTP semantics are relevant. 399 * 400 * @since 7.0.0 401 * 402 * @param Throwable $throwable The throwable to convert. 403 * @return WP_Error The resulting WP_Error object. 404 */ 405 private function throwable_to_wp_error( Throwable $throwable ): WP_Error { 406 if ( $throwable instanceof NetworkException ) { 407 $error_code = 'prompt_network_error'; 408 $status_code = 503; 409 } elseif ( $throwable instanceof ClientException ) { 410 // `ClientException` uses HTTP status codes as exception codes, so we can rely on them. 411 $error_code = 'prompt_client_error'; 412 $status_code = $throwable->getCode() ? $throwable->getCode() : 400; 413 } elseif ( $throwable instanceof ServerException ) { 414 // `ServerException` uses HTTP status codes as exception codes, so we can rely on them. 415 $error_code = 'prompt_upstream_server_error'; 416 $status_code = $throwable->getCode() ? $throwable->getCode() : 500; 417 } elseif ( $throwable instanceof TokenLimitReachedException ) { 418 $error_code = 'prompt_token_limit_reached'; 419 $status_code = 400; 420 } elseif ( $throwable instanceof InvalidArgumentException ) { 421 $error_code = 'prompt_invalid_argument'; 422 $status_code = 400; 423 } else { 424 $error_code = 'prompt_builder_error'; 425 $status_code = 500; 426 } 427 428 return new WP_Error( 429 $error_code, 430 $throwable->getMessage(), 431 array( 432 'status' => $status_code, 433 'exception_class' => get_class( $throwable ), 434 ) 435 ); 436 } 437 438 /** 439 * Checks if a method name is a support check method (is_supported*). 440 * 441 * @since 7.0.0 442 * 443 * @param string $name The method name. 444 * @return bool True if the method is a support check method, false otherwise. 445 */ 446 private static function is_support_check_method( string $name ): bool { 447 return isset( self::$support_check_methods[ $name ] ); 448 } 449 450 /** 451 * Checks if a method name is a generating method (generate_*, convert_text_to_speech*). 452 * 453 * @since 7.0.0 454 * 455 * @param string $name The method name. 456 * @return bool True if the method is a generating method, false otherwise. 457 */ 458 private static function is_generating_method( string $name ): bool { 459 return isset( self::$generating_methods[ $name ] ); 460 } 461 462 /** 463 * Retrieves a callable for a given PHP AI Client SDK prompt builder method name. 464 * 465 * @since 7.0.0 466 * 467 * @param string $name The method name in snake_case. 468 * @return callable The callable for the specified method. 469 * 470 * @throws BadMethodCallException If the method does not exist. 471 */ 472 protected function get_builder_callable( string $name ): callable { 473 $camel_case_name = $this->snake_to_camel_case( $name ); 474 475 $method = array( $this->builder, $camel_case_name ); 476 if ( ! is_callable( $method ) ) { 477 throw new BadMethodCallException( 478 sprintf( 479 /* translators: 1: Method name. 2: Class name. */ 480 __( 'Method %1$s does not exist on %2$s.' ), 481 $name, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 482 get_class( $this->builder ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped 483 ) 484 ); 485 } 486 487 return $method; 488 } 489 490 /** 491 * Converts snake_case to camelCase. 492 * 493 * @since 7.0.0 494 * 495 * @param string $snake_case The snake_case string. 496 * @return string The camelCase string. 497 */ 498 private function snake_to_camel_case( string $snake_case ): string { 499 $parts = explode( '_', $snake_case ); 500 501 $camel_case = $parts[0]; 502 $parts_count = count( $parts ); 503 for ( $i = 1; $i < $parts_count; $i++ ) { 504 $camel_case .= ucfirst( $parts[ $i ] ); 505 } 506 507 return $camel_case; 508 } 509 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Sep 2 08:20:30 2026 | Cross-referenced by PHPXref |