Files
projektypkmp.pagedev.pl/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php
Roman Pyrih acb036dbd9 first commit
2026-05-21 15:33:11 +02:00

233 lines
6.1 KiB
PHP

<?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 );
}
}