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 new Content Generation Feature #859

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro
* Generate key takeaways from post content and render at the top of a post using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or locally using [Ollama](https://ollama.com/)
* Generate titles from post content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service), [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview), [xAI's Grok](https://x.ai/) or locally using [Ollama](https://ollama.com/)
* Expand or condense text content using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service), [Google's Gemini API](https://ai.google.dev/docs/gemini_api_overview), [xAI's Grok](https://x.ai/) or locally using [Ollama](https://ollama.com/)
* Draft a full length article using [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/chat), [Microsoft Azure's OpenAI service](https://azure.microsoft.com/en-us/products/ai-services/openai-service) or locally using [Ollama](https://ollama.com/)
* Generate new images on demand to use in-content or as a featured image using [OpenAI's DALL·E 3 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)
Expand All @@ -44,9 +45,9 @@ Tap into leading cloud-based services like [OpenAI](https://openai.com/), [Micro
| :-: | :-: | :-: | :-: |
| ![Screenshot of ClassifAI audio transcript generation](assets/img/screenshot-9.png "Example of automatic audio transcript generation with OpenAI.") | ![Screenshot of ClassifAI title generation](assets/img/screenshot-10.png "Example of automatic title generation with OpenAI.") | ![Screenshot of ClassifAI expand/condense text feature](assets/img/screenshot-12.png "Example of expanding or condensing text with OpenAI.") | ![Screenshot of ClassifAI text to speech generation](assets/img/screenshot-11.png "Example of automatic text to speech generation with Azure.") |

| Key Takeaways | | | |
| Key Takeaways | Content Generation | | |
| :-: | :-: | :-: | :-: |
| ![Screenshot of the ClassifAI Key Takeaways block](assets/img/screenshot-14.png "Example of generating key takeaways using OpenAI.") | | | |
| ![Screenshot of the ClassifAI Key Takeaways block](assets/img/screenshot-14.png "Example of generating key takeaways using OpenAI.") | ![Screenshot of the ClassifAI Content Generation Feature](assets/img/screenshot-15.png "Example of generating content using OpenAI.") | | |

### Image Processing

Expand Down Expand Up @@ -215,7 +216,7 @@ 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 > Title Generation, Excerpt Generation or Content Resizing > Settings
### 2. Configure OpenAI API Keys under Tools > ClassifAI > Language Processing > Title Generation, Excerpt Generation, Content Generation or Content Resizing > Settings

* Select **OpenAI ChatGPT** in the provider dropdown.
* Enter your API Key copied from the above step into the `API Key` field.
Expand All @@ -233,6 +234,9 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand
* 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 generation, edit (or create) an item.
* Add a title to this item.
* Open the Summary panel in the sidebar and click on `Generate content`.
* 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.
Expand All @@ -249,7 +253,7 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand
* Click `Keys and Endpoint` in the left hand Resource Management menu to get the endpoint for this resource.
* Click the copy icon next to `KEY 1` to copy the API Key credential for this resource.

### 2. Configure API Keys under Tools > ClassifAI > Language Processing > Title Generation, Excerpt Generation or Content Resizing > Settings
### 2. Configure API Keys under Tools > ClassifAI > Language Processing > Title Generation, Excerpt Generation, Content Generation or Content Resizing > Settings

* Select **Azure OpenAI** in the provider dropdown.
* Enter your endpoint you copied from the above step into the `Endpoint URL` field.
Expand All @@ -270,6 +274,9 @@ IBM Watson's [Categories](https://cloud.ibm.com/docs/natural-language-understand
* 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 generation, edit (or create) an item.
* Add a title to this item.
* Open the Summary panel in the sidebar and click on `Generate content`.
* 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.
Expand Down
Binary file added assets/img/screenshot-15.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 changes: 274 additions & 0 deletions includes/Classifai/Features/ContentGeneration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
<?php

namespace Classifai\Features;

use Classifai\Providers\Azure\OpenAI;
use Classifai\Providers\GoogleAI\GeminiAPI;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Providers\Browser\ChromeAI;
use Classifai\Providers\XAI\Grok;
use Classifai\Providers\Localhost\Ollama;
use Classifai\Services\LanguageProcessing;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;

use function Classifai\sanitize_prompts;
use function Classifai\get_asset_info;

/**
* Class ContentGeneration
*/
class ContentGeneration extends Feature {
/**
* ID of the feature.
*
* @var string
*/
const ID = 'feature_content_generation';

/**
* Prompt for creating content.
*
* @var string
*/
public $prompt = 'Act as an experienced SEO copywriter tasked with writing an article based off of a given summary and an optionally provided title. Your goal is to craft a compelling, informative piece that adheres to SEO best practices, is well-researched, engaging to the target audience, and structured in a way that enhances readability. Incorporate relevant keywords naturally throughout the text, without compromising the flow or quality of the content. Ensure that the article provides value to the reader. Only return the contents of the article, not the title or other commentary. Separate each paragraph with double line breaks.';

/**
* Constructor.
*/
public function __construct() {
$this->label = __( 'Content Generation', 'classifai' );

// Contains all providers that are registered to the service.
$this->provider_instances = $this->get_provider_instances( LanguageProcessing::get_service_providers() );

// Contains just the providers this feature supports.
$this->supported_providers = [
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
OpenAI::ID => __( 'Azure OpenAI', 'classifai' ),
Ollama::ID => __( 'Ollama', 'classifai' ),
];
}

/**
* Set up necessary hooks.
*
* We utilize this so we can register the REST route.
*/
public function setup() {
parent::setup();
add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
}

/**
* Set up necessary hooks.
*/
public function feature_setup() {
add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] );
}

/**
* Register any needed endpoints.
*/
public function register_endpoints() {
register_rest_route(
'classifai/v1',
'create-content',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'rest_endpoint_callback' ],
'permission_callback' => [ $this, 'create_content_permissions_check' ],
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID where content should be stored.', 'classifai' ),
],
'summary' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'The summary that will be used to generate the full article.', 'classifai' ),
],
'title' => [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'The title of the article.', 'classifai' ),
],
'conversation' => [
'type' => 'object',
'properties' => [
'prompt' => [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'The prompt a user sent.', 'classifai' ),
],
'response' => [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'The response from the assistant to the prompt.', 'classifai' ),
],
],
'description' => esc_html__( 'Any previous conversation between a user and assistant.', 'classifai' ),
],
],
]
);
}

