[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ai-client/ -> class-wp-ai-client-prompt-builder.php (source)

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


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