<?php
/**
* Chrome AI integration
*/
namespace Classifai\Providers\Browser;
use Classifai\Features\ContentResizing;
use Classifai\Features\ExcerptGeneration;
use Classifai\Features\TitleGeneration;
use Classifai\Providers\Provider;
use Classifai\Providers\OpenAI\Tokenizer;
use Classifai\Normalizer;
use WP_Error;
use function Classifai\get_default_prompt;
class ChromeAI extends Provider {
/**
* Provider ID
*
* @var string
*/
const ID = 'chrome_ai';
/**
* Maximum number of tokens our model supports
*
* @var int
*/
protected $max_tokens = 6144;
/**
* ChromeAI constructor.
*
* @param \Classifai\Features\Feature $feature_instance The feature instance.
*/
public function __construct( $feature_instance = null ) {
$this->feature_instance = $feature_instance;
}
/**
* Render the provider fields.
*/
public function render_provider_fields() {
do_action( 'classifai_' . static::ID . '_render_provider_fields', $this );
}
/**
* Returns the default settings for this provider.
*
* @return array
*/
public function get_default_provider_settings(): array {
$common_settings = [
'authenticated' => true,
];
return $common_settings;
}
/**
* Sanitize the settings for this provider.
*
* @param array $new_settings The settings array.
* @return array
*/
public function sanitize_settings( array $new_settings ): array {
return $new_settings;
}
/**
* Common entry point for all REST endpoints for this provider.
*
* @param int $post_id The Post ID we're processing.
* @param string $route_to_call The route we are processing.
* @param array $args Optional arguments to pass to the route.
* @return string|WP_Error
*/
public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to generate titles.', 'classifai' ) );
}
$route_to_call = strtolower( $route_to_call );
$return = '';
// Handle all of our routes.
switch ( $route_to_call ) {
case 'excerpt':
$return = $this->generate_excerpt( $post_id, $args );
break;
case 'title':
$return = $this->generate_title( $post_id, $args );
break;
case 'resize_content':
$return = $this->resize_content( $post_id, $args );
break;
}
return $return;
}
/**
* Generate an excerpt.
*
* @param int $post_id The Post ID we're processing
* @param array $args Arguments passed in.
* @return string|WP_Error
*/
public function generate_excerpt( int $post_id = 0, array $args = [] ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to generate an excerpt.', 'classifai' ) );
}
$feature = new ExcerptGeneration();
$settings = $feature->get_settings();
$args = wp_parse_args(
array_filter( $args ),
[
'content' => '',
'title' => get_the_title( $post_id ),
]
);
// These checks (and the one above) happen in the REST permission_callback,
// but we run them again here in case this method is called directly.
if ( empty( $settings ) || ! $feature->is_feature_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Excerpt generation is disabled or authentication failed. Please check your settings.', 'classifai' ) );
}
$excerpt_length = absint( $settings['length'] ?? 55 );
$excerpt_prompt = esc_textarea( get_default_prompt( $settings['generate_excerpt_prompt'] ) ?? $feature->prompt );
// Replace our variables in the prompt.
$prompt_search = array( '{{WORDS}}', '{{TITLE}}' );
$prompt_replace = array( $excerpt_length, $args['title'] );
$prompt = str_replace( $prompt_search, $prompt_replace, $excerpt_prompt );
/**
* Filter the prompt we will send to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_excerpt_prompt
*
* @param {string} $prompt Prompt we are sending. Gets added before post content.
* @param {int} $post_id ID of post we are summarizing.
* @param {int} $excerpt_length Length of final excerpt.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chrome_ai_excerpt_prompt', $prompt, $post_id, $excerpt_length );
/**
* Filter the request body before sending to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_excerpt_request_body
*
* @param {array} $body Request body that will be sent.
* @param {int} $post_id ID of post we are summarizing.
*
* @return {array} Request body.
*/
$body = apply_filters(
'classifai_chrome_ai_excerpt_request_body',
[
'prompt' => 'You will be provided with content delimited by triple quotes. ' . $prompt,
'content' => $this->get_content( $post_id, $excerpt_length, false, $args['content'] ),
'func' => static::ID,
],
$post_id
);
return $body;
}
/**
* Generate a title using Chrome AI.
*
* @param int $post_id The Post Id we're processing
* @param array $args Arguments passed in.
* @return string|WP_Error
*/
public function generate_title( int $post_id = 0, array $args = [] ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'Post ID is required to generate titles.', 'classifai' ) );
}
$feature = new TitleGeneration();
$settings = $feature->get_settings();
$args = wp_parse_args(
array_filter( $args ),
[
'content' => '',
]
);
// These checks happen in the REST permission_callback,
// but we run them again here in case this method is called directly.
if ( empty( $settings ) || ! $feature->is_feature_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Title generation is disabled or authentication failed. Please check your settings.', 'classifai' ) );
}
$prompt = esc_textarea( get_default_prompt( $settings['generate_title_prompt'] ) ?? $feature->prompt );
/**
* Filter the prompt we will send to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_title_prompt
*
* @param {string} $prompt Prompt we are sending. Gets added before post content.
* @param {int} $post_id ID of post we are summarizing.
* @param {array} $args Arguments passed to endpoint.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chrome_ai_title_prompt', $prompt, $post_id, $args );
/**
* Filter the request body before sending to Azure OpenAI.
*
* @since x.x.x
* @hook classifai_chrome_ai_title_request_body
*
* @param {array} $body Request body that will be sent.
* @param {int} $post_id ID of post we are summarizing.
*
* @return {array} Request body.
*/
$body = apply_filters(
'classifai_chrome_ai_title_request_body',
[
'prompt' => 'You will be provided with content delimited by triple quotes. ' . $prompt,
'content' => $this->get_content( $post_id, 15, false, $args['content'] ),
'func' => static::ID,
],
$post_id
);
return $body;
}
/**
* Resizes content.
*
* @param int $post_id The Post ID we're processing
* @param array $args Arguments passed in.
* @return string|WP_Error
*/
public function resize_content( int $post_id, array $args = array() ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'Post ID is required to resize content.', 'classifai' ) );
}
$feature = new ContentResizing();
$settings = $feature->get_settings();
if ( 'shrink' === $args['resize_type'] ) {
$prompt = esc_textarea( get_default_prompt( $settings['condense_text_prompt'] ) ?? $feature->condense_prompt );
} else {
$prompt = esc_textarea( get_default_prompt( $settings['expand_text_prompt'] ) ?? $feature->expand_prompt );
}
/**
* Filter the resize prompt we will send to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_' . $args['resize_type'] . '_content_prompt
*
* @param {string} $prompt Resize prompt we are sending. Gets added as a system prompt.
* @param {int} $post_id ID of post.
* @param {array} $args Arguments passed to endpoint.
*
* @return {string} Prompt.
*/
$prompt = apply_filters( 'classifai_chrome_ai_' . $args['resize_type'] . '_content_prompt', $prompt, $post_id, $args );
/**
* Filter the resize request body before sending to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_resize_content_request_body
*
* @param {array} $body Request body that will be sent.
* @param {int} $post_id ID of post.
*
* @return {array} Request body.
*/
$body = apply_filters(
'classifai_chrome_ai_resize_content_request_body',
[
'prompt' => 'You will be provided with content delimited by triple quotes. ' . $prompt,
'content' => esc_html( $args['content'] ),
'func' => static::ID,
],
$post_id
);
return $body;
}
/**
* Get our content, trimming if needed.
*
* @param int $post_id Post ID to get content from.
* @param int $return_length Word length of returned content.
* @param bool $use_title Whether to use the title or not.
* @param string $post_content The post content.
* @return string
*/
public function get_content( int $post_id = 0, int $return_length = 0, bool $use_title = true, string $post_content = '' ): string {
$tokenizer = new Tokenizer( $this->max_tokens );
$normalizer = new Normalizer();
/**
* We first determine how many tokens, roughly, our returned content will require.
* This is determined by the number of words we expect to be returned and how
* many tokens are in an average word.
*/
$return_tokens = $tokenizer->tokens_in_words( $return_length );
/**
* We then subtract those tokens from the max number of tokens ChromeAI allows
* in a single request, as well as subtracting out the number of tokens in our
* prompt (~50). ChromeAI counts both the tokens in the request and in
* the response towards the max.
*/
$max_content_tokens = $this->max_tokens - $return_tokens - 50;
if ( empty( $post_content ) ) {
$post = get_post( $post_id );
$post_content = apply_filters( 'the_content', $post->post_content );
}
$post_content = preg_replace( '#\[.+\](.+)\[/.+\]#', '$1', $post_content );
// Then trim our content, if needed, to stay under the max.
if ( $use_title ) {
$content = $tokenizer->trim_content(
$normalizer->normalize( $post_id, $post_content ),
(int) $max_content_tokens
);
} else {
$content = $tokenizer->trim_content(
$normalizer->normalize_content( $post_content, '', $post_id ),
(int) $max_content_tokens
);
}
/**
* Filter content that will get sent to Chrome AI.
*
* @since x.x.x
* @hook classifai_chrome_ai_content
*
* @param {string} $content Content that will be sent.
* @param {int} $post_id ID of post we are summarizing.
*
* @return {string} Content.
*/
return apply_filters( 'classifai_chrome_ai_content', $content, $post_id );
}
}