-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add WP AI Client #10881
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Add WP AI Client #10881
Changes from all commits
ce79165
b69e828
f5a9bf8
13b5f4f
68b268f
478e953
169ddff
7e30d97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there no tests for the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
|
||
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, correct.