Skip to content
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

Add Chrome AI as a Provider for our text generation Features #819

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions includes/Classifai/Features/ContentResizing.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Classifai\Providers\Azure\OpenAI;
use Classifai\Providers\GoogleAI\GeminiAPI;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Providers\Browser\ChromeAI;
use Classifai\Services\LanguageProcessing;
use WP_REST_Server;
use WP_REST_Request;
Expand Down Expand Up @@ -52,6 +53,7 @@ public function __construct() {
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ),
OpenAI::ID => __( 'Azure OpenAI', 'classifai' ),
ChromeAI::ID => __( 'Chrome AI', 'classifai' ),
];
}

Expand Down
2 changes: 2 additions & 0 deletions includes/Classifai/Features/ExcerptGeneration.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Classifai\Providers\GoogleAI\GeminiAPI;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Providers\Azure\OpenAI;
use Classifai\Providers\Browser\ChromeAI;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;
Expand Down Expand Up @@ -45,6 +46,7 @@ public function __construct() {
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ),
OpenAI::ID => __( 'Azure OpenAI', 'classifai' ),
ChromeAI::ID => __( 'Chrome AI', 'classifai' ), // TODO: only add this Provider if the browser supports AI.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this Provider shows up even if your browser doesn't support Chrome AI. Would be great to remove it but doesn't cause any problems if you do select it. With the settings refactor in #502, I think will be much easier to remove this since all settings are registered in javascript at that point, so may leave this until it's integrated in there

];
}

Expand Down
2 changes: 2 additions & 0 deletions includes/Classifai/Features/TitleGeneration.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Classifai\Providers\Azure\OpenAI;
use Classifai\Providers\GoogleAI\GeminiAPI;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Providers\Browser\ChromeAI;
use Classifai\Services\LanguageProcessing;
use WP_REST_Server;
use WP_REST_Request;
Expand Down Expand Up @@ -45,6 +46,7 @@ public function __construct() {
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ),
OpenAI::ID => __( 'Azure OpenAI', 'classifai' ),
ChromeAI::ID => __( 'Chrome AI', 'classifai' ),
];
}

Expand Down
339 changes: 339 additions & 0 deletions includes/Classifai/Providers/Browser/ChromeAI.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
<?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\Normalizer;
use WP_Error;

use function Classifai\get_default_prompt;

class ChromeAI extends Provider {

/**
* Provider ID
*
* @var string
*/
const ID = 'chrome_ai';

/**
* 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, 0, 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.
*
* We don't trim content here as we don't know for sure which model
* someone is using.
*
* @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 {
$normalizer = new Normalizer();

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

// Add the title to the content, if needed, and normalize things.
if ( $use_title ) {
$content = $normalizer->normalize( $post_id, $post_content );
} else {
$content = $normalizer->normalize_content( $post_content, '', $post_id );
}

/**
* 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 );
}
}
1 change: 1 addition & 0 deletions includes/Classifai/Services/LanguageProcessing.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static function get_service_providers(): array {
'Classifai\Providers\Azure\OpenAI',
'Classifai\Providers\AWS\AmazonPolly',
'Classifai\Providers\Azure\Embeddings',
'Classifai\Providers\Browser\ChromeAI',
]
);
}
Expand Down
Loading
Loading