diff --git a/README.md b/README.md index 12bdfc83d..40636a4cf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ *You can learn more about ClassifAI's features at [ClassifAIPlugin.com](https://classifaiplugin.com/) and documentation at the [ClassifAI documentation site](https://10up.github.io/classifai/).* ## Table of Contents + * [Overview](#overview) * [Features](#features) * [Requirements](#requirements) @@ -17,6 +18,7 @@ * [Register ClassifAI account](#register-classifai-account) * [Set Up IBM Watson NLU Language Processing](#set-up-language-processing-via-ibm-watson) * [Set Up OpenAI ChatGPT Language Processing](#set-up-language-processing-via-openai-chatgpt) +* [Set Up Google AI (Gemini API) Language Processing](#set-up-language-processing-via-google-ai-gemini-api) * [Set Up OpenAI Embeddings Language Processing](#set-up-language-processing-via-openai-embeddings) * [Set Up OpenAI Whisper Language Processing](#set-up-language-processing-via-openai-whisper) * [Set Up Azure AI Language Processing](#set-up-language-processing-via-microsoft-azure) @@ -36,9 +38,9 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro ## Features -* Generate a summary of post content and store it as an excerpt using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) -* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) -* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) +* Generate a summary of post content and store it as an excerpt using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) +* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) +* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E API](https://platform.openai.com/docs/guides/images) * Generate transcripts of audio files using [OpenAI's Whisper API](https://platform.openai.com/docs/guides/speech-to-text) * Moderate incoming comments for sensitive content using [OpenAI's Moderation API](https://platform.openai.com/docs/guides/moderation) @@ -68,24 +70,27 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro ## Requirements * PHP 7.4+ -* [WordPress](http://wordpress.org) 5.7+ +* [WordPress](http://wordpress.org) 6.1+ * To utilize the NLU Language Processing functionality, you will need an active [IBM Watson](https://cloud.ibm.com/registration) account. * To utilize the ChatGPT, Embeddings, or Whisper Language Processing functionality or DALL·E Image Processing functionality, you will need an active [OpenAI](https://platform.openai.com/signup) account. * To utilize the Azure AI Vision Image Processing functionality or Text to Speech Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account. +* To utilize the Google Gemini Language Processing functionality, you will need an active [Google Gemini](https://ai.google.dev/tutorials/setup) account. ## Pricing Note that there is no cost to using ClassifAI itself. Both IBM Watson and Microsoft Azure have free plans for their AI services, but above those free plans there are paid levels as well. So if you expect to process a high volume of content, then you'll want to review the pricing plans for these services to understand if you'll incur any costs. For the most part, both services' free plans are quite generous and should at least allow for testing ClassifAI to better understand its featureset and could at best allow for totally free usage. OpenAI has a limited trial option that can be used for testing but will require a valid paid plan after that. -The service that powers ClassifAI's NLU Language Processing, IBM Watson's Natural Language Understanding ("NLU"), has a ["lite" pricing tier](https://www.ibm.com/cloud/watson-natural-language-understanding/pricing) that offers 30,000 free NLU items per month. +IBM Watson's Natural Language Understanding ("NLU"), which is one of the providers that powers the classification feature, has a ["lite" pricing tier](https://www.ibm.com/cloud/watson-natural-language-understanding/pricing) that offers 30,000 free NLU items per month. + +OpenAI, which is one of the providers that powers the classification, title generation, excerpt generation, content resizing, audio transcripts generation, moderation and image generation features, has a limited free trial and then requires a [pay per usage](https://openai.com/pricing) plan. -The service that powers ClassifAI's ChatGPT, Embeddings and Whisper Language Processing and DALL·E Image Processing, OpenAI, has a limited free trial and then requires a [pay per usage](https://openai.com/pricing) plan. +Microsoft Azure AI Vision, which is one of the providers that powers the descriptive text generator, image tags generator, image cropping, image text extraction and PDF text extraction features, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/computer-vision/) that offers 20 transactions per minute and 5,000 transactions per month. -The service that powers ClassifAI's Azure AI Vision Image Processing, Microsoft Azure, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/computer-vision/) that offers 20 transactions per minute and 5,000 transactions per month. +Microsoft Azure AI Speech, which is one of the providers that powers the text to speech feature, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/speech-services/) that offers 0.5 million characters per month. -The service that powers ClassifAI's Text to Speech Language Processing, Microsoft Azure, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/speech-services/) that offers 0.5 million characters per month. +Microsoft Azure AI Personalizer, which is one of the providers that powers the recommended content feature, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/personalizer/) that offers 50,000 transactions per month. -The service that powers ClassifAI's Recommended Content, Microsoft Azure AI Personalizer, has a ["free" pricing tier](https://azure.microsoft.com/en-us/pricing/details/cognitive-services/personalizer/) that offers 50,000 transactions per month. +Google Gemini, which is one of the providers that powers the title generation, excerpt generation and content resizing features, has a ["free" pricing tier](https://ai.google.dev/pricing) that offers 60 queries per minute. ## Installation @@ -139,7 +144,7 @@ Finally, require the plugin, using the version number you specified in the previ ```json "require": { - "10up/classifai": "2.0.0" + "10up/classifai": "3.0.0" } ``` @@ -164,7 +169,7 @@ ClassifAI is a sophisticated solution that we want organizations of all shapes a ![Screenshot of registration settings](assets/img/screenshot-6.png "Example of an empty ClassifAI Settings registration screen.") -## Set Up Language Processing (via IBM Watson) +## Set Up Classification (via IBM Watson) ### 1. Sign up for Watson services @@ -173,7 +178,9 @@ ClassifAI is a sophisticated solution that we want organizations of all shapes a - Log into your account (accepting the privacy policy) and create a new [*Natural Language Understanding*](https://cloud.ibm.com/catalog/services/natural-language-understanding) Resource if you do not already have one. It may take a minute for your account to fully populate with the default resource group to use. - Click `Manage` in the left hand menu, then `Show credentials` on the Manage page to view the credentials for this resource. -### 2. Configure IBM Watson API Keys under Tools > ClassifAI > Language Processing > IBM Watson +### 2. Configure IBM Watson API Keys under Tools > ClassifAI > Language Processing > Classification + +- Select **IBM Watson NLU** in the provider dropdown. **The credentials screen will show either an API key or a username/password combination.** @@ -198,14 +205,14 @@ For more information, see https://cloud.ibm.com/docs/watson?topic=watson-endpoin IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#categories), [Keywords](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#keywords), [Concepts](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#concepts) & [Entities](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#entities) can each be stored in existing WordPress taxonomies or a custom Watson taxonomy. -### 3. Configure Post Types to classify and IBM Watson Features to enable under ClassifAI > Language Processing > IBM Watson +### 3. Configure Post Types to classify and IBM Watson Features to enable under ClassifAI > Language Processing > Classification - Choose which public post types to classify when saved. - Choose whether to assign category, keyword, entity, and concept as well as the thresholds and taxonomies used for each. ### 4. Save a Post/Page/CPT or run WP CLI command to batch classify your content -## Set Up Language Processing (via OpenAI ChatGPT) +## Set Up Language Processing Features (via OpenAI ChatGPT) ### 1. Sign up for OpenAI @@ -214,21 +221,52 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand * Log into your account and go to the [API key page](https://platform.openai.com/account/api-keys). * Click `Create new secret key` and copy the key that is shown. -### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > OpenAI ChatGPT +### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > Title Generation, Excerpt Generation or Content Resizing +* Select **OpenAI ChatGPT** in the provider dropdown. +* Enter your API Key copied from the above step into the `API Key` field. + +### 3. Enable specific Language Processing feature settings + +* For each feature, set any options as needed. +* Save changes and ensure a success message is shown. An error will show if API authentication fails. + +### 4. Edit a content type to test enabled features + +* To test excerpt generation, edit (or create) an item that supports excerpts. Note: only the block editor is supported. +* Ensure this item has content saved. +* Open the Excerpt panel in the sidebar and click on `Generate Excerpt`. +* To test title generation, edit (or create) an item that supports titles. +* Ensure this item has content saved. +* Open the Summary panel in the sidebar and click on `Generate titles`. +* To test content resizing, edit (or create) an item. Note: only the block editor is supported. +* Add a paragraph block with some content. +* With this block selected, select the AI icon in the toolbar and choose to either expand or condense the text. +* In the modal that pops up, select one of the options. + +## Set Up Language Processing Features (via Google AI (Gemini API)) + +### 1. Sign up for Google AI + +* [Sign up for a Google account](https://www.google.com/) or sign into your existing one. +* Go to [Google AI Gemini](https://ai.google.dev/) website and click on the Get API key button or go to the [API key page](https://makersuite.google.com/app/apikey) directly. +* Note that if this page doesn't work, it's likely that Gemini is not enabled in your workspace. Contact your workspace administrator to get this enabled. +* Click `Create API key` and copy the key that is shown. + +### 2. Configure API Keys under Tools > ClassifAI > Language Processing > Title Generation, Excerpt Generation or Content Resizing + +* Select **Google AI (Gemini API)** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable specific Language Processing features -* Choose to add the ability to generate excerpts. -* Choose to add the ability to generate titles. -* Choose to add the ability to resize content. +* Check the "Enable" checkbox in above screen. * Set the other options as needed. * Save changes and ensure a success message is shown. An error will show if API authentication fails. ### 4. Edit a content type to test enabled features -* To test excerpt generation, edit (or create) an item that supports excerpts. Note: only the block editor is supported. +* To test excerpt generation, edit (or create) an item that supports excerpts. * Ensure this item has content saved. * Open the Excerpt panel in the sidebar and click on `Generate Excerpt`. * To test title generation, edit (or create) an item that supports titles. @@ -239,7 +277,7 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand * With this block selected, select the AI icon in the toolbar and choose to either expand or condense the text. * In the modal that pops up, select one of the options. -## Set Up Language Processing (via OpenAI Embeddings) +## Set Up Classification (via OpenAI Embeddings) ### 1. Sign up for OpenAI @@ -248,8 +286,9 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand * Log into your account and go to the [API key page](https://platform.openai.com/account/api-keys). * Click `Create new secret key` and copy the key that is shown. -### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > OpenAI Embeddings +### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > Classification +* Select **OpenAI Embeddings** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable specific Language Processing features @@ -264,7 +303,7 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand * Create a new piece of content that matches the post type and post status chosen in settings. * Open the taxonomy panel in the sidebar and see terms that were auto-applied. -## Set Up Language Processing (via OpenAI Whisper) +## Set Up Audio Transcripts Generation (via OpenAI Whisper) Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can create a transcript for audio files that meet the following requirements: * The file must be presented in mp3, mp4, mpeg, mpga, m4a, wav, or webm format @@ -277,8 +316,9 @@ Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can c * Log into your account and go to the [API key page](https://platform.openai.com/account/api-keys). * Click `Create new secret key` and copy the key that is shown. -### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > OpenAI Whisper +### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > Audio Transcripts Generation +* Select **OpenAI Embeddings** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable specific features @@ -292,7 +332,7 @@ Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can c * Upload a new audio file. * Check to make sure the transcript was stored in the Description field. -## Set Up Language Processing (via Microsoft Azure) +## Set Up Text to Speech (via Microsoft Azure) ### 1. Sign up for Azure services @@ -301,8 +341,9 @@ Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can c * Click `Keys and Endpoint` in the left hand Resource Management menu to view the `Location/Region` for this resource. * Click the copy icon next to `KEY 1` to copy the API Key credential for this resource. -### 2. Configure Microsoft Azure API and Key under Tools > ClassifAI > Language Processing > Microsoft Azure +### 2. Configure Microsoft Azure API and Key under Tools > ClassifAI > Language Processing > Text to Speech +* Select **Microsoft Azure AI Speech** in the provider dropdown. * In the `Endpoint URL` field, enter the following URL, replacing `LOCATION` with the `Location/Region` you found above: `https://LOCATION.tts.speech.microsoft.com/`. * In the `API Key` field, enter your `KEY 1` copied from above. * Click **Save Changes** (the page will reload). @@ -317,7 +358,7 @@ Note that [OpenAI](https://platform.openai.com/docs/guides/speech-to-text) can c * Click the button to preview the generated speech audio for the post. * View the post on the front-end and see a read-to-me feature has been added -## Set Up Image Processing (via Microsoft Azure) +## Set Up Image Processing features (via Microsoft Azure) Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-services/computer-vision/home#image-requirements) can analyze and crop images that meet the following requirements: - The image must be presented in JPEG, PNG, GIF, or BMP format @@ -332,20 +373,20 @@ Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-ser - Click `Keys and Endpoint` in the left hand Resource Management menu to view the `Endpoint` URL for this resource. - Click the copy icon next to `KEY 1` to copy the API Key credential for this resource. -### 2. Configure Microsoft Azure API and Key under Tools > ClassifAI > Image Processing +### 2. Configure Microsoft Azure API and Key under Tools > ClassifAI > Image Processing > Descriptive Text Generator, Image Tags Generator, Image Cropping, Image Text Extraction or PDF Text Extraction +- Select **Microsoft Azure AI Vision** in the provider dropdown. - In the `Endpoint URL` field, enter your `API endpoint`. - In the `API Key` field, enter your `KEY 1`. -### 3. Enable specific Image Processing features +### 3. Configure specific Image Processing features -- Choose to `Generate descriptive text`, `Tag images`, `Enable smart cropping`, and/or `Scan image or PDF for text`. -- For features that have thresholds or taxonomy settings, set those as well. +- For features that have thresholds or taxonomy settings, set those as needed. - Image tagging uses Azure's [Describe Image](https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/56f91f2e778daf14a499e1fe) ### 4. Save Image or PDF file or run WP CLI command to batch classify your content -## Set Up Image Processing (via OpenAI) +## Set Up Image Generation (via OpenAI) ### 1. Sign up for OpenAI @@ -354,8 +395,9 @@ Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-ser * Log into your account and go to the [API key page](https://platform.openai.com/account/api-keys). * Click `Create new secret key` and copy the key that is shown. -### 2. Configure OpenAI API Keys under Tools > ClassifAI > Image Processing > OpenAI +### 2. Configure OpenAI API Keys under Tools > ClassifAI > Image Processing > Image Generation +* Select **OpenAI DALL-E** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable specific Image Processing features @@ -384,6 +426,7 @@ Note that [Azure AI Vision](https://docs.microsoft.com/en-us/azure/cognitive-ser ### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > Moderation +* Select **OpenAI Moderation** in the provider dropdown. * Enter your API Key copied from the above step into the `API Key` field. ### 3. Enable Comment Moderation @@ -414,7 +457,7 @@ For more information, see https://docs.microsoft.com/en-us/azure/cognitive-servi - In the `Endpoint URL` field, enter your `Endpoint` URL from Step 1 above. - In the `API Key` field, enter your `KEY 1` from Step 1 above. -### 3. Use "Recommended Content" block to display recommended content on your website. +### 3. Use "Recommended Content" block to display recommended content on your website ## WP CLI Commands @@ -430,9 +473,9 @@ ClassifAI connects your WordPress site directly to your account with specific se [Categories](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#categories) are five levels of hierarchies that IBM Watson can identify from your text. [Keywords](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#keywords) are specific terms from your text that IBM Watson is able to identify. [Concepts](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#concepts) are high-level concepts that are not necessarily directly referenced in your text. [Entities](https://cloud.ibm.com/docs/natural-language-understanding?topic=natural-language-understanding-about#entities) are people, companies, locations, and classifications that are made by IBM Watson from your text. -### How can I view the taxonomies that are generated from the NLU Language Processing? +### How can I view the taxonomies that are generated from the NLU classification? -Whatever options you have selected in the Category, Keyword, Entity, and Concept taxonomy dropdowns in the NLU Language Processing settings can be viewed within Classic Editor metaboxes and the Block Editor side panel. They can also be viewed in the All Posts and All Pages table list views by utilizing the Screen Options to enable those columns if they're not already appearing in your table list view. +Whatever options you have selected in the Category, Keyword, Entity, and Concept taxonomy dropdowns in the NLU classification settings can be viewed within Classic Editor metaboxes and the Block Editor side panel. They can also be viewed in the All Posts and All Pages table list views by utilizing the Screen Options to enable those columns if they're not already appearing in your table list view. ### Should I alert my site's users that AI tools are being used? diff --git a/includes/Classifai/Features/ContentResizing.php b/includes/Classifai/Features/ContentResizing.php index ed0e5303a..7f186a9fa 100644 --- a/includes/Classifai/Features/ContentResizing.php +++ b/includes/Classifai/Features/ContentResizing.php @@ -2,6 +2,7 @@ namespace Classifai\Features; +use Classifai\Providers\GoogleAI\GeminiAPI; use Classifai\Providers\OpenAI\ChatGPT; use Classifai\Services\LanguageProcessing; use WP_REST_Server; @@ -48,7 +49,8 @@ public function __construct() { // Contains just the providers this feature supports. $this->supported_providers = [ - ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ), ]; } @@ -234,22 +236,6 @@ public function get_enable_description(): string { public function add_custom_settings_fields() { $settings = $this->get_settings(); - add_settings_field( - 'number_of_suggestions', - esc_html__( 'Number of suggestions', 'classifai' ), - [ $this, 'render_input' ], - $this->get_option_name(), - $this->get_option_name() . '_section', - [ - 'label_for' => 'number_of_suggestions', - 'input_type' => 'number', - 'min' => 1, - 'step' => 1, - 'default_value' => $settings['number_of_suggestions'], - 'description' => esc_html__( 'Number of suggestions that will be generated in one request.', 'classifai' ), - ] - ); - add_settings_field( 'condense_text_prompt', esc_html__( 'Condense text prompt', 'classifai' ), @@ -286,22 +272,21 @@ public function add_custom_settings_fields() { */ public function get_feature_default_settings(): array { return [ - 'number_of_suggestions' => 1, - 'condense_text_prompt' => [ + 'condense_text_prompt' => [ [ 'title' => esc_html__( 'ClassifAI default', 'classifai' ), 'prompt' => $this->condense_prompt, 'original' => 1, ], ], - 'expand_text_prompt' => [ + 'expand_text_prompt' => [ [ 'title' => esc_html__( 'ClassifAI default', 'classifai' ), 'prompt' => $this->expand_prompt, 'original' => 1, ], ], - 'provider' => ChatGPT::ID, + 'provider' => ChatGPT::ID, ]; } @@ -314,9 +299,8 @@ public function get_feature_default_settings(): array { public function sanitize_default_feature_settings( array $new_settings ): array { $settings = $this->get_settings(); - $new_settings['number_of_suggestions'] = sanitize_number_of_responses_field( 'number_of_suggestions', $new_settings, $settings ); - $new_settings['condense_text_prompt'] = sanitize_prompts( 'condense_text_prompt', $new_settings ); - $new_settings['expand_text_prompt'] = sanitize_prompts( 'expand_text_prompt', $new_settings ); + $new_settings['condense_text_prompt'] = sanitize_prompts( 'condense_text_prompt', $new_settings ); + $new_settings['expand_text_prompt'] = sanitize_prompts( 'expand_text_prompt', $new_settings ); return $new_settings; } diff --git a/includes/Classifai/Features/ExcerptGeneration.php b/includes/Classifai/Features/ExcerptGeneration.php index 2add6f960..c08d3278e 100644 --- a/includes/Classifai/Features/ExcerptGeneration.php +++ b/includes/Classifai/Features/ExcerptGeneration.php @@ -2,6 +2,7 @@ namespace Classifai\Features; +use Classifai\Providers\GoogleAI\GeminiAPI; use Classifai\Services\LanguageProcessing; use Classifai\Providers\OpenAI\ChatGPT; use WP_REST_Server; @@ -40,7 +41,8 @@ public function __construct() { // Contains just the providers this feature supports. $this->supported_providers = [ - ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ), ]; } @@ -263,7 +265,7 @@ public function enqueue_admin_assets( string $hook_suffix ) { * @return string */ public function get_enable_description(): string { - return esc_html__( 'A button will be added to the status panel that can be used to generate titles.', 'classifai' ); + return esc_html__( 'A button will be added to the excerpt panel that can be used to generate an excerpt.', 'classifai' ); } /** @@ -320,7 +322,7 @@ public function add_custom_settings_fields() { 'min' => 1, 'step' => 1, 'default_value' => $settings['length'], - 'description' => __( 'How many words should the excerpt be? Note that the final result may not exactly match this. In testing, ChatGPT tended to exceed this number by 10-15 words.', 'classifai' ), + 'description' => __( 'How many words should the excerpt be? Note that the final result may not exactly match this, it often tends to exceed this number by 10-15 words.', 'classifai' ), ] ); } diff --git a/includes/Classifai/Features/TitleGeneration.php b/includes/Classifai/Features/TitleGeneration.php index b43a3ccf8..6c88394c3 100644 --- a/includes/Classifai/Features/TitleGeneration.php +++ b/includes/Classifai/Features/TitleGeneration.php @@ -2,6 +2,7 @@ namespace Classifai\Features; +use Classifai\Providers\GoogleAI\GeminiAPI; use Classifai\Services\LanguageProcessing; use Classifai\Providers\OpenAI\ChatGPT; use WP_REST_Server; @@ -9,7 +10,6 @@ use WP_Error; use function Classifai\sanitize_prompts; -use function Classifai\sanitize_number_of_responses_field; use function Classifai\get_asset_info; /** @@ -41,7 +41,8 @@ public function __construct() { // Contains just the providers this feature supports. $this->supported_providers = [ - ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ), + GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ), ]; } @@ -315,22 +316,6 @@ public function get_enable_description(): string { public function add_custom_settings_fields() { $settings = $this->get_settings(); - add_settings_field( - 'number_of_titles', - esc_html__( 'Number of titles', 'classifai' ), - [ $this, 'render_input' ], - $this->get_option_name(), - $this->get_option_name() . '_section', - [ - 'label_for' => 'number_of_titles', - 'input_type' => 'number', - 'min' => 1, - 'step' => 1, - 'default_value' => $settings['number_of_titles'], - 'description' => esc_html__( 'Number of titles that will be generated in one request.', 'classifai' ), - ] - ); - add_settings_field( 'generate_title_prompt', esc_html__( 'Prompt', 'classifai' ), @@ -353,7 +338,6 @@ public function add_custom_settings_fields() { */ public function get_feature_default_settings(): array { return [ - 'number_of_titles' => 1, 'generate_title_prompt' => [ [ 'title' => esc_html__( 'ClassifAI default', 'classifai' ), @@ -372,9 +356,6 @@ public function get_feature_default_settings(): array { * @return array */ public function sanitize_default_feature_settings( array $new_settings ): array { - $settings = $this->get_settings(); - - $new_settings['number_of_titles'] = sanitize_number_of_responses_field( 'number_of_titles', $new_settings, $settings ); $new_settings['generate_title_prompt'] = sanitize_prompts( 'generate_title_prompt', $new_settings ); return $new_settings; diff --git a/includes/Classifai/Providers/GoogleAI/APIRequest.php b/includes/Classifai/Providers/GoogleAI/APIRequest.php new file mode 100644 index 000000000..5c046eb82 --- /dev/null +++ b/includes/Classifai/Providers/GoogleAI/APIRequest.php @@ -0,0 +1,236 @@ +post( $googleai_url, $options ); + */ +class APIRequest { + + /** + * The Google AI API key. + * + * @var string + */ + public $api_key; + + /** + * The feature name. + * + * @var string + */ + public $feature; + + /** + * Google AI APIRequest constructor. + * + * @param string $api_key Google AI API key. + * @param string $feature Feature name. + */ + public function __construct( string $api_key = '', string $feature = '' ) { + $this->api_key = $api_key; + $this->feature = $feature; + } + + /** + * Makes an authorized GET request. + * + * @param string $url The Google AI API url + * @param array $options Additional query params + * @return array|WP_Error + */ + public function get( string $url, array $options = [] ) { + /** + * Filter the URL for the get request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_request_get_url + * + * @param {string} $url The URL for the request. + * @param {array} $options The options for the request. + * @param {string} $this->feature The feature name. + * + * @return {string} The URL for the request. + */ + $url = apply_filters( 'classifai_googleai_api_request_get_url', $url, $options, $this->feature ); + + /** + * Filter the options for the get request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_request_get_options + * + * @param {array} $options The options for the request. + * @param {string} $url The URL for the request. + * @param {string} $this->feature The feature name. + * + * @return {array} The options for the request. + */ + $options = apply_filters( 'classifai_googleai_api_request_get_options', $options, $url, $this->feature ); + + $this->add_headers( $options ); + + /** + * Filter the response from Google AI for a get request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_response_get + * + * @param {string} $url Request URL. + * @param {array} $options Request body options. + * @param {string} $this->feature Feature name. + * + * @return {array} API response. + */ + return apply_filters( + 'classifai_googleai_api_response_get', + $this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + $url, + $options, + $this->feature + ); + } + + /** + * Makes an authorized POST request. + * + * @param string $url The Google AI API URL. + * @param array $options Additional query params. + * @return array|WP_Error + */ + public function post( string $url = '', array $options = [] ) { + $options = wp_parse_args( + $options, + [ + 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ] + ); + + /** + * Filter the URL for the post request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_request_post_url + * + * @param {string} $url The URL for the request. + * @param {array} $options The options for the request. + * @param {string} $this->feature The feature name. + * + * @return {string} The URL for the request. + */ + $url = apply_filters( 'classifai_googleai_api_request_post_url', $url, $options, $this->feature ); + + /** + * Filter the options for the post request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_request_post_options + * + * @param {array} $options The options for the request. + * @param {string} $url The URL for the request. + * @param {string} $this->feature The feature name. + * + * @return {array} The options for the request. + */ + $options = apply_filters( 'classifai_googleai_api_request_post_options', $options, $url, $this->feature ); + + $this->add_headers( $options ); + + /** + * Filter the response from Google AI for a post request. + * + * @since 3.0.0 + * @hook classifai_googleai_api_response_post + * + * @param {string} $url Request URL. + * @param {array} $options Request body options. + * @param {string} $this->feature Feature name. + * + * @return {array} API response. + */ + return apply_filters( + 'classifai_googleai_api_response_post', + $this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + $url, + $options, + $this->feature + ); + } + + /** + * Get results from the response. + * + * @param object $response The API response. + * @return array|WP_Error + */ + public function get_result( $response ) { + if ( ! is_wp_error( $response ) ) { + $body = wp_remote_retrieve_body( $response ); + $code = wp_remote_retrieve_response_code( $response ); + $json = json_decode( $body, true ); + + if ( json_last_error() === JSON_ERROR_NONE ) { + if ( empty( $json['error'] ) ) { + return $json; + } else { + $message = $json['error']['message'] ?? esc_html__( 'An error occured', 'classifai' ); + return new WP_Error( $code, $message ); + } + } elseif ( ! empty( wp_remote_retrieve_response_message( $response ) ) ) { + return new WP_Error( $code, wp_remote_retrieve_response_message( $response ) ); + } else { + return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); + } + } else { + return $response; + } + } + + /** + * Add the headers. + * + * @param array $options The header options, passed by reference. + */ + public function add_headers( array &$options = [] ) { + if ( empty( $options['headers'] ) ) { + $options['headers'] = []; + } + + if ( ! isset( $options['headers']['x-goog-api-key'] ) ) { + $options['headers']['x-goog-api-key'] = $this->get_auth_header(); + } + + if ( ! isset( $options['headers']['Content-Type'] ) ) { + $options['headers']['Content-Type'] = 'application/json'; + } + } + + /** + * Get the auth header. + * + * @return string + */ + public function get_auth_header() { + return $this->get_api_key(); + } + + /** + * Get the Google AI API key. + * + * @return string + */ + public function get_api_key() { + return $this->api_key; + } +} diff --git a/includes/Classifai/Providers/GoogleAI/GeminiAPI.php b/includes/Classifai/Providers/GoogleAI/GeminiAPI.php new file mode 100644 index 000000000..d55981a86 --- /dev/null +++ b/includes/Classifai/Providers/GoogleAI/GeminiAPI.php @@ -0,0 +1,604 @@ +feature_instance = $feature_instance; + } + + /** + * Render the provider fields. + */ + public function render_provider_fields() { + $settings = $this->feature_instance->get_settings( static::ID ); + + add_settings_field( + static::ID . '_api_key', + esc_html__( 'API Key', 'classifai' ), + [ $this->feature_instance, 'render_input' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'api_key', + 'input_type' => 'password', + 'default_value' => $settings['api_key'], + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. + 'description' => sprintf( + wp_kses( + /* translators: %1$s is replaced with the OpenAI sign up URL */ + __( 'Don\'t have an Google AI (Gemini API) key? Get an API key now.', 'classifai' ), + [ + 'a' => [ + 'href' => [], + 'title' => [], + ], + ] + ), + esc_url( 'https://makersuite.google.com/app/apikey' ) + ), + ] + ); + + 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 = [ + 'api_key' => '', + 'authenticated' => false, + ]; + + 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 { + $settings = $this->feature_instance->get_settings(); + $api_key_settings = $this->sanitize_api_key_settings( $new_settings, $settings ); + + $new_settings[ static::ID ]['api_key'] = $api_key_settings[ static::ID ]['api_key']; + $new_settings[ static::ID ]['authenticated'] = $api_key_settings[ static::ID ]['authenticated']; + + return $new_settings; + } + + /** + * Sanitize the API key, showing an error message if needed. + * + * @param array $new_settings Incoming settings, if any. + * @param array $settings Current settings, if any. + * @return array + */ + public function sanitize_api_key_settings( array $new_settings = [], array $settings = [] ): array { + $authenticated = $this->authenticate_credentials( $new_settings[ static::ID ]['api_key'] ?? '' ); + + $new_settings[ static::ID ]['authenticated'] = $settings[ static::ID ]['authenticated']; + + if ( is_wp_error( $authenticated ) ) { + $new_settings[ static::ID ]['authenticated'] = false; + $error_message = $authenticated->get_error_message(); + + // Add an error message. + add_settings_error( + 'api_key', + 'classifai-auth', + $error_message, + 'error' + ); + } else { + $new_settings[ static::ID ]['authenticated'] = true; + } + + $new_settings[ static::ID ]['api_key'] = sanitize_text_field( $new_settings[ static::ID ]['api_key'] ?? $settings[ static::ID ]['api_key'] ); + + return $new_settings; + } + + /** + * Authenticate our credentials. + * + * @param string $api_key Api Key. + * @return bool|WP_Error + */ + protected function authenticate_credentials( string $api_key = '' ) { + // Check that we have credentials before hitting the API. + if ( empty( $api_key ) ) { + return new WP_Error( 'auth', esc_html__( 'Please enter your Google AI (Gemini API) key.', 'classifai' ) ); + } + + // Make request to ensure credentials work. + $request = new APIRequest( $api_key ); + $response = $request->get( $this->googleai_url . '/models' ); + + return ! is_wp_error( $response ) ? true : $response; + } + + /** + * Sanitize the API key. + * + * @param array $new_settings The settings array. + * @return string + */ + public function sanitize_api_key( array $new_settings ): string { + $settings = $this->feature_instance->get_settings(); + return sanitize_text_field( $new_settings[ static::ID ]['api_key'] ?? $settings[ static::ID ]['api_key'] ?? '' ); + } + + /** + * 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_titles( $post_id, $args ); + break; + case 'resize_content': + $return = $this->resize_content( $post_id, $args ); + break; + } + + return $return; + } + + /** + * Generate an excerpt using Google AI (Gemini API). + * + * @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 ) || ( isset( $settings[ static::ID ]['authenticated'] ) && false === $settings[ static::ID ]['authenticated'] ) || ( ! $feature->is_feature_enabled() && ( ! defined( 'WP_CLI' ) || ! WP_CLI ) ) ) { + return new WP_Error( 'not_enabled', esc_html__( 'Excerpt generation is disabled or Google AI authentication failed. Please check your settings.', 'classifai' ) ); + } + + $excerpt_length = absint( $settings['length'] ?? 55 ); + + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); + + $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 Gemini API. + * + * @since 3.0.0 + * @hook classifai_googleai_gemini_api_excerpt_prompt + * + * @param {string} $prompt Prompt we are sending to Gemini API. 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_googleai_gemini_api_excerpt_prompt', $prompt, $post_id, $excerpt_length ); + + /** + * Filter the request body before sending to Gemini API. + * + * @since 3.0.0 + * @hook classifai_googleai_gemini_api_excerpt_request_body + * + * @param {array} $body Request body that will be sent to Gemini API. + * @param {int} $post_id ID of post we are summarizing. + * + * @return {array} Request body. + */ + $body = apply_filters( + 'classifai_googleai_gemini_api_excerpt_request_body', + [ + 'contents' => [ + [ + 'parts' => [ + 'text' => 'You will be provided with content delimited by triple quotes. ' . $prompt . ' \n """' . $this->get_content( $post_id, false, $args['content'] ) . '"""', + ], + ], + ], + 'generationConfig' => [ + 'temperature' => 0.9, + 'topK' => 1, + 'topP' => 1, + 'maxOutputTokens' => 2048, + ], + ], + $post_id + ); + + // Make our API request. + $response = $request->post( + $this->googleai_url . '/' . $this->googleai_model . ':generateContent', + [ + 'body' => wp_json_encode( $body ), + ] + ); + + set_transient( 'classifai_googleai_gemini_api_excerpt_generation_latest_response', $response, DAY_IN_SECONDS * 30 ); + + // Extract out the text response, if it exists. + if ( ! is_wp_error( $response ) && ! empty( $response['candidates'] ) ) { + foreach ( $response['candidates'] as $candidate ) { + if ( isset( $candidate['content'], $candidate['content']['parts'] ) ) { + $parts = $candidate['content']['parts']; + $response = sanitize_text_field( trim( $parts[0]['text'], ' "\'' ) ); + } + } + } + + return $response; + } + + /** + * Generate titles using Google AI (Gemini API). + * + * @param int $post_id The Post Id we're processing + * @param array $args Arguments passed in. + * @return string|WP_Error + */ + public function generate_titles( 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 ), + [ + 'num' => 1, // Gemini API only returns 1 title. + '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 ) || ( isset( $settings[ static::ID ]['authenticated'] ) && false === $settings[ static::ID ]['authenticated'] ) || ! $feature->is_feature_enabled() ) { + return new WP_Error( 'not_enabled', esc_html__( 'Title generation is disabled or Google AI authentication failed. Please check your settings.', 'classifai' ) ); + } + + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); + + $prompt = esc_textarea( get_default_prompt( $settings['generate_title_prompt'] ) ?? $feature->prompt ); + + /** + * Filter the prompt we will send to Gemini API. + * + * @since 3.0.0 + * @hook classifai_googleai_gemini_api_title_prompt + * + * @param {string} $prompt Prompt we are sending to Gemini API. 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_googleai_gemini_api_title_prompt', $prompt, $post_id, $args ); + + /** + * Filter the request body before sending to Gemini API. + * + * @since 3.0.0 + * @hook classifai_googleai_gemini_api_title_request_body + * + * @param {array} $body Request body that will be sent to Gemini API. + * @param {int} $post_id ID of post we are summarizing. + * + * @return {array} Request body. + */ + $body = apply_filters( + 'classifai_googleai_gemini_api_title_request_body', + [ + 'contents' => [ + [ + 'parts' => [ + 'text' => 'You will be provided with content delimited by triple quotes. ' . $prompt . '\n"""' . $this->get_content( $post_id, false, $args['content'] ) . '"""', + ], + ], + ], + 'generationConfig' => [ + 'temperature' => 0.9, + 'topK' => 1, + 'topP' => 1, + 'maxOutputTokens' => 2048, + ], + ], + $post_id + ); + + // Make our API request. + $response = $request->post( + $this->googleai_url . '/' . $this->googleai_model . ':generateContent', + [ + 'body' => wp_json_encode( $body ), + ] + ); + + set_transient( 'classifai_googleai_gemini_api_title_generation_latest_response', $response, DAY_IN_SECONDS * 30 ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( empty( $response['candidates'] ) ) { + return new WP_Error( 'no_choices', esc_html__( 'No choices were returned from Google AI.', 'classifai' ) ); + } + + // Extract out the text response. + $return = []; + foreach ( $response['candidates'] as $candidate ) { + if ( isset( $candidate['content'], $candidate['content']['parts'] ) ) { + $parts = $candidate['content']['parts']; + $return[] = sanitize_text_field( trim( $parts[0]['text'], ' "\'' ) ); + } + } + + return $return; + } + + /** + * 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(); + + $args = wp_parse_args( + array_filter( $args ), + [ + 'num' => 1, // Gemini API only returns 1 variation as of now. + ] + ); + + $request = new APIRequest( $settings[ static::ID ]['api_key'] ?? '', $feature->get_option_name() ); + + 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 Gemini API. + * + * @since 3.0.0 + * @hook classifai_googleai_gemini_api_' . $args['resize_type'] . '_content_prompt + * + * @param {string} $prompt Resize prompt we are sending to Gemini API. 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_googleai_gemini_api_' . $args['resize_type'] . '_content_prompt', $prompt, $post_id, $args ); + + /** + * Filter the resize request body before sending to Gemini API. + * + * @since 2.3.0 + * @hook classifai_googleai_gemini_api_resize_content_request_body + * + * @param {array} $body Request body that will be sent to Gemini API. + * @param {int} $post_id ID of post. + * + * @return {array} Request body. + */ + $body = apply_filters( + 'classifai_googleai_gemini_api_resize_content_request_body', + [ + 'contents' => [ + [ + 'parts' => [ + 'text' => 'You will be provided with content delimited by triple quotes. ' . $prompt . '\n"""' . esc_html( $args['content'] ) . '"""', + ], + ], + ], + 'generationConfig' => [ + 'temperature' => 0.9, + 'topK' => 1, + 'topP' => 1, + 'maxOutputTokens' => 2048, + ], + ], + $post_id + ); + + // Make our API request. + $response = $request->post( + $this->googleai_url . '/' . $this->googleai_model . ':generateContent', + [ + 'body' => wp_json_encode( $body ), + ] + ); + + set_transient( 'classifai_googleai_gemini_api_content_resizing_latest_response', $response, DAY_IN_SECONDS * 30 ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( empty( $response['candidates'] ) ) { + return new WP_Error( 'no_choices', esc_html__( 'No choices were returned from Google AI.', 'classifai' ) ); + } + + // Extract out the text response. + $return = []; + foreach ( $response['candidates'] as $candidate ) { + if ( isset( $candidate['content'], $candidate['content']['parts'] ) ) { + $parts = $candidate['content']['parts']; + $return[] = sanitize_text_field( trim( $parts[0]['text'], ' "\'' ) ); + } + } + + return $return; + } + + /** + * Get our content, trimming if needed. + * + * ### Important Note: + * The content length is not limited in this implementation. + * The Gemini Pro model can process up to 30,720 input tokens, which is approximately equivalent to 18,000 - 24,000 words. (https://ai.google.dev/models/gemini#model_variations) + * Given that the average blog post length ranges from 1,500 - 2,500 words, this limit is more than sufficient for our use case. + * + * @param int $post_id Post ID to get content from. + * @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, 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 ); + + // Then trim our content, if needed, to stay under the max. + 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 GoogleAI. + * + * @since 3.0.0 + * @hook classifai_googleai_content + * + * @param {string} $content Content that will be sent to GoogleAI. + * @param {int} $post_id ID of post we are summarizing. + * + * @return {string} Content. + */ + return apply_filters( 'classifai_googleai_gemini_api_content', $content, $post_id ); + } + + /** + * Returns the debug information for the provider settings. + * + * @return array + */ + public function get_debug_information(): array { + $settings = $this->feature_instance->get_settings(); + $provider_settings = $settings[ static::ID ]; + $debug_info = []; + + if ( $this->feature_instance instanceof TitleGeneration ) { + $debug_info[ __( 'No. of titles', 'classifai' ) ] = 1; + $debug_info[ __( 'Generate title prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_title_prompt'] ?? [] ); + $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_googleai_gemini_api_title_generation_latest_response' ) ); + } elseif ( $this->feature_instance instanceof ExcerptGeneration ) { + $debug_info[ __( 'Excerpt length', 'classifai' ) ] = $settings['length'] ?? 55; + $debug_info[ __( 'Generate excerpt prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_excerpt_prompt'] ?? [] ); + $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_googleai_gemini_api_excerpt_generation_latest_response' ) ); + } elseif ( $this->feature_instance instanceof ContentResizing ) { + $debug_info[ __( 'No. of suggestions', 'classifai' ) ] = 1; + $debug_info[ __( 'Expand text prompt', 'classifai' ) ] = wp_json_encode( $settings['expand_text_prompt'] ?? [] ); + $debug_info[ __( 'Condense text prompt', 'classifai' ) ] = wp_json_encode( $settings['condense_text_prompt'] ?? [] ); + $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_googleai_gemini_api_content_resizing_latest_response' ) ); + } + + return apply_filters( + 'classifai_' . self::ID . '_debug_information', + $debug_info, + $settings, + $this->feature_instance + ); + } +} diff --git a/includes/Classifai/Providers/OpenAI/ChatGPT.php b/includes/Classifai/Providers/OpenAI/ChatGPT.php index f17b3a058..5b7dc640a 100644 --- a/includes/Classifai/Providers/OpenAI/ChatGPT.php +++ b/includes/Classifai/Providers/OpenAI/ChatGPT.php @@ -13,6 +13,7 @@ use WP_Error; use function Classifai\get_default_prompt; +use function Classifai\sanitize_number_of_responses_field; class ChatGPT extends Provider { @@ -84,6 +85,29 @@ public function render_provider_fields() { ] ); + switch ( $this->feature_instance::ID ) { + case ContentResizing::ID: + case TitleGeneration::ID: + add_settings_field( + static::ID . '_number_of_suggestions', + esc_html__( 'Number of suggestions', 'classifai' ), + [ $this->feature_instance, 'render_input' ], + $this->feature_instance->get_option_name(), + $this->feature_instance->get_option_name() . '_section', + [ + 'option_index' => static::ID, + 'label_for' => 'number_of_suggestions', + 'input_type' => 'number', + 'min' => 1, + 'step' => 1, + 'default_value' => $settings['number_of_suggestions'], + 'class' => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this. + 'description' => esc_html__( 'Number of suggestions that will be generated in one request.', 'classifai' ), + ] + ); + break; + } + do_action( 'classifai_' . static::ID . '_render_provider_fields', $this ); } @@ -98,6 +122,16 @@ public function get_default_provider_settings(): array { 'authenticated' => false, ]; + /** + * Default values for feature specific settings. + */ + switch ( $this->feature_instance::ID ) { + case ContentResizing::ID: + case TitleGeneration::ID: + $common_settings['number_of_suggestions'] = 1; + break; + } + return $common_settings; } @@ -114,6 +148,13 @@ public function sanitize_settings( array $new_settings ): array { $new_settings[ static::ID ]['api_key'] = $api_key_settings[ static::ID ]['api_key']; $new_settings[ static::ID ]['authenticated'] = $api_key_settings[ static::ID ]['authenticated']; + switch ( $this->feature_instance::ID ) { + case ContentResizing::ID: + case TitleGeneration::ID: + $new_settings[ static::ID ]['number_of_suggestions'] = sanitize_number_of_responses_field( 'number_of_suggestions', $new_settings[ static::ID ], $settings[ static::ID ] ); + break; + } + return $new_settings; } @@ -283,7 +324,7 @@ public function generate_titles( int $post_id = 0, array $args = [] ) { $args = wp_parse_args( array_filter( $args ), [ - 'num' => $settings['number_of_titles'] ?? 1, + 'num' => $settings[ static::ID ]['number_of_suggestions'] ?? 1, 'content' => '', ] ); @@ -391,7 +432,7 @@ public function resize_content( int $post_id, array $args = array() ) { $args = wp_parse_args( array_filter( $args ), [ - 'num' => $settings['number_of_suggestions'] ?? 1, + 'num' => $settings[ static::ID ]['number_of_suggestions'] ?? 1, ] ); @@ -552,17 +593,17 @@ public function get_debug_information(): array { $debug_info = []; if ( $this->feature_instance instanceof TitleGeneration ) { - $debug_info[ __( 'No. of titles', 'classifai' ) ] = $provider_settings['number_of_titles'] ?? 1; - $debug_info[ __( 'Generate title prompt', 'classifai' ) ] = wp_json_encode( $provider_settings['generate_title_prompt'] ?? [] ); + $debug_info[ __( 'No. of titles', 'classifai' ) ] = $provider_settings['number_of_suggestions'] ?? 1; + $debug_info[ __( 'Generate title prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_title_prompt'] ?? [] ); $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_title_generation_latest_response' ) ); } elseif ( $this->feature_instance instanceof ExcerptGeneration ) { $debug_info[ __( 'Excerpt length', 'classifai' ) ] = $settings['length'] ?? 55; - $debug_info[ __( 'Generate excerpt prompt', 'classifai' ) ] = wp_json_encode( $provider_settings['generate_excerpt_prompt'] ?? [] ); + $debug_info[ __( 'Generate excerpt prompt', 'classifai' ) ] = wp_json_encode( $settings['generate_excerpt_prompt'] ?? [] ); $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_excerpt_generation_latest_response' ) ); } elseif ( $this->feature_instance instanceof ContentResizing ) { $debug_info[ __( 'No. of suggestions', 'classifai' ) ] = $provider_settings['number_of_suggestions'] ?? 1; - $debug_info[ __( 'Expand text prompt', 'classifai' ) ] = wp_json_encode( $provider_settings['expand_text_prompt'] ?? [] ); - $debug_info[ __( 'Condense text prompt', 'classifai' ) ] = wp_json_encode( $provider_settings['condense_text_prompt'] ?? [] ); + $debug_info[ __( 'Expand text prompt', 'classifai' ) ] = wp_json_encode( $settings['expand_text_prompt'] ?? [] ); + $debug_info[ __( 'Condense text prompt', 'classifai' ) ] = wp_json_encode( $settings['condense_text_prompt'] ?? [] ); $debug_info[ __( 'Latest response', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_openai_chatgpt_content_resizing_latest_response' ) ); } diff --git a/includes/Classifai/Services/LanguageProcessing.php b/includes/Classifai/Services/LanguageProcessing.php index 23c924c3d..6b36787aa 100644 --- a/includes/Classifai/Services/LanguageProcessing.php +++ b/includes/Classifai/Services/LanguageProcessing.php @@ -43,6 +43,7 @@ public static function get_service_providers(): array { 'Classifai\Providers\OpenAI\Moderation', 'Classifai\Providers\OpenAI\Whisper', 'Classifai\Providers\Watson\NLU', + 'Classifai\Providers\GoogleAI\GeminiAPI', ] ); } diff --git a/readme.txt b/readme.txt index 49188a0ed..cc04ea699 100644 --- a/readme.txt +++ b/readme.txt @@ -18,9 +18,9 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro **Features** -* Generate a summary of post content and store it as an excerpt using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) -* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) -* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) +* Generate a summary of post content and store it as an excerpt using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) +* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) +* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat) and [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview) * Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E API](https://platform.openai.com/docs/guides/images) * Generate transcripts of audio files using [OpenAI's Whisper API](https://platform.openai.com/docs/guides/speech-to-text) * Convert text content into audio and output a "read-to-me" feature on the front-end to play this audio using [Microsoft Azure's Text to Speech API](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/text-to-speech) @@ -35,6 +35,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro * To utilize the NLU Language Processing functionality, you will need an active [IBM Watson](https://cloud.ibm.com/registration) account. * To utilize the ChatGPT, Embeddings, or Whisper Language Processing functionality or DALL·E Image Processing functionality, you will need an active [OpenAI](https://platform.openai.com/signup) account. * To utilize the Azure AI Vision Image Processing functionality or Text to Speech Language Processing functionality, you will need an active [Microsoft Azure](https://signup.azure.com/signup) account. +* To utilize the Google Gemini Language Processing functionality, you will need an active [Google Gemini](https://ai.google.dev/tutorials/setup) account. == Upgrade Notice == diff --git a/tests/cypress/integration/language-processing/excerpt-generation-googleai-gemini-api.test.js b/tests/cypress/integration/language-processing/excerpt-generation-googleai-gemini-api.test.js new file mode 100644 index 000000000..9be2e7e07 --- /dev/null +++ b/tests/cypress/integration/language-processing/excerpt-generation-googleai-gemini-api.test.js @@ -0,0 +1,117 @@ +import { getGeminiAPIData } from '../../plugins/functions'; + +describe( '[Language processing] Excerpt Generation Tests', () => { + before( () => { + cy.login(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' + ); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_excerpt_generation_post_types_post' + ).check(); + cy.get( '#submit' ).click(); + cy.optInAllFeatures(); + cy.disableClassicEditor(); + } ); + + beforeEach( () => { + cy.login(); + } ); + + it( 'Can save Google AI (Gemini API) "Language Processing" settings', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' + ); + cy.get( '#provider' ).select( 'googleai_gemini_api' ); + cy.get( + 'input[name="classifai_feature_excerpt_generation[googleai_gemini_api][api_key]"]' + ) + .clear() + .type( 'password' ); + + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_excerpt_generation_roles_administrator' + ).check(); + cy.get( '#length' ).clear().type( 35 ); + cy.get( '#submit' ).click(); + } ); + + it( 'Can see the generate excerpt button in a post', () => { + cy.visit( '/wp-admin/plugins.php' ); + cy.disableClassicEditor(); + + const data = getGeminiAPIData(); + + // Create test post. + cy.createPost( { + title: 'Test ChatGPT post', + content: 'Test GPT content', + } ); + + // Close post publish panel. + const closePanelSelector = 'button[aria-label="Close panel"]'; + cy.get( 'body' ).then( ( $body ) => { + if ( $body.find( closePanelSelector ).length > 0 ) { + cy.get( closePanelSelector ).click(); + } + } ); + + // Open post settings sidebar. + cy.openDocumentSettingsSidebar(); + + // Find and open the excerpt panel. + const panelButtonSelector = `.components-panel__body .components-panel__body-title button:contains("Excerpt")`; + + cy.get( panelButtonSelector ).then( ( $panelButton ) => { + // Find the panel container. + const $panel = $panelButton.parents( '.components-panel__body' ); + + // Open panel. + if ( ! $panel.hasClass( 'is-opened' ) ) { + cy.wrap( $panelButton ).click(); + } + + // Verify button exists. + cy.wrap( $panel ) + .find( '.editor-post-excerpt button' ) + .should( 'exist' ); + + // Click on button and verify data loads in. + cy.wrap( $panel ).find( '.editor-post-excerpt button' ).click(); + cy.wrap( $panel ).find( 'textarea' ).should( 'have.value', data ); + } ); + } ); + + it( 'Can see the generate excerpt button in a post (Classic Editor)', () => { + cy.enableClassicEditor(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' + ); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + const data = getGeminiAPIData(); + + cy.createClassicPost( { + title: 'Excerpt test classic', + content: 'Test GPT content.', + postType: 'post', + } ); + + // Ensure excerpt metabox is shown. + cy.get( '#show-settings-link' ).click(); + cy.get( '#postexcerpt-hide' ).check( { force: true } ); + + // Verify button exists. + cy.get( '#classifai-openai__excerpt-generate-btn' ).should( 'exist' ); + + // Click on button and verify data loads in. + cy.get( '#classifai-openai__excerpt-generate-btn' ).click(); + cy.get( '#excerpt' ).should( 'have.value', data ); + + cy.disableClassicEditor(); + } ); +} ); diff --git a/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js index 5516472fb..475e5edf8 100644 --- a/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/excerpt-generation-openapi-chatgpt.test.js @@ -24,6 +24,7 @@ describe( '[Language processing] Excerpt Generation Tests', () => { '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_excerpt_generation' ); + cy.get( '#provider' ).select( 'openai_chatgpt' ); cy.get( '#api_key' ).clear().type( 'password' ); cy.get( '#status' ).check(); diff --git a/tests/cypress/integration/language-processing/resize_content-googleai-gemini-api.test.js b/tests/cypress/integration/language-processing/resize_content-googleai-gemini-api.test.js new file mode 100644 index 000000000..daf7c22d3 --- /dev/null +++ b/tests/cypress/integration/language-processing/resize_content-googleai-gemini-api.test.js @@ -0,0 +1,81 @@ +describe( '[Language processing] Speech to Text Tests', () => { + before( () => { + cy.login(); + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' + ); + cy.get( '#status' ).check(); + cy.get( '#provider' ).select( 'googleai_gemini_api' ); + cy.get( + 'input[name="classifai_feature_content_resizing[googleai_gemini_api][api_key]"]' + ) + .clear() + .type( 'abc123' ); + cy.get( '#submit' ).click(); + cy.optInAllFeatures(); + cy.disableClassicEditor(); + } ); + + beforeEach( () => { + cy.login(); + } ); + + it( 'Resize content feature can grow and shrink content', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' + ); + + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_content_resizing_roles_administrator' + ).check(); + cy.get( '#submit' ).click(); + + cy.createPost( { + title: 'Resize content', + content: 'Hello, world.', + } ); + + cy.get( '.classifai-resize-content-btn' ).click(); + cy.get( '.components-button' ).contains( 'Expand this text' ).click(); + cy.get( + '.classifai-content-resize__result-table tbody tr:first .classifai-content-resize__grow-stat' + ).should( 'contain.text', '+8 words' ); + cy.get( + '.classifai-content-resize__result-table tbody tr:first .classifai-content-resize__grow-stat' + ).should( 'contain.text', '+49 characters' ); + cy.get( + '.classifai-content-resize__result-table tbody tr:first button' + ).click(); + cy.getBlockEditor() + .find( '[data-type="core/paragraph"]' ) + .should( + 'contain.text', + 'Start with the basic building block of one narrative.' + ); + + cy.createPost( { + title: 'Resize content', + content: + 'Start with the basic building block of one narrative to begin with the editorial process.', + } ); + + cy.get( '.classifai-resize-content-btn' ).click(); + cy.get( '.components-button' ).contains( 'Condense this text' ).click(); + cy.get( + '.classifai-content-resize__result-table tbody tr:first .classifai-content-resize__shrink-stat' + ).should( 'contain.text', '-5 words' ); + cy.get( + '.classifai-content-resize__result-table tbody tr:first .classifai-content-resize__shrink-stat' + ).should( 'contain.text', '-27 characters' ); + cy.get( + '.classifai-content-resize__result-table tbody tr:first button' + ).click(); + cy.getBlockEditor() + .find( '[data-type="core/paragraph"]' ) + .should( + 'contain.text', + 'Start with the basic building block of one narrative.' + ); + } ); +} ); diff --git a/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js index 9d4fbb0c9..136f3cfe5 100644 --- a/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/resize_content-openapi-chatgpt.test.js @@ -5,6 +5,7 @@ describe( '[Language processing] Speech to Text Tests', () => { '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_content_resizing' ); cy.get( '#status' ).check(); + cy.get( '#provider' ).select( 'openai_chatgpt' ); cy.get( '#api_key' ).type( 'abc123' ); cy.get( '#submit' ).click(); cy.optInAllFeatures(); diff --git a/tests/cypress/integration/language-processing/title-generation-googleai-gemini-api.test.js b/tests/cypress/integration/language-processing/title-generation-googleai-gemini-api.test.js new file mode 100644 index 000000000..2d44f6bbf --- /dev/null +++ b/tests/cypress/integration/language-processing/title-generation-googleai-gemini-api.test.js @@ -0,0 +1,121 @@ +import { getGeminiAPIData } from '../../plugins/functions'; + +describe( '[Language processing] Title Generation Tests', () => { + before( () => { + cy.login(); + cy.optInAllFeatures(); + cy.disableClassicEditor(); + } ); + + beforeEach( () => { + cy.login(); + } ); + + it( 'Can save Google AI (Gemini API) "Language Processing" title settings', () => { + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' + ); + + cy.get( '#provider' ).select( 'googleai_gemini_api' ); + cy.get( + 'input[name="classifai_feature_title_generation[googleai_gemini_api][api_key]"]' + ) + .clear() + .type( 'password' ); + cy.get( '#status' ).check(); + cy.get( + '#classifai_feature_title_generation_roles_administrator' + ).check(); + cy.get( '#submit' ).click(); + } ); + + it( 'Can see the generate titles button in a post', () => { + const data = getGeminiAPIData(); + + // Create test post. + cy.createPost( { + title: 'Test ChatGPT generate titles', + content: 'Test content', + } ); + + // Close post publish panel. + const closePanelSelector = 'button[aria-label="Close panel"]'; + cy.get( 'body' ).then( ( $body ) => { + if ( $body.find( closePanelSelector ).length > 0 ) { + cy.get( closePanelSelector ).click(); + } + } ); + + // Open post settings sidebar. + cy.openDocumentSettingsSidebar(); + + // Find and open the summary panel. + const panelButtonSelector = `.components-panel__body.edit-post-post-status .components-panel__body-title button`; + + cy.get( panelButtonSelector ).then( ( $panelButton ) => { + // Find the panel container. + const $panel = $panelButton.parents( '.components-panel__body' ); + + // Open panel. + if ( ! $panel.hasClass( 'is-opened' ) ) { + cy.wrap( $panelButton ).click(); + } + + // Verify button exists. + cy.wrap( $panel ) + .find( '.classifai-post-status button.title' ) + .should( 'exist' ); + + // Click on button and verify modal shows. + cy.wrap( $panel ) + .find( '.classifai-post-status button.title' ) + .click(); + } ); + + cy.get( '.title-modal' ).should( 'exist' ); + + // Click on button and verify data loads in. + cy.get( '.title-modal .classifai-title' ) + .first() + .find( 'textarea' ) + .should( 'have.value', data ); + cy.get( '.title-modal .classifai-title' ) + .first() + .find( 'button' ) + .click(); + + cy.get( '.title-modal' ).should( 'not.exist' ); + cy.getBlockEditor() + .find( '.editor-post-title__input' ) + .should( ( $el ) => { + expect( $el.first() ).to.contain( data ); + } ); + } ); + + it( 'Can see the generate titles button in a post (Classic Editor)', () => { + cy.enableClassicEditor(); + + cy.visit( + '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' + ); + cy.get( '#status' ).check(); + cy.get( '#submit' ).click(); + + const data = getGeminiAPIData(); + + cy.visit( '/wp-admin/post-new.php' ); + + cy.get( '#classifai-openai__title-generate-btn' ).click(); + cy.get( '#classifai-openai__modal' ).should( 'be.visible' ); + cy.get( '.classifai-openai__result-item' ) + .first() + .find( 'textarea' ) + .should( 'have.value', data ); + + cy.get( '.classifai-openai__select-title' ).first().click(); + cy.get( '#classifai-openai__modal' ).should( 'not.be.visible' ); + cy.get( '#title' ).should( 'have.value', data ); + + cy.disableClassicEditor(); + } ); +} ); diff --git a/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js b/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js index 05ae7e787..7c7419b66 100644 --- a/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js +++ b/tests/cypress/integration/language-processing/title-generation-openapi-chatgpt.test.js @@ -16,12 +16,13 @@ describe( '[Language processing] Title Generation Tests', () => { '/wp-admin/tools.php?page=classifai&tab=language_processing&feature=feature_title_generation' ); + cy.get( '#provider' ).select( 'openai_chatgpt' ); cy.get( '#api_key' ).clear().type( 'password' ); cy.get( '#status' ).check(); cy.get( '#classifai_feature_title_generation_roles_administrator' ).check(); - cy.get( '#number_of_titles' ).type( 1 ); + cy.get( '#number_of_suggestions' ).type( 1 ); cy.get( '#submit' ).click(); } ); diff --git a/tests/cypress/plugins/functions.js b/tests/cypress/plugins/functions.js index fb3085d72..014016904 100644 --- a/tests/cypress/plugins/functions.js +++ b/tests/cypress/plugins/functions.js @@ -7,6 +7,7 @@ import * as ocrData from '../../test-plugin/ocr.json'; import * as whisperData from '../../test-plugin/whisper.json'; import * as imageData from '../../test-plugin/image_analyze.json'; import * as pdfData from '../../test-plugin/pdf.json'; +import * as geminiData from '../../test-plugin/geminiapi.json'; /** * Get Taxonomy data from test NLU json file. @@ -59,6 +60,20 @@ export const getChatGPTData = ( type = 'default' ) => { return text.join( ' ' ); }; +/** + * Get text data from test GeminiAPI json file. + * + * @return {string[]} GeminiAPI Data. + */ +export const getGeminiAPIData = () => { + const text = []; + geminiData.candidates.forEach( ( el ) => { + text.push( el.content.parts[ 0 ].text ); + } ); + + return text.join( ' ' ); +}; + /** * Get data from test DALL·E json file. * diff --git a/tests/test-plugin/e2e-test-plugin.php b/tests/test-plugin/e2e-test-plugin.php index 8ce052df1..450df3da1 100644 --- a/tests/test-plugin/e2e-test-plugin.php +++ b/tests/test-plugin/e2e-test-plugin.php @@ -84,6 +84,20 @@ function classifai_test_mock_http_requests( $preempt, $parsed_args, $url ) { 'success' => 1, 'body' => '', ); + } elseif ( strpos( $url, 'https://generativelanguage.googleapis.com/v1beta' ) !== false ) { + $response = file_get_contents( __DIR__ . '/geminiapi.json' ); + $body_json = $parsed_args['body'] ?? false; + + if ( $body_json ) { + $body = json_decode( $body_json, JSON_OBJECT_AS_ARRAY ); + $contents = isset( $body['contents'] ) ? $body['contents'] : []; + $parts = isset( $contents[0]['parts'] ) ? $contents[0]['parts'] : []; + $prompt = $parts['text'] ?? ''; + + if ( str_contains( $prompt, 'Increase the content' ) || str_contains( $prompt, 'Decrease the content' ) ) { + $response = file_get_contents( __DIR__ . '/geminiapi-resize-content.json' ); + } + } } if ( ! empty( $response ) ) { diff --git a/tests/test-plugin/geminiapi-resize-content.json b/tests/test-plugin/geminiapi-resize-content.json new file mode 100644 index 000000000..6a627ebc8 --- /dev/null +++ b/tests/test-plugin/geminiapi-resize-content.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "[Gemini] Start with the basic building block of one narrative." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/tests/test-plugin/geminiapi.json b/tests/test-plugin/geminiapi.json new file mode 100644 index 000000000..d2daef7ee --- /dev/null +++ b/tests/test-plugin/geminiapi.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Hello, this is sample response from the model." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +}