• File: class-wp-ai-client-ability-function-resolver.php
  • Full Path: /home/u505525515/domains/medineeestates.com/public_html/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php
  • Date Modified: 05/25/2026 6:29 AM
  • File size: 6.12 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?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 );
    }
}