Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

<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',
'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 );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
/**
* WP AI Client: WP_AI_Client_Discovery_Strategy class
*
* @package WordPress
* @subpackage AI
* @since 7.0.0
*/

use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy;
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
*/
class WP_AI_Client_Discovery_Strategy implements DiscoveryStrategy {

/**
* Initializes and registers the discovery strategy.
*
* @since 7.0.0
*/
public static function init() {
if ( ! class_exists( '\WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery' ) ) {
return;
}

Psr18ClientDiscovery::prependStrategy( self::class );
}

/**
* Gets candidates for discovery.
*
* @since 7.0.0
*
* @param string $type The type of discovery.
* @return array<array<string, mixed>> List of candidates.
*/
public static function getCandidates( $type ) {
if ( ClientInterface::class === $type ) {
return array(
array(
'class' => static function () {
return self::create_wordpress_client();
},
),
);
}

$psr17_factories = array(
'WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface',
'WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface',
'WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface',
'WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface',
'WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface',
'WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface',
);

if ( in_array( $type, $psr17_factories, true ) ) {
return array(
array(
'class' => WP_AI_Client_PSR17_Factory::class,
),
);
}

return array();
}

/**
* Creates an instance of the WordPress HTTP client.
*
* @since 7.0.0
*
* @return WP_AI_Client_HTTP_Client
*/
private static function create_wordpress_client() {
$psr17_factory = new WP_AI_Client_PSR17_Factory();
return new WP_AI_Client_HTTP_Client(
$psr17_factory,
$psr17_factory
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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
*/
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;
}
}
Loading
Loading