= $max_tools ) { break; } } return $tools; } /** * Whether a tool name belongs to an ability. * * @param string $action_name Provider tool/action name. * @return bool */ public static function is_ability_tool( $action_name ) { return '' !== self::action_name_to_ability_name( $action_name ); } /** * Execute an ability tool call. * * @param array $tool_call OpenAI function_call item. * @param array $context Request context available only to AI Editor. * @return array */ public static function execute_tool_call( $tool_call, $context = array() ) { if ( empty( $tool_call['name'] ) || ! is_string( $tool_call['name'] ) ) { return array( 'status' => 'error', 'message' => __( 'The model requested an invalid ability tool.', 'ai-editor' ), ); } $ability_name = self::action_name_to_ability_name( $tool_call['name'] ); if ( '' === $ability_name || ! function_exists( 'wp_get_ability' ) ) { return array( 'status' => 'error', 'message' => __( 'The requested WordPress ability is not available.', 'ai-editor' ), ); } $ability = wp_get_ability( $ability_name ); if ( ! $ability || ! self::is_ai_editor_enabled_ability( $ability ) ) { return array( 'status' => 'error', 'ability' => $ability_name, 'message' => __( 'The requested WordPress ability is not enabled for AI Editor.', 'ai-editor' ), ); } $arguments = array(); if ( isset( $tool_call['arguments'] ) && '' !== $tool_call['arguments'] ) { $arguments = json_decode( $tool_call['arguments'], true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $arguments ) ) { return array( 'status' => 'error', 'ability' => $ability_name, 'message' => __( 'The model returned invalid ability arguments.', 'ai-editor' ), ); } } $arguments = self::apply_ai_editor_context_to_arguments( $ability_name, $arguments, $context ); $input = self::prepare_input_for_ability( $ability, $arguments ); $result = $ability->execute( $input ); if ( is_wp_error( $result ) ) { return array( 'status' => 'error', 'ability' => $ability_name, 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), ); } return array( 'status' => 'success', 'ability' => $ability_name, 'result' => $result, ); } /** * Apply private AI Editor request context to supported built-in ability inputs. * * @param string $ability_name WordPress ability name. * @param array $arguments Decoded OpenAI tool arguments. * @param array $context Request context. * @return array */ private static function apply_ai_editor_context_to_arguments( $ability_name, $arguments, $context ) { if ( 'ai-editor/query-content' !== $ability_name || empty( $arguments['use_current_document'] ) || ! is_array( $context ) ) { return $arguments; } $current_document = isset( $context['current_document'] ) && is_array( $context['current_document'] ) ? $context['current_document'] : array(); $post_id = isset( $current_document['id'] ) ? absint( $current_document['id'] ) : 0; if ( $post_id <= 0 ) { return $arguments; } $post_type = isset( $current_document['type'] ) ? sanitize_key( (string) $current_document['type'] ) : ''; if ( '' !== $post_type ) { $arguments['post_type'] = $post_type; } $post_status = isset( $current_document['status'] ) ? sanitize_key( (string) $current_document['status'] ) : ''; if ( '' !== $post_status ) { $arguments['post_status'] = $post_status; } $arguments['post_id'] = $post_id; $arguments['search'] = ''; return $arguments; } /** * Return abilities opted into AI Editor. * * @return array */ private static function get_ai_editor_abilities() { $abilities = wp_get_abilities(); if ( ! is_array( $abilities ) ) { return array(); } $abilities = array_values( array_filter( $abilities, static function ( $ability ) { return self::is_ai_editor_enabled_ability( $ability ); } ) ); $abilities = apply_filters( 'ai_editor_allowed_abilities', $abilities ); return is_array( $abilities ) ? array_values( $abilities ) : array(); } /** * Whether an ability opted into AI Editor. * * @param mixed $ability Ability object. * @return bool */ private static function is_ai_editor_enabled_ability( $ability ) { $config = self::get_ai_editor_meta_config( $ability ); return ! empty( $config['enabled'] ) && '' !== self::sanitize_action_name( $config['action_name'] ) && '' !== $config['description']; } /** * Normalize the ai_editor meta configuration. * * @param mixed $ability Ability object. * @return array */ private static function get_ai_editor_meta_config( $ability ) { if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_meta_item' ) ) { return array( 'enabled' => false ); } $meta = $ability->get_meta_item( 'ai_editor', false ); if ( true === $meta ) { return array( 'enabled' => true, 'description' => '', 'action_name' => '', ); } if ( is_array( $meta ) ) { return array( 'enabled' => ! array_key_exists( 'enabled', $meta ) || false !== $meta['enabled'], 'description' => isset( $meta['description'] ) && is_string( $meta['description'] ) ? trim( $meta['description'] ) : '', 'action_name' => isset( $meta['action_name'] ) && is_string( $meta['action_name'] ) ? trim( $meta['action_name'] ) : '', ); } return array( 'enabled' => false ); } /** * Convert an ability to a provider tool definition. * * @param mixed $ability Ability object. * @return array */ private static function ability_to_openai_tool( $ability ) { if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) || ! method_exists( $ability, 'get_input_schema' ) ) { return array(); } $config = self::get_ai_editor_meta_config( $ability ); $action_name = self::sanitize_action_name( $config['action_name'] ); if ( '' === $action_name || '' === $config['description'] ) { return array(); } return array( 'type' => 'function', 'name' => $action_name, 'description' => $config['description'], 'strict' => true, 'parameters' => self::normalize_schema_for_openai( $ability->get_input_schema() ), ); } /** * Return a valid provider tool/action name. * * @param string $action_name Tool name from meta.ai_editor.action_name. * @return string */ private static function sanitize_action_name( $action_name ) { if ( ! is_string( $action_name ) ) { return ''; } $action_name = trim( $action_name ); if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $action_name ) ) { return ''; } if ( in_array( $action_name, self::get_reserved_action_names(), true ) ) { return ''; } return $action_name; } /** * Return provider tool/action names reserved by AI Editor's built-in editor tools. * * @return array */ private static function get_reserved_action_names() { return array( 'insert_block_markup', 'update_selected_block_markup', 'change_editor_history', 'request_clarification', ); } /** * Resolve an OpenAI ability tool name back to a WordPress ability name. * * @param string $action_name Provider tool/action name. * @return string */ private static function action_name_to_ability_name( $action_name ) { $action_name = self::sanitize_action_name( $action_name ); if ( '' === $action_name || ! self::is_supported() ) { return ''; } foreach ( self::get_ai_editor_abilities() as $ability ) { if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) ) { continue; } $config = self::get_ai_editor_meta_config( $ability ); if ( $action_name === self::sanitize_action_name( $config['action_name'] ) ) { return $ability->get_name(); } } return ''; } /** * Normalize an ability input schema into an OpenAI function parameters object. * * @param array $schema Ability input schema. * @return array */ private static function normalize_schema_for_openai( $schema ) { if ( ! is_array( $schema ) || empty( $schema ) ) { return array( 'type' => 'object', 'properties' => new stdClass(), 'required' => array(), 'additionalProperties' => false, ); } $type = isset( $schema['type'] ) ? $schema['type'] : ''; $is_object_schema = 'object' === $type || isset( $schema['properties'] ); if ( ! $is_object_schema ) { return array( 'type' => 'object', 'properties' => (object) array( 'value' => self::normalize_schema_node( $schema ), ), 'required' => array( 'value' ), 'additionalProperties' => false, ); } $normalized = self::normalize_schema_node( $schema ); if ( ! isset( $normalized['type'] ) ) { $normalized['type'] = 'object'; } if ( ! isset( $normalized['properties'] ) || ! is_array( $normalized['properties'] ) ) { $normalized['properties'] = new stdClass(); } if ( is_array( $normalized['properties'] ) && empty( $normalized['properties'] ) ) { $normalized['properties'] = new stdClass(); } $normalized['additionalProperties'] = false; return $normalized; } /** * Normalize schema nodes so empty object-like children encode as JSON objects. * * @param mixed $node Schema node. * @return mixed */ private static function normalize_schema_node( $node ) { if ( ! is_array( $node ) ) { return $node; } $normalized = array(); foreach ( $node as $key => $value ) { if ( 'properties' === $key && is_array( $value ) && empty( $value ) ) { $normalized[ $key ] = new stdClass(); continue; } $normalized[ $key ] = self::normalize_schema_node( $value ); } if ( isset( $normalized['required'] ) && ! is_array( $normalized['required'] ) ) { unset( $normalized['required'] ); } $type = isset( $normalized['type'] ) ? $normalized['type'] : ''; $is_object_schema = 'object' === $type || isset( $normalized['properties'] ); if ( $is_object_schema ) { if ( isset( $normalized['properties'] ) && is_array( $normalized['properties'] ) ) { $normalized['required'] = array_values( array_keys( $normalized['properties'] ) ); $normalized['additionalProperties'] = false; } elseif ( ! isset( $normalized['properties'] ) ) { $normalized['properties'] = new stdClass(); $normalized['required'] = array(); $normalized['additionalProperties'] = false; } elseif ( $normalized['properties'] instanceof stdClass ) { $normalized['required'] = array(); $normalized['additionalProperties'] = false; } } return $normalized; } /** * Prepare OpenAI tool arguments for WP_Ability::execute(). * * @param mixed $ability Ability object. * @param array $arguments Decoded OpenAI tool arguments. * @return mixed */ private static function prepare_input_for_ability( $ability, $arguments ) { $schema = method_exists( $ability, 'get_input_schema' ) ? $ability->get_input_schema() : array(); if ( empty( $schema ) ) { return null; } $type = isset( $schema['type'] ) ? $schema['type'] : ''; $is_object_schema = 'object' === $type || isset( $schema['properties'] ); if ( ! $is_object_schema ) { return array_key_exists( 'value', $arguments ) ? $arguments['value'] : null; } return $arguments; } }