'object', 'properties' => array( 'comment_id' => array( 'type' => 'integer', 'description' => esc_html__( 'The ID of the comment to generate a reply suggestion for.', 'ai' ), ), 'tone' => array( 'type' => 'string', 'enum' => array( 'professional', 'friendly', 'casual' ), 'default' => 'friendly', 'description' => esc_html__( 'The tone for the reply suggestion.', 'ai' ), ), ), 'required' => array( 'comment_id' ), ); } /** * {@inheritDoc} * * @since 1.2.0 */ protected function output_schema(): array { return array( 'type' => 'string', 'description' => esc_html__( 'The generated reply suggestion.', 'ai' ), ); } /** * {@inheritDoc} * * @since 1.2.0 * * @return string|\WP_Error The generated reply suggestion. */ protected function execute_callback( $input ) { $input = wp_parse_args( (array) $input, array( 'comment_id' => 0, 'tone' => 'friendly', ) ); $comment_id = absint( $input['comment_id'] ); if ( ! $comment_id ) { return new WP_Error( 'missing_comment_id', esc_html__( 'A comment ID is required.', 'ai' ) ); } $comment = get_comment( $comment_id ); if ( ! $comment || ! is_a( $comment, '\WP_Comment' ) ) { return new WP_Error( 'comment_not_found', sprintf( /* translators: %d: Comment ID. */ esc_html__( 'Comment with ID %d not found.', 'ai' ), $comment_id ) ); } // Fetch post context. $post = get_post( (int) $comment->comment_post_ID ); if ( ! $post instanceof \WP_Post ) { return new WP_Error( 'post_not_found', esc_html__( 'Post not found.', 'ai' ) ); } $post_context = $this->get_post_context( $post ); $post_title = $post_context['title']; $post_excerpt = $post_context['excerpt']; $tone = in_array( $input['tone'], array( 'professional', 'friendly', 'casual' ), true ) ? $input['tone'] : 'friendly'; // Build the prompt context. $context = $this->build_context( $comment, $post_title, $post_excerpt, $tone ); $context = $this->filter_prompt( $context, $comment, $post_title, $post_excerpt, $tone ); // Generate the reply. $reply = $this->generate_reply( $context ); if ( is_wp_error( $reply ) ) { return $reply; } return $reply; } /** * {@inheritDoc} * * @since 1.2.0 */ protected function permission_callback( $input ) { if ( ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'insufficient_capabilities', esc_html__( 'You do not have permission to generate reply suggestions.', 'ai' ) ); } return true; } /** * {@inheritDoc} * * @since 1.2.0 */ protected function meta(): array { return array( 'show_in_rest' => true, ); } /** * Returns the parent post's title and excerpt for the prompt. * * @since 1.5.0 * * @param \WP_Post $post The post the comment was left on. * @return array{title: string, excerpt: string} The post title and excerpt, or empty strings. */ private function get_post_context( \WP_Post $post ): array { $shareable = is_post_publicly_readable( $post ); /** * Filters whether a post's title and excerpt may be sent as reply suggestion context. * * @since 1.5.0 * * @param bool $shareable Whether the post's title and excerpt are safe to share as context. * @param \WP_Post $post The post the comment was left on. */ if ( ! (bool) apply_filters( 'wpai_suggest_reply_post_context_shareable', $shareable, $post ) ) { return array( 'title' => '', 'excerpt' => '', ); } return array( 'title' => (string) $post->post_title, 'excerpt' => (string) get_the_excerpt( $post ), ); } /** * Builds the prompt context string from the comment and post data. * * @since 1.2.0 * * @param \WP_Comment $comment The comment to reply to. * @param string $post_title The title of the parent post. * @param string $post_excerpt A short excerpt of the parent post content. * @param string $tone The desired reply tone (e.g. 'friendly', 'professional'). * @return string The assembled prompt context. */ private function build_context( \WP_Comment $comment, string $post_title, string $post_excerpt, string $tone ): string { $parts = array(); if ( '' !== $post_title ) { $parts[] = sprintf( '%s', $post_title ); } if ( '' !== $post_excerpt ) { $parts[] = sprintf( '%s', $post_excerpt ); } $parts[] = sprintf( '%s', $comment->comment_author ); $parts[] = sprintf( '%s', $comment->comment_content ); $parts[] = sprintf( '%s', $tone ); return implode( "\n", $parts ); } /** * Generates a reply suggestion via the AI client. * * @since 1.2.0 * * @param string $context The assembled prompt context string. * @return string|\WP_Error The sanitized reply text, or a WP_Error on failure. */ private function generate_reply( string $context ) { $prompt_builder = $this->get_prompt_builder( $context ); if ( is_wp_error( $prompt_builder ) ) { return $prompt_builder; } $result = $prompt_builder->generate_text(); if ( is_wp_error( $result ) ) { return $result; } return sanitize_textarea_field( trim( (string) $result ) ); } /** * Gets a prompt builder for generating reply suggestions. * * @since 1.2.0 * * @param string $context The prompt context string. * @return \WP_AI_Client_Prompt_Builder|\WP_Error The prompt builder, or a WP_Error on failure. */ private function get_prompt_builder( string $context ) { $prompt_builder = wp_ai_client_prompt( $context ) ->using_system_instruction( $this->get_system_instruction() ); $prompt_builder = $this->filter_prompt_builder( $prompt_builder, Suggest_Reply_Experiment::class, array(), $context ); return $this->ensure_text_generation_supported( $prompt_builder, esc_html__( 'Reply suggestion could not be generated. Please ensure you have a connected provider that supports text generation.', 'ai' ) ); } }