-%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  ¥2" ÿÄ               ÿÄ J  	     ! 1AQ"aq2‘#BR‚¡ÁÑ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ              ÿÄ *        !1AQa‘"2q3±ð#b¡ÿÚ   ? ¼QxJQaÍuò¸Zö Úü8,ÐÚú
"SSn<rçù–´âE—^ªBÖ9À\†¸ÔÁT­ÃÛ5
ëd´³Í#Ý;Þ38œî ¶H£M:wÎ3…³…âpÔF&‚FK¸9„â4àGEõªfÿ ‘ñ(ßw­pŽF|È¥ù®häðÍÑ¶¹‘[ÒinÙW¶ùñY˜Q{›K"išÒ[Ú8žë\F¹@-?v"ÔU”,ìöžkÿ {I‡£šÍ?e
ríV
..............................................................................................................................................................................
.............................................................................                                                  
                                                                                                                                                                                     ÿØÿà JFIF      ÿÛ „ 	 ( %!1!%)+//.383,7(-.+



-%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  ¥2" ÿÄ               ÿÄ J  	     ! 1AQ"aq2‘#BR‚¡ÁÑ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ              ÿÄ *        !1AQa‘"2q3±ð#b¡ÿÚ   ? ¼QxJQaÍuò¸Zö Úü8,ÐÚú
"SSn<rçù–´âE—^ªBÖ9À\†¸ÔÁT­ÃÛ5
ëd´³Í#Ý;Þ38œî ¶H£M:wÎ3…³…âpÔF&‚FK¸9„â4àGEõªfÿ ‘ñ(ßw­pŽF|È¥ù®häðÍÑ¶¹‘[ÒinÙW¶ùñY˜Q{›K"išÒ[Ú8žë\F¹@-?v"ÔU”,ìöžkÿ {I‡£šÍ?e
ríV
..............................................................................................................................................................................
.............................................................................                                                  
                                                                                                                                                                                     class-wp-ai-client-ability-function-resolver.php                                                    0000644                 00000014171 15213106061 0015765 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_Ability_Function_Resolver class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;

/**
 * Resolves and executes WordPress Abilities API function calls from AI models.
 *
 * This class must be instantiated with the specific abilities that the AI model
 * is allowed to execute, ensuring that only explicitly specified abilities can
 * be called. This prevents the model from executing arbitrary abilities.
 *
 * @since 7.0.0
 */
class WP_AI_Client_Ability_Function_Resolver {

	/**
	 * Prefix used to identify ability function calls.
	 *
	 * @since 7.0.0
	 * @var string
	 */
	private const ABILITY_PREFIX = 'wpab__';

	/**
	 * Map of allowed ability names for this instance.
	 *
	 * Keys are ability name strings, values are `true` for O(1) lookup.
	 *
	 * @since 7.0.0
	 * @var array<string, true>
	 */
	private array $allowed_abilities;

	/**
	 * Constructor.
	 *
	 * @since 7.0.0
	 *
	 * @param WP_Ability|string ...$abilities The abilities that this resolver is allowed to execute.
	 */
	public function __construct( ...$abilities ) {
		$this->allowed_abilities = array();

		foreach ( $abilities as $ability ) {
			if ( $ability instanceof WP_Ability ) {
				$this->allowed_abilities[ $ability->get_name() ] = true;
			} elseif ( is_string( $ability ) ) {
				$this->allowed_abilities[ $ability ] = true;
			}
		}
	}

	/**
	 * Checks if a function call is an ability call.
	 *
	 * @since 7.0.0
	 *
	 * @param FunctionCall $call The function call to check.
	 * @return bool True if the function call is an ability call, false otherwise.
	 */
	public function is_ability_call( FunctionCall $call ): bool {
		$name = $call->getName();
		if ( null === $name ) {
			return false;
		}

		return str_starts_with( $name, self::ABILITY_PREFIX );
	}

	/**
	 * Executes a WordPress ability from a function call.
	 *
	 * Only abilities that were specified in the constructor are allowed to be
	 * executed. If the ability is not in the allowed list, an error response
	 * with code `ability_not_allowed` is returned.
	 *
	 * @since 7.0.0
	 *
	 * @param FunctionCall $call The function call to execute.
	 * @return FunctionResponse The response from executing the ability.
	 */
	public function execute_ability( FunctionCall $call ): FunctionResponse {
		$function_name = $call->getName() ?? 'unknown';
		$function_id   = $call->getId() ?? 'unknown';

		if ( ! $this->is_ability_call( $call ) ) {
			return new FunctionResponse(
				$function_id,
				$function_name,
				array(
					'error' => __( 'Not an ability function call' ),
					'code'  => 'invalid_ability_call',
				)
			);
		}

		$ability_name = self::function_name_to_ability_name( $function_name );

		if ( ! isset( $this->allowed_abilities[ $ability_name ] ) ) {
			return new FunctionResponse(
				$function_id,
				$function_name,
				array(
					/* translators: %s: ability name */
					'error' => sprintf( __( 'Ability "%s" was not specified in the allowed abilities list.' ), $ability_name ),
					'code'  => 'ability_not_allowed',
				)
			);
		}

		$ability = wp_get_ability( $ability_name );

		if ( ! $ability instanceof WP_Ability ) {
			return new FunctionResponse(
				$function_id,
				$function_name,
				array(
					/* translators: %s: ability name */
					'error' => sprintf( __( 'Ability "%s" not found' ), $ability_name ),
					'code'  => 'ability_not_found',
				)
			);
		}

		$args   = $call->getArgs();
		$result = $ability->execute( ! empty( $args ) ? $args : null );

		if ( is_wp_error( $result ) ) {
			return new FunctionResponse(
				$function_id,
				$function_name,
				array(
					'error' => $result->get_error_message(),
					'code'  => $result->get_error_code(),
					'data'  => $result->get_error_data(),
				)
			);
		}

		return new FunctionResponse(
			$function_id,
			$function_name,
			$result
		);
	}