/**
* Check if a given request has access to create content.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function create_content_permissions_check( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );

// Ensure we have a logged in user that can edit the item.
if ( empty( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return false;
}

$post_type = get_post_type( $post_id );
$post_type_obj = get_post_type_object( $post_type );

// Ensure the post type is allowed in REST endpoints.
if ( ! $post_type || empty( $post_type_obj ) || empty( $post_type_obj->show_in_rest ) ) {
return false;
}

// Ensure the feature is enabled. Also runs a user check.
if ( ! $this->is_feature_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Content Generation is not currently enabled.', 'classifai' ) );
}

return true;
}

/**
* Generic request handler for all our custom routes.
*
* @param WP_REST_Request $request The full request object.
* @return \WP_REST_Response
*/
public function rest_endpoint_callback( WP_REST_Request $request ) {
$route = $request->get_route();

if ( strpos( $route, '/classifai/v1/create-content' ) === 0 ) {
return rest_ensure_response(
$this->run(
$request->get_param( 'id' ),
'create_content',
[
'title' => $request->get_param( 'title' ),
'summary' => $request->get_param( 'summary' ),
'conversation' => $request->get_param( 'conversation' ),
]
)
);
}

return parent::rest_endpoint_callback( $request );
}

/**
* Enqueue the editor scripts.
*/
public function enqueue_editor_assets() {
global $post;

if ( empty( $post ) || ! is_admin() ) {
return;
}

wp_enqueue_script(
'classifai-plugin-content-generation-js',
CLASSIFAI_PLUGIN_URL . 'dist/classifai-plugin-content-generation.js',
get_asset_info( 'classifai-plugin-content-generation', 'dependencies' ),
get_asset_info( 'classifai-plugin-content-generation', 'version' ),
true
);
}

/**
* Get the description for the enable field.
*
* @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 draft content.', 'classifai' );
}

/**
* Returns the default settings for the feature.
*
* @return array
*/
public function get_feature_default_settings(): array {
return [
'prompt' => [
[
'title' => esc_html__( 'ClassifAI default', 'classifai' ),
'prompt' => $this->prompt,
'original' => 1,
],
],
'post_types' => [
'post' => 'post',
],
'provider' => ChatGPT::ID,
];
}

/**
* Returns the settings for the feature.
*
* @param string $index The index of the setting to return.
* @return array|mixed
*/
public function get_settings( $index = false ) {
$settings = parent::get_settings( $index );

// Keep using the original prompt from the codebase to allow updates.
if ( $settings && ! empty( $settings['prompt'] ) ) {
foreach ( $settings['prompt'] as $key => $prompt ) {
if ( 1 === intval( $prompt['original'] ) ) {
$settings['prompt'][ $key ]['prompt'] = $this->prompt;
break;
}
}
}

return $settings;
}

/**
* Sanitizes the default feature settings.
*
* @param array $new_settings Settings being saved.
* @return array
*/
public function sanitize_default_feature_settings( array $new_settings ): array {
$settings = $this->get_settings();
$post_types = \Classifai\get_post_types_for_language_settings();

$new_settings['prompt'] = sanitize_prompts( 'prompt', $new_settings );

foreach ( $post_types as $post_type ) {
if ( ! isset( $new_settings['post_types'][ $post_type->name ] ) ) {
$new_settings['post_types'][ $post_type->name ] = $settings['post_types'];
} else {
$new_settings['post_types'][ $post_type->name ] = sanitize_text_field( $new_settings['post_types'][ $post_type->name ] );
}
}

return $new_settings;
}
}
Loading
Loading