'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' )
);
}
}