Skip to content
Open
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<exclude-pattern>/src/wp-includes/js/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/PHPMailer/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/Requests/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/php-ai-client/*</exclude-pattern>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to confirm. Is this entire directory "external library" code?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, correct.

<exclude-pattern>/src/wp-includes/SimplePie/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/sodium_compat/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/Text/*</exclude-pattern>
Expand Down
2 changes: 2 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
<directory suffix=".php">src/wp-includes/IXR</directory>
<directory suffix=".php">src/wp-includes/PHPMailer</directory>
<directory suffix=".php">src/wp-includes/Requests</directory>
<directory suffix=".php">src/wp-includes/php-ai-client</directory>
<directory suffix=".php">src/wp-includes/ai-client-utils</directory>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there no tests for the src/wp-includes/ai-client-utils directory? At a quick glance, it seems like tests/phpunit/tests/ai-client/wpAiClientAbilityFunctionResolver.php may test the class within the wp-includes/ai-client-utils/class-wp-ai-client-ability-function-resolver.php file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I agree. This should not be excluded, it needs test because this will be Core code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we saying both should be removed from exclusion? Or just the ai-client-utils?

<directory suffix=".php">src/wp-includes/SimplePie</directory>
<directory suffix=".php">src/wp-includes/sodium_compat</directory>
<directory suffix=".php">src/wp-includes/Text</directory>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?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.
*
* @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__';

/**
* 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 static 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.
*
* @since 7.0.0
*
* @param FunctionCall $call The function call to execute.
* @return FunctionResponse The response from executing the ability.
*/
public static function execute_ability( FunctionCall $call ): FunctionResponse {
$function_name = $call->getName() ?? 'unknown';
$function_id = $call->getId() ?? 'unknown';

if ( ! self::is_ability_call( $call ) ) {
return new FunctionResponse(
$function_id,
$function_name,
array(
'error' => 'Not an ability function call',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know there's some disagreement around translating error or debug messages but wondering if all the strings in this file should be passed through translation?

'code' => 'invalid_ability_call',
)
);
}

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

if ( ! $ability instanceof WP_Ability ) {
return new FunctionResponse(
$function_id,
$function_name,
array(
'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 static 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 && self::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 static 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 = self::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.
*/
private 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 );
}
}
Loading
Loading