	/**
	 * Checks if a message contains any ability function calls.
	 *
	 * @since 7.0.0
	 *
	 * @param Message $message The message to check.
	 * @return bool True if the message contains ability calls, false otherwise.
	 */
	public function has_ability_calls( Message $message ): bool {
		foreach ( $message->getParts() as $part ) {
			if ( $part->getType()->isFunctionCall() ) {
				$function_call = $part->getFunctionCall();
				if ( $function_call instanceof FunctionCall && $this->is_ability_call( $function_call ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Executes all ability function calls in a message.
	 *
	 * @since 7.0.0
	 *
	 * @param Message $message The message containing function calls.
	 * @return Message A new message with function responses.
	 */
	public function execute_abilities( Message $message ): Message {
		$response_parts = array();

		foreach ( $message->getParts() as $part ) {
			if ( $part->getType()->isFunctionCall() ) {
				$function_call = $part->getFunctionCall();
				if ( $function_call instanceof FunctionCall ) {
					$function_response = $this->execute_ability( $function_call );
					$response_parts[]  = new MessagePart( $function_response );
				}
			}
		}

		return new UserMessage( $response_parts );
	}

	/**
	 * Converts an ability name to a function name.
	 *
	 * Transforms "tec/create_event" to "wpab__tec__create_event".
	 *
	 * @since 7.0.0
	 *
	 * @param string $ability_name The ability name to convert.
	 * @return string The function name.
	 */
	public static function ability_name_to_function_name( string $ability_name ): string {
		return self::ABILITY_PREFIX . str_replace( '/', '__', $ability_name );
	}

	/**
	 * Converts a function name to an ability name.
	 *
	 * Transforms "wpab__tec__create_event" to "tec/create_event".
	 *
	 * @since 7.0.0
	 *
	 * @param string $function_name The function name to convert.
	 * @return string The ability name.
	 */
	public static function function_name_to_ability_name( string $function_name ): string {
		$without_prefix = substr( $function_name, strlen( self::ABILITY_PREFIX ) );

		return str_replace( '__', '/', $without_prefix );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                       class-wp-ai-client-prompt-builder.php                                                               0000644                 00000046513 15213106063 0013622 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_Prompt_Builder class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClient\AiClient;
use WordPress\AiClient\Builders\PromptBuilder;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\TokenLimitReachedException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Exception\ClientException;
use WordPress\AiClient\Providers\Http\Exception\NetworkException;
use WordPress\AiClient\Providers\Http\Exception\ServerException;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
use WordPress\AiClient\Tools\DTO\WebSearch;

/**
 * Fluent builder for constructing AI prompts, returning WP_Error on failure.
 *
 * This class provides a fluent interface for building prompts with various
 * content types and model configurations. It wraps the PHP AI Client SDK's
 * PromptBuilder and adds WordPress-specific behavior including WP_Error
 * handling instead of exceptions, snake_case method naming, and integration
 * with the Abilities API.
 *
 * Only the generating methods will return a WP_Error, to not break the fluent
 * interface. As soon as any exception is caught in a chain of method calls,
 * the returned instance will be in an error state, and all subsequent method
 * calls will be no-ops that just return the same error state instance. Only
 * when a generating method is called, the WP_Error will be returned.
 *
 * @since 7.0.0
 *
 * @phpstan-import-type Prompt from PromptBuilder
 *
 * @method self with_text(string $text) Adds text to the current message.
 * @method self with_file($file, ?string $mimeType = null) Adds a file to the current message.
 * @method self with_function_response(FunctionResponse $functionResponse) Adds a function response to the current message.
 * @method self with_message_parts(MessagePart ...$parts) Adds message parts to the current message.
 * @method self with_history(Message ...$messages) Adds conversation history messages.
 * @method self using_model(ModelInterface $model) Sets the model to use for generation.
 * @method self using_model_preference(...$preferredModels) Sets preferred models to evaluate in order.
 * @method self using_model_config(ModelConfig $config) Sets the model configuration.
 * @method self using_provider(string $providerIdOrClassName) Sets the provider to use for generation.
 * @method self using_system_instruction(string $systemInstruction) Sets the system instruction.
 * @method self using_max_tokens(int $maxTokens) Sets the maximum number of tokens to generate.
 * @method self using_temperature(float $temperature) Sets the temperature for generation.
 * @method self using_top_p(float $topP) Sets the top-p value for generation.
 * @method self using_top_k(int $topK) Sets the top-k value for generation.
 * @method self using_stop_sequences(string ...$stopSequences) Sets stop sequences for generation.
 * @method self using_candidate_count(int $candidateCount) Sets the number of candidates to generate.
 * @method self using_function_declarations(FunctionDeclaration ...$functionDeclarations) Sets the function declarations available to the model.
 * @method self using_presence_penalty(float $presencePenalty) Sets the presence penalty for generation.
 * @method self using_frequency_penalty(float $frequencyPenalty) Sets the frequency penalty for generation.
 * @method self using_web_search(WebSearch $webSearch) Sets the web search configuration.
 * @method self using_request_options(RequestOptions $options) Sets the request options for HTTP transport.
 * @method self using_top_logprobs(?int $topLogprobs = null) Sets the top log probabilities configuration.
 * @method self as_output_mime_type(string $mimeType) Sets the output MIME type.
 * @method self as_output_schema(array<string, mixed> $schema) Sets the output schema.
 * @method self as_output_modalities(ModalityEnum ...$modalities) Sets the output modalities.
 * @method self as_output_file_type(FileTypeEnum $fileType) Sets the output file type.
 * @method self as_output_media_orientation(MediaOrientationEnum $orientation) Sets the output media orientation.
 * @method self as_output_media_aspect_ratio(string $aspectRatio) Sets the output media aspect ratio.
 * @method self as_output_speech_voice(string $voice) Sets the output speech voice.
 * @method self as_json_response(?array<string, mixed> $schema = null) Configures the prompt for JSON response output.
 * @method bool|WP_Error is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability.
 * @method bool is_supported_for_text_generation() Checks if the prompt is supported for text generation.
 * @method bool is_supported_for_image_generation() Checks if the prompt is supported for image generation.
 * @method bool is_supported_for_text_to_speech_conversion() Checks if the prompt is supported for text to speech conversion.
 * @method bool is_supported_for_video_generation() Checks if the prompt is supported for video generation.
 * @method bool is_supported_for_speech_generation() Checks if the prompt is supported for speech generation.
 * @method bool is_supported_for_music_generation() Checks if the prompt is supported for music generation.
 * @method bool is_supported_for_embedding_generation() Checks if the prompt is supported for embedding generation.
 * @method GenerativeAiResult|WP_Error generate_result(?CapabilityEnum $capability = null) Generates a result from the prompt.
 * @method GenerativeAiResult|WP_Error generate_text_result() Generates a text result from the prompt.
 * @method GenerativeAiResult|WP_Error generate_image_result() Generates an image result from the prompt.
 * @method GenerativeAiResult|WP_Error generate_speech_result() Generates a speech result from the prompt.
 * @method GenerativeAiResult|WP_Error convert_text_to_speech_result() Converts text to speech and returns the result.
 * @method GenerativeAiResult|WP_Error generate_video_result() Generates a video result from the prompt.
 * @method string|WP_Error generate_text() Generates text from the prompt.
 * @method list<string>|WP_Error generate_texts(?int $candidateCount = null) Generates multiple text candidates from the prompt.
 * @method File|WP_Error generate_image() Generates an image from the prompt.
 * @method list<File>|WP_Error generate_images(?int $candidateCount = null) Generates multiple images from the prompt.
 * @method File|WP_Error convert_text_to_speech() Converts text to speech.
 * @method list<File>|WP_Error convert_text_to_speeches(?int $candidateCount = null) Converts text to multiple speech outputs.
 * @method File|WP_Error generate_speech() Generates speech from the prompt.
 * @method list<File>|WP_Error generate_speeches(?int $candidateCount = null) Generates multiple speech outputs from the prompt.
 * @method File|WP_Error generate_video() Generates a video from the prompt.
 * @method list<File>|WP_Error generate_videos(?int $candidateCount = null) Generates multiple videos from the prompt.
 */
class WP_AI_Client_Prompt_Builder {

	/**
	 * Wrapped prompt builder instance from the PHP AI Client SDK.
	 *
	 * @since 7.0.0
	 * @var PromptBuilder
	 */
	private PromptBuilder $builder;

	/**
	 * WordPress error instance, if any error occurred during method calls.
	 *
	 * @since 7.0.0
	 * @var WP_Error|null
	 */
	private ?WP_Error $error = null;

	/**
	 * List of methods that generate a result from the prompt.
	 *
	 * Structured as a map for faster lookups.
	 *
	 * @since 7.0.0
	 * @var array<string, bool>
	 */
	private static array $generating_methods = array(
		'generate_result'               => true,
		'generate_text_result'          => true,
		'generate_image_result'         => true,
		'generate_speech_result'        => true,
		'convert_text_to_speech_result' => true,
		'generate_video_result'         => true,
		'generate_text'                 => true,
		'generate_texts'                => true,
		'generate_image'                => true,
		'generate_images'               => true,
		'convert_text_to_speech'        => true,
		'convert_text_to_speeches'      => true,
		'generate_speech'               => true,
		'generate_speeches'             => true,
		'generate_video'                => true,
		'generate_videos'               => true,
	);

	/**
	 * List of methods that check whether the prompt is supported.
	 *
	 * Structured as a map for faster lookups.
	 *
	 * @since 7.0.0
	 * @var array<string, bool>
	 */
	private static array $support_check_methods = array(
		'is_supported'                               => true,
		'is_supported_for_text_generation'           => true,
		'is_supported_for_image_generation'          => true,
		'is_supported_for_text_to_speech_conversion' => true,
		'is_supported_for_video_generation'          => true,
		'is_supported_for_speech_generation'         => true,
		'is_supported_for_music_generation'          => true,
		'is_supported_for_embedding_generation'      => true,
	);

	/**
	 * Constructor.
	 *
	 * @since 7.0.0
	 *
	 * @param ProviderRegistry $registry The provider registry for finding suitable models.
	 * @param Prompt           $prompt   Optional. Initial prompt content.
	 *                                   A string for simple text prompts,
	 *                                   a MessagePart or Message object for
	 *                                   structured content, an array for a
	 *                                   message array shape, or a list of
	 *                                   parts or messages for multi-turn
	 *                                   conversations. Default null.
	 */
	public function __construct( ProviderRegistry $registry, $prompt = null ) {
		try {
			$this->builder = new PromptBuilder( $registry, $prompt, AiClient::getEventDispatcher() );
		} catch ( Exception $e ) {
			$this->builder = new PromptBuilder( $registry, null, AiClient::getEventDispatcher() );
			$this->error   = $this->exception_to_wp_error( $e );
		}

		$default_timeout = 30.0;

		/**
		 * Filters the default request timeout in seconds for AI Client HTTP requests.
		 *
		 * @since 7.0.0
		 *
		 * @param float $default_timeout The default timeout in seconds.
		 */
		$filtered_default_timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout );
		if ( is_numeric( $filtered_default_timeout ) && (float) $filtered_default_timeout >= 0.0 ) {
			$default_timeout = (float) $filtered_default_timeout;
		} else {
			_doing_it_wrong(
				__METHOD__,
				sprintf(
					/* translators: %s: wp_ai_client_default_request_timeout */
					__( 'The %s filter must return a non-negative number.' ),
					'<code>wp_ai_client_default_request_timeout</code>'
				),
				'7.0.0'
			);
		}

		$this->builder->usingRequestOptions(
			RequestOptions::fromArray(
				array(
					RequestOptions::KEY_TIMEOUT => $default_timeout,
				)
			)
		);
	}

	/**
	 * Registers WordPress abilities as function declarations for the AI model.
	 *
	 * Converts each WP_Ability to a FunctionDeclaration using the wpab__ prefix
	 * naming convention and passes them to the underlying prompt builder.
	 *
	 * @since 7.0.0
	 *
	 * @param WP_Ability|string ...$abilities The abilities to register, either as WP_Ability objects or ability name strings.
	 * @return self The current instance for method chaining.
	 */
	public function using_abilities( ...$abilities ): self {
		$declarations = array();

		foreach ( $abilities as $ability ) {
			if ( is_string( $ability ) ) {
				$ability_name = $ability;
				$ability      = wp_get_ability( $ability );
				if ( ! $ability ) {
					_doing_it_wrong(
						__METHOD__,
						sprintf(
							/* translators: %s: string value of the ability name. */
							__( 'The ability %s was not found.' ),
							'<code>' . esc_html( $ability_name ) . '</code>'
						),
						'7.0.0'
					);
					continue;
				}
			}

			// This is only here as a sanity check, the method signature should ensure this already.
			if ( ! $ability instanceof WP_Ability ) {
				continue;
			}

			$function_name = WP_AI_Client_Ability_Function_Resolver::ability_name_to_function_name( $ability->get_name() );
			$input_schema  = $ability->get_input_schema();

			$declarations[] = new FunctionDeclaration(
				$function_name,
				$ability->get_description(),
				! empty( $input_schema ) ? $input_schema : null
			);
		}

		if ( ! empty( $declarations ) ) {
			return $this->using_function_declarations( ...$declarations );
		}

		return $this;
	}

	/**
	 * Magic method to proxy snake_case method calls to their PHP AI Client camelCase counterparts.
	 *
	 * This allows WordPress developers to use snake_case naming conventions. It catches
	 * any exceptions thrown, stores them, and returns a WP_Error when a terminate method
	 * is called.
	 *
	 * @since 7.0.0
	 *
	 * @param string            $name      The method name in snake_case.
	 * @param array<int, mixed> $arguments The method arguments.
	 * @return mixed The result of the method call.
	 */
	public function __call( string $name, array $arguments ) {
		/*
		 * If an error occurred in a previous method call, either return the error for terminate methods,
		 * or return the same instance for other methods to maintain the fluent interface.
		 */
		if ( null !== $this->error ) {
			if ( self::is_generating_method( $name ) ) {
				return $this->error;
			}
			if ( self::is_support_check_method( $name ) ) {
				return false;
			}
			return $this;
		}

		// Check if the prompt should be prevented for is_supported* and generate_*/convert_text_to_speech* methods.
		if ( self::is_support_check_method( $name ) || self::is_generating_method( $name ) ) {
			// If AI is not supported, then there's no need to apply the filter as the prompt will be prevented anyway.
			$is_ai_disabled = ! wp_supports_ai();
			$prevent        = $is_ai_disabled;
			if ( ! $prevent ) {
				/**
				 * Filters whether to prevent the prompt from being executed.
				 *
				 * @since 7.0.0
				 *
				 * @param bool                        $prevent Whether to prevent the prompt. Default false.
				 * @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only).
				 */
				$prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this );
			}

			if ( $prevent ) {
				// For is_supported* methods, return false.
				if ( self::is_support_check_method( $name ) ) {
					return false;
				}

				$error_message = $is_ai_disabled
					? __( 'AI features are not supported in this environment.' )
					: __( 'Prompt execution was prevented by a filter.' );

				// For generate_* and convert_text_to_speech* methods, create a WP_Error.
				$this->error = new WP_Error(
					'prompt_prevented',
					$error_message,
					array(
						'status' => 503,
					)
				);

				if ( self::is_generating_method( $name ) ) {
					return $this->error;
				}
				return $this;
			}
		}

		try {
			$callable = $this->get_builder_callable( $name );
			$result   = $callable( ...$arguments );

			// If the result is a PromptBuilder, return the current instance to allow method chaining.
			if ( $result instanceof PromptBuilder ) {
				return $this;
			}

			return $result;
		} catch ( Exception $e ) {
			$this->error = $this->exception_to_wp_error( $e );

			if ( self::is_generating_method( $name ) ) {
				return $this->error;
			}
			return $this;
		}
	}

	/**
	 * Converts an exception into a WP_Error with a structured error code and message.
	 *
	 * This method maps different exception types to specific WP_Error codes and HTTP status codes.
	 * The presence of the status codes means these WP_Error objects can be easily used in REST API responses
	 * or other contexts where HTTP semantics are relevant.
	 *
	 * @since 7.0.0
	 *
	 * @param Exception $e The exception to convert.
	 * @return WP_Error The resulting WP_Error object.
	 */
	private function exception_to_wp_error( Exception $e ): WP_Error {
		if ( $e instanceof NetworkException ) {
			$error_code  = 'prompt_network_error';
			$status_code = 503;
		} elseif ( $e instanceof ClientException ) {
			// `ClientException` uses HTTP status codes as exception codes, so we can rely on them.
			$error_code  = 'prompt_client_error';
			$status_code = $e->getCode() ? $e->getCode() : 400;
		} elseif ( $e instanceof ServerException ) {
			// `ServerException` uses HTTP status codes as exception codes, so we can rely on them.
			$error_code  = 'prompt_upstream_server_error';
			$status_code = $e->getCode() ? $e->getCode() : 500;
		} elseif ( $e instanceof TokenLimitReachedException ) {
			$error_code  = 'prompt_token_limit_reached';
			$status_code = 400;
		} elseif ( $e instanceof InvalidArgumentException ) {
			$error_code  = 'prompt_invalid_argument';
			$status_code = 400;
		} else {
			$error_code  = 'prompt_builder_error';
			$status_code = 500;
		}

		return new WP_Error(
			$error_code,
			$e->getMessage(),
			array(
				'status'          => $status_code,
				'exception_class' => get_class( $e ),
			)
		);
	}

	/**
	 * Checks if a method name is a support check method (is_supported*).
	 *
	 * @since 7.0.0
	 *
	 * @param string $name The method name.
	 * @return bool True if the method is a support check method, false otherwise.
	 */
	private static function is_support_check_method( string $name ): bool {
		return isset( self::$support_check_methods[ $name ] );
	}

	/**
	 * Checks if a method name is a generating method (generate_*, convert_text_to_speech*).
	 *
	 * @since 7.0.0
	 *
	 * @param string $name The method name.
	 * @return bool True if the method is a generating method, false otherwise.
	 */
	private static function is_generating_method( string $name ): bool {
		return isset( self::$generating_methods[ $name ] );
	}

	/**
	 * Retrieves a callable for a given PHP AI Client SDK prompt builder method name.
	 *
	 * @since 7.0.0
	 *
	 * @param string $name The method name in snake_case.
	 * @return callable The callable for the specified method.
	 *
	 * @throws BadMethodCallException If the method does not exist.
	 */
	protected function get_builder_callable( string $name ): callable {
		$camel_case_name = $this->snake_to_camel_case( $name );

		$method = array( $this->builder, $camel_case_name );
		if ( ! is_callable( $method ) ) {
			throw new BadMethodCallException(
				sprintf(
					/* translators: 1: Method name. 2: Class name. */
					__( 'Method %1$s does not exist on %2$s.' ),
					$name, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
					get_class( $this->builder ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				)
			);
		}

		return $method;
	}

	/**
	 * Converts snake_case to camelCase.
	 *
	 * @since 7.0.0
	 *
	 * @param string $snake_case The snake_case string.
	 * @return string The camelCase string.
	 */
	private function snake_to_camel_case( string $snake_case ): string {
		$parts = explode( '_', $snake_case );

		$camel_case  = $parts[0];
		$parts_count = count( $parts );
		for ( $i = 1; $i < $parts_count; $i++ ) {
			$camel_case .= ucfirst( $parts[ $i ] );
		}

		return $camel_case;
	}
}
                                                                                                                                                                                     adapters/class-wp-ai-client-discovery-strategy.php                                                  0000644                 00000002217 15213106063 0016320 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_Discovery_Strategy class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy;
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\Psr17Factory;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;

/**
 * Discovery strategy for WordPress HTTP client.
 *
 * Registers the WordPress HTTP client adapter with the HTTPlug discovery system
 * so the AI Client SDK can find and use it automatically.
 *
 * @since 7.0.0
 * @internal Intended only to register WordPress's HTTP client so that the PHP AI Client SDK can use it.
 * @access private
 */
class WP_AI_Client_Discovery_Strategy extends AbstractClientDiscoveryStrategy {

	/**
	 * Creates an instance of the WordPress HTTP client.
	 *
	 * @since 7.0.0
	 *
	 * @param Psr17Factory $psr17_factory The PSR-17 factory for creating HTTP messages.
	 * @return ClientInterface The PSR-18 HTTP client.
	 */
	protected static function createClient( Psr17Factory $psr17_factory ): ClientInterface {
		return new WP_AI_Client_HTTP_Client( $psr17_factory, $psr17_factory );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                 adapters/class-wp-ai-client-http-client.php                                                         0000644                 00000015057 15213106063 0014712 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_HTTP_Client class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Exception\NetworkException;

/**
 * PSR-18 HTTP Client adapter using WordPress HTTP API.
 *
 * Allows WordPress HTTP functions to be used as a PSR-18 compliant HTTP client
 * for the AI Client SDK.
 *
 * @since 7.0.0
 * @internal Intended only to wire up the PHP AI Client SDK to WordPress's HTTP client.
 * @access private
 */
class WP_AI_Client_HTTP_Client implements ClientInterface, ClientWithOptionsInterface {

	/**
	 * Response factory instance.
	 *
	 * @since 7.0.0
	 */
	private ResponseFactoryInterface $response_factory;

	/**
	 * Stream factory instance.
	 *
	 * @since 7.0.0
	 */
	private StreamFactoryInterface $stream_factory;

	/**
	 * Constructor.
	 *
	 * @since 7.0.0
	 *
	 * @param ResponseFactoryInterface $response_factory PSR-17 Response factory.
	 * @param StreamFactoryInterface   $stream_factory   PSR-17 Stream factory.
	 */
	public function __construct( ResponseFactoryInterface $response_factory, StreamFactoryInterface $stream_factory ) {
		$this->response_factory = $response_factory;
		$this->stream_factory   = $stream_factory;
	}

	/**
	 * Sends a PSR-7 request and returns a PSR-7 response.
	 *
	 * @since 7.0.0
	 *
	 * @param RequestInterface $request The PSR-7 request.
	 * @return ResponseInterface The PSR-7 response.
	 *
	 * @throws NetworkException If the WordPress HTTP request fails.
	 */
	public function sendRequest( RequestInterface $request ): ResponseInterface {
		$args = $this->prepare_wp_args( $request );
		$url  = (string) $request->getUri();

		$response = wp_safe_remote_request( $url, $args );

		if ( is_wp_error( $response ) ) {
			$message = sprintf(
				/* translators: 1: HTTP method (e.g. GET, POST). 2: Request URL. 3: Error message. */
				__( 'Network error occurred while sending %1$s request to %2$s: %3$s' ),
				$request->getMethod(),
				$url,
				$response->get_error_message()
			);
			throw new NetworkException( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		return $this->create_psr_response( $response );
	}

	/**
	 * Sends a PSR-7 request with transport options and returns a PSR-7 response.
	 *
	 * @since 7.0.0
	 *
	 * @param RequestInterface $request The PSR-7 request.
	 * @param RequestOptions   $options Transport options for the request.
	 * @return ResponseInterface The PSR-7 response.
	 *
	 * @throws NetworkException If the WordPress HTTP request fails.
	 */
	public function sendRequestWithOptions( RequestInterface $request, RequestOptions $options ): ResponseInterface {
		$args = $this->prepare_wp_args( $request, $options );
		$url  = (string) $request->getUri();

		$response = wp_safe_remote_request( $url, $args );

		if ( is_wp_error( $response ) ) {
			$message = sprintf(
				/* translators: 1: Request URL. 2: Error message. */
				__( 'Network error occurred while sending request to %1$s: %2$s' ),
				$url,
				$response->get_error_message()
			);

			throw new NetworkException(
				$message, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				$response->get_error_code() ? (int) $response->get_error_code() : 0
			);
		}

		return $this->create_psr_response( $response );
	}

	/**
	 * Prepares WordPress HTTP API arguments from a PSR-7 request.
	 *
	 * @since 7.0.0
	 *
	 * @param RequestInterface    $request The PSR-7 request.
	 * @param RequestOptions|null $options Optional transport options for the request.
	 * @return array<string, mixed> WordPress HTTP API arguments.
	 */
	private function prepare_wp_args( RequestInterface $request, ?RequestOptions $options = null ): array {
		$args = array(
			'method'      => $request->getMethod(),
			'headers'     => $this->prepare_headers( $request ),
			'body'        => $this->prepare_body( $request ),
			'httpversion' => $request->getProtocolVersion(),
			'blocking'    => true,
		);

		if ( null !== $options ) {
			if ( null !== $options->getTimeout() ) {
				$args['timeout'] = $options->getTimeout();
			}

			if ( null !== $options->getMaxRedirects() ) {
				$args['redirection'] = $options->getMaxRedirects();
			}
		}

		return $args;
	}

	/**
	 * Prepares headers for WordPress HTTP API.
	 *
	 * @since 7.0.0
	 *
	 * @param RequestInterface $request The PSR-7 request.
	 * @return array<string, string> Headers array for WordPress HTTP API.
	 */
	private function prepare_headers( RequestInterface $request ): array {
		$headers = array();

		foreach ( $request->getHeaders() as $name => $values ) {
			$headers[ (string) $name ] = implode( ', ', $values );
		}

		return $headers;
	}

	/**
	 * Prepares request body for WordPress HTTP API.
	 *
	 * @since 7.0.0
	 *
	 * @param RequestInterface $request The PSR-7 request.
	 * @return string|null The request body.
	 */
	private function prepare_body( RequestInterface $request ): ?string {
		$body = $request->getBody();

		if ( $body->getSize() === 0 ) {
			return null;
		}

		if ( $body->isSeekable() ) {
			$body->rewind();
		}

		return (string) $body;
	}

	/**
	 * Creates a PSR-7 response from a WordPress HTTP response.
	 *
	 * @since 7.0.0
	 *
	 * @param array<string, mixed> $wp_response WordPress HTTP API response array.
	 * @return ResponseInterface PSR-7 response.
	 */
	private function create_psr_response( array $wp_response ): ResponseInterface {
		$status_code   = wp_remote_retrieve_response_code( $wp_response );
		$reason_phrase = wp_remote_retrieve_response_message( $wp_response );
		$headers       = wp_remote_retrieve_headers( $wp_response );
		$body          = wp_remote_retrieve_body( $wp_response );

		$response = $this->response_factory->createResponse( (int) $status_code, $reason_phrase );

		if ( $headers instanceof WP_HTTP_Requests_Response ) {
			$headers = $headers->get_headers();
		}

		if ( is_array( $headers ) || $headers instanceof Traversable ) {
			foreach ( $headers as $name => $value ) {
				$response = $response->withHeader( $name, $value );
			}
		}

		if ( ! empty( $body ) ) {
			$stream   = $this->stream_factory->createStream( $body );
			$response = $response->withBody( $stream );
		}

		return $response;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 adapters/class-wp-ai-client-cache.php                                                               0000644                 00000013662 15213106063 0013522 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_Cache class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface;

/**
 * WordPress-specific PSR-16 cache adapter for the AI Client.
 *
 * Bridges PSR-16 cache operations to WordPress object cache functions,
 * enabling the AI client to leverage WordPress caching infrastructure.
 *
 * @since 7.0.0
 * @internal Intended only to wire up the PHP AI Client SDK to WordPress's caching system.
 * @access private
 */
class WP_AI_Client_Cache implements CacheInterface {

	/**
	 * Cache group used for all cache operations.
	 *
	 * @since 7.0.0
	 * @var string
	 */
	private const CACHE_GROUP = 'wp_ai_client';

	/**
	 * Fetches a value from the cache.
	 *
	 * @since 7.0.0
	 *
	 * @param string $key           The unique key of this item in the cache.
	 * @param mixed  $default_value Default value to return if the key does not exist.
	 * @return mixed The value of the item from the cache, or $default_value in case of cache miss.
	 */
	public function get( $key, $default_value = null ) {
		$found = false;
		$value = wp_cache_get( $key, self::CACHE_GROUP, false, $found );

		if ( ! $found ) {
			return $default_value;
		}

		return $value;
	}

	/**
	 * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
	 *
	 * @since 7.0.0
	 *
	 * @param string                $key   The key of the item to store.
	 * @param mixed                 $value The value of the item to store, must be serializable.
	 * @param null|int|DateInterval $ttl   Optional. The TTL value of this item.
	 * @return bool True on success and false on failure.
	 */
	public function set( $key, $value, $ttl = null ): bool {
		$expire = $this->ttl_to_seconds( $ttl );

		return wp_cache_set( $key, $value, self::CACHE_GROUP, $expire );
	}

	/**
	 * Delete an item from the cache by its unique key.
	 *
	 * @since 7.0.0
	 *
	 * @param string $key The unique cache key of the item to delete.
	 * @return bool True if the item was successfully removed. False if there was an error.
	 */
	public function delete( $key ): bool {
		return wp_cache_delete( $key, self::CACHE_GROUP );
	}

	/**
	 * Wipes clean the entire cache's keys.
	 *
	 * This method only clears the cache group used by this adapter. If the underlying
	 * cache implementation does not support group flushing, this method returns false.
	 *
	 * @since 7.0.0
	 *
	 * @return bool True on success and false on failure.
	 */
	public function clear(): bool {
		if ( ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) {
			return false;
		}

		return wp_cache_flush_group( self::CACHE_GROUP );
	}

	/**
	 * Obtains multiple cache items by their unique keys.
	 *
	 * @since 7.0.0
	 *
	 * @param iterable<string> $keys          A list of keys that can be obtained in a single operation.
	 * @param mixed            $default_value Default value to return for keys that do not exist.
	 * @return array<string, mixed> A list of key => value pairs.
	 */
	public function getMultiple( $keys, $default_value = null ): array {
		/**
		 * Keys array.
		 *
		 * @var array<string> $keys_array
		 */
		$keys_array = $this->iterable_to_array( $keys );
		$values     = wp_cache_get_multiple( $keys_array, self::CACHE_GROUP );
		$result     = array();

		foreach ( $keys_array as $key ) {
			if ( false === $values[ $key ] ) {
				// Could be a stored false or a cache miss — disambiguate via get().
				$result[ $key ] = $this->get( $key, $default_value );
			} else {
				$result[ $key ] = $values[ $key ];
			}
		}

		return $result;
	}

	/**
	 * Persists a set of key => value pairs in the cache, with an optional TTL.
	 *
	 * @since 7.0.0
	 *
	 * @param iterable<string, mixed> $values A list of key => value pairs for a multiple-set operation.
	 * @param null|int|DateInterval   $ttl    Optional. The TTL value of this item.
	 * @return bool True on success and false on failure.
	 */
	public function setMultiple( $values, $ttl = null ): bool {
		$values_array = $this->iterable_to_array( $values );
		$expire       = $this->ttl_to_seconds( $ttl );
		$results      = wp_cache_set_multiple( $values_array, self::CACHE_GROUP, $expire );

		// Return true only if all operations succeeded.
		return ! in_array( false, $results, true );
	}

	/**
	 * Deletes multiple cache items in a single operation.
	 *
	 * @since 7.0.0
	 *
	 * @param iterable<string> $keys A list of string-based keys to be deleted.
	 * @return bool True if the items were successfully removed. False if there was an error.
	 */
	public function deleteMultiple( $keys ): bool {
		$keys_array = $this->iterable_to_array( $keys );
		$results    = wp_cache_delete_multiple( $keys_array, self::CACHE_GROUP );

		// Return true only if all operations succeeded.
		return ! in_array( false, $results, true );
	}

	/**
	 * Determines whether an item is present in the cache.
	 *
	 * @since 7.0.0
	 *
	 * @param string $key The cache item key.
	 * @return bool True if the item exists in the cache, false otherwise.
	 */
	public function has( $key ): bool {
		$found = false;
		wp_cache_get( $key, self::CACHE_GROUP, false, $found );

		return (bool) $found;
	}

	/**
	 * Converts a PSR-16 TTL value to seconds for WordPress cache functions.
	 *
	 * @since 7.0.0
	 *
	 * @param null|int|DateInterval $ttl The TTL value.
	 * @return int The TTL in seconds, or 0 for no expiration.
	 */
	private function ttl_to_seconds( $ttl ): int {
		if ( null === $ttl ) {
			return 0;
		}

		if ( $ttl instanceof DateInterval ) {
			$now = new DateTime();
			$end = ( clone $now )->add( $ttl );

			return $end->getTimestamp() - $now->getTimestamp();
		}

		return max( 0, (int) $ttl );
	}

	/**
	 * Converts an iterable to an array.
	 *
	 * @since 7.0.0
	 *
	 * @param iterable<mixed> $items The iterable to convert.
	 * @return array<mixed> The array.
	 */
	private function iterable_to_array( $items ): array {
		if ( is_array( $items ) ) {
			return $items;
		}

		return iterator_to_array( $items );
	}
}
                                                                              adapters/error_log                                                                                  0000644                 00000011143 15213106063 0010261 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [08-Jun-2026 03:56:18 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php:29
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php on line 29
[08-Jun-2026 13:42:22 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php on line 22
[08-Jun-2026 16:26:48 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php on line 22
[08-Jun-2026 16:26:49 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php:24
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php on line 24
[08-Jun-2026 16:26:50 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php:24
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php on line 24
[08-Jun-2026 16:26:51 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php on line 22
[08-Jun-2026 16:26:52 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php:29
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php on line 29
[08-Jun-2026 20:42:42 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php on line 22
[12-Jun-2026 17:27:03 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php on line 22
[12-Jun-2026 17:27:38 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php:29
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php on line 29
[12-Jun-2026 17:29:58 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php:22
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-event-dispatcher.php on line 22
[12-Jun-2026 17:29:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy" not found in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php:24
Stack trace:
#0 {main}
  thrown in /home/kagibyzw/public_html/wp-includes/ai-client/adapters/class-wp-ai-client-discovery-strategy.php on line 24
                                                                                                                                                                                                                                                                                                                                                                                                                             adapters/class-wp-ai-client-event-dispatcher.php                                                    0000644                 00000004773 15213106063 0015727 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * WP AI Client: WP_AI_Client_Event_Dispatcher class
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;

/**
 * WordPress-specific PSR-14 event dispatcher for the AI Client.
 *
 * Bridges PSR-14 events to WordPress action hooks, enabling plugins to hook
 * into AI client lifecycle events.
 *
 * @since 7.0.0
 * @internal Intended only to wire up the PHP AI Client SDK to WordPress's hook system.
 * @access private
 */
class WP_AI_Client_Event_Dispatcher implements EventDispatcherInterface {

	/**
	 * Dispatches an event to WordPress action hooks.
	 *
	 * Converts the event class name to a WordPress action hook name and fires it.
	 * For example, BeforeGenerateResultEvent becomes wp_ai_client_before_generate_result.
	 *
	 * @since 7.0.0
	 *
	 * @param object $event The event object to dispatch.
	 * @return object The same event object, potentially modified by listeners.
	 */
	public function dispatch( object $event ): object {
		$event_name = $this->get_hook_name_portion_for_event( $event );

		/**
		 * Fires when an AI client event is dispatched.
		 *
		 * The dynamic portion of the hook name, `$event_name`, refers to the
		 * snake_case version of the event class name, without the `_event` suffix.
		 *
		 * For example, an event class named `BeforeGenerateResultEvent` will fire the
		 * `wp_ai_client_before_generate_result` action hook.
		 *
		 * In practice, the available action hook names are:
		 *
		 * - wp_ai_client_before_generate_result
		 * - wp_ai_client_after_generate_result
		 *
		 * @since 7.0.0
		 *
		 * @param object $event The event object.
		 */
		do_action( "wp_ai_client_{$event_name}", $event );

		return $event;
	}

	/**
	 * Converts an event object class name to a WordPress action hook name portion.
	 *
	 * @since 7.0.0
	 *
	 * @param object $event The event object.
	 * @return string The hook name portion derived from the event class name.
	 */
	private function get_hook_name_portion_for_event( object $event ): string {
		$class_name = get_class( $event );
		$pos        = strrpos( $class_name, '\\' );
		$short_name = false !== $pos ? substr( $class_name, $pos + 1 ) : $class_name;

		// Convert PascalCase to snake_case.
		$snake_case = strtolower( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $short_name ) );

		// Strip '_event' suffix if present.
		if ( str_ends_with( $snake_case, '_event' ) ) {
			$snake_case = (string) substr( $snake_case, 0, -6 );
		}

		return $snake_case;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     