Skip to content

Commit

Permalink
Merge pull request #525 from 10up/feature/post-rest-endpoint
Browse files Browse the repository at this point in the history
Add POST endpoints for excerpt and title generation
  • Loading branch information
dkotter authored Jul 17, 2023
2 parents d3d85c4 + 07e57f9 commit aa63866
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 72 deletions.
42 changes: 27 additions & 15 deletions includes/Classifai/Providers/OpenAI/ChatGPT.php
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ public function rest_endpoint_callback( $post_id = 0, $route_to_call = '', $args
// Handle all of our routes.
switch ( $route_to_call ) {
case 'excerpt':
$return = $this->generate_excerpt( $post_id );
$return = $this->generate_excerpt( $post_id, $args );
break;
case 'title':
$return = $this->generate_titles( $post_id, $args );
Expand All @@ -474,15 +474,22 @@ public function rest_endpoint_callback( $post_id = 0, $route_to_call = '', $args
/**
* Generate an excerpt using ChatGPT.
*
* @param int $post_id The Post ID we're processing
* @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 ) {
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' ) );
}

$settings = $this->get_settings();
$args = wp_parse_args(
array_filter( $args ),
[
'content' => '',
]
);

// 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.
Expand Down Expand Up @@ -526,7 +533,7 @@ public function generate_excerpt( int $post_id = 0 ) {
'messages' => [
[
'role' => 'user',
'content' => $prompt . ': ' . $this->get_content( $post_id, $excerpt_length ) . '',
'content' => $prompt . ': ' . $this->get_content( $post_id, $excerpt_length, true, $args['content'] ) . '',
],
],
'temperature' => 0,
Expand Down Expand Up @@ -573,7 +580,8 @@ public function generate_titles( int $post_id = 0, array $args = [] ) {
$args = wp_parse_args(
array_filter( $args ),
[
'num' => $settings['number_titles'] ?? 1,
'num' => $settings['number_titles'] ?? 1,
'content' => '',
]
);

Expand Down Expand Up @@ -617,7 +625,7 @@ public function generate_titles( int $post_id = 0, array $args = [] ) {
'messages' => [
[
'role' => 'user',
'content' => esc_html( $prompt ) . ': ' . $this->get_content( $post_id, absint( $args['num'] ) * 15, false ) . '',
'content' => esc_html( $prompt ) . ': ' . $this->get_content( $post_id, absint( $args['num'] ) * 15, false, $args['content'] ) . '',
],
],
'temperature' => 0.9,
Expand Down Expand Up @@ -659,12 +667,13 @@ public function generate_titles( int $post_id = 0, array $args = [] ) {
/**
* Get our content, trimming if needed.
*
* @param int $post_id Post ID to get content from.
* @param int $return_length Word length of returned content.
* @param bool $use_title Whether to use the title or not.
* @param int $post_id Post ID to get content from.
* @param int $return_length Word length of returned content.
* @param bool $use_title Whether to use the title or not.
* @param string $post_content The post content.
* @return string
*/
public function get_content( int $post_id = 0, int $return_length = 0, bool $use_title = true ) {
public function get_content( int $post_id = 0, int $return_length = 0, bool $use_title = true, string $post_content = '' ) {
$tokenizer = new Tokenizer( $this->max_tokens );
$normalizer = new Normalizer();

Expand All @@ -683,17 +692,20 @@ public function get_content( int $post_id = 0, int $return_length = 0, bool $use
*/
$max_content_tokens = $this->max_tokens - $return_tokens - 13;

if ( empty( $post_content ) ) {
$post = get_post( $post_id );
$post_content = apply_filters( 'the_content', $post->post_content );
}

$post_content = preg_replace( '#\[.+\](.+)\[/.+\]#', '$1', $post_content );

// Then trim our content, if needed, to stay under the max.
if ( $use_title ) {
$content = $tokenizer->trim_content(
$normalizer->normalize( $post_id ),
$normalizer->normalize( $post_id, $post_content ),
(int) $max_content_tokens
);
} else {
$post = get_post( $post_id );
$post_content = apply_filters( 'the_content', $post->post_content );
$post_content = preg_replace( '#\[.+\](.+)\[/.+\]#', '$1', $post_content );

$content = $tokenizer->trim_content(
$normalizer->normalize_content( $post_content, '', $post_id ),
(int) $max_content_tokens
Expand Down
92 changes: 63 additions & 29 deletions includes/Classifai/Services/LanguageProcessing.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,35 @@ public function register_endpoints() {

register_rest_route(
'classifai/v1/openai',
'generate-excerpt/(?P<id>\d+)',
'generate-excerpt(?:/(?P<id>\d+))?',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_post_excerpt' ],
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to generate excerpt for.', 'classifai' ),
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_post_excerpt' ],
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to generate excerpt for.', 'classifai' ),
],
],
'permission_callback' => [ $this, 'generate_post_excerpt_permissions_check' ],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'generate_post_excerpt' ],
'args' => [
'content' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'Content to generate a title for', 'classifai' ),
],
],
'permission_callback' => [ $this, 'generate_post_excerpt_permissions_check' ],
],
'permission_callback' => [ $this, 'generate_post_excerpt_permissions_check' ],
]
);

Expand Down Expand Up @@ -119,27 +135,43 @@ public function register_endpoints() {

register_rest_route(
'classifai/v1/openai',
'generate-title/(?P<id>\d+)',
'generate-title(?:/(?P<id>\d+))?',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_post_title' ],
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to generate title for.', 'classifai' ),
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'generate_post_title' ],
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Post ID to generate title for.', 'classifai' ),
],
'n' => [
'type' => 'integer',
'minimum' => 1,
'maximum' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'Number of titles to generate', 'classifai' ),
],
],
'n' => [
'type' => 'integer',
'minimum' => 1,
'maximum' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'Number of titles to generate', 'classifai' ),
'permission_callback' => [ $this, 'generate_post_title_permissions_check' ],
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'generate_post_title' ],
'args' => [
'content' => [
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => esc_html__( 'Content to generate a title for', 'classifai' ),
],
],
'permission_callback' => [ $this, 'generate_post_title_permissions_check' ],
],
'permission_callback' => [ $this, 'generate_post_title_permissions_check' ],
]
);
}
Expand Down Expand Up @@ -242,6 +274,7 @@ public function generate_post_tags_permissions_check( WP_REST_Request $request )
*/
public function generate_post_excerpt( WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );
$content = $request->get_param( 'content' );

// Find the right provider class.
$provider = find_provider_class( $this->provider_classes ?? [], 'ChatGPT' );
Expand All @@ -251,7 +284,7 @@ public function generate_post_excerpt( WP_REST_Request $request ) {
return $provider;
}

return rest_ensure_response( $provider->rest_endpoint_callback( $post_id, 'excerpt' ) );
return rest_ensure_response( $provider->rest_endpoint_callback( $post_id, 'excerpt', [ 'content' => $content ] ) );
}

/**
Expand Down Expand Up @@ -456,7 +489,8 @@ public function generate_post_title( WP_REST_Request $request ) {
$post_id,
'title',
[
'num' => $request->get_param( 'n' ),
'num' => $request->get_param( 'n' ),
'content' => $request->get_param( 'content' ),
]
)
);
Expand Down
7 changes: 4 additions & 3 deletions includes/Classifai/Watson/Normalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ class Normalizer {
* The post title is also included in the content to improve
* accuracy.
*
* @param int $post_id The post to normalize
* @param int $post_id The post to normalize
* @param string $post_content The post content to normalize
* @return string
*/
public function normalize( $post_id ) {
public function normalize( $post_id, $post_content = '' ) {
$post = get_post( $post_id );
$post_content = apply_filters( 'the_content', $post->post_content );
$post_content = empty( $post_content ) ? apply_filters( 'the_content', $post->post_content ) : $post_content;
$post_title = apply_filters( 'the_title', $post->post_title );

/* Strip shortcodes but keep internal caption text */
Expand Down
6 changes: 5 additions & 1 deletion src/js/gutenberg-plugins/post-status-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const PostStatusInfo = () => {
}

const postId = select( 'core/editor' ).getCurrentPostId();
const postContent =
select( 'core/editor' ).getEditedPostAttribute( 'content' );
const openModal = () => setOpen( true );
const closeModal = () =>
setOpen( false ) && setData( [] ) && setError( false );
Expand All @@ -45,6 +47,8 @@ const PostStatusInfo = () => {
openModal();
apiFetch( {
path,
method: 'POST',
data: { id: postId, content: postContent },
} ).then(
( res ) => {
setData( res );
Expand Down Expand Up @@ -111,7 +115,7 @@ const PostStatusInfo = () => {
</Modal>
) }
{ classifaiChatGPTData.enabledFeatures.map( ( feature ) => {
const path = feature?.path + postId;
const path = feature?.path;
return (
<PostTypeSupportCheck
key={ feature?.feature }
Expand Down
71 changes: 47 additions & 24 deletions src/js/post-excerpt/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import { __ } from '@wordpress/i18n';
import { Button, ExternalLink, TextareaControl } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { handleClick } from '../helpers';
import { useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';

/**
* PostExcerpt component.
Expand All @@ -23,15 +20,39 @@ import { handleClick } from '../helpers';
* @param {Function} props.onUpdateExcerpt Callback to update the post excerpt.
*/
function PostExcerpt( { excerpt, onUpdateExcerpt } ) {
const [ isLoading, setIsLoading ] = useState( false );
const [ error, setError ] = useState( false );

const { select } = wp.data;
const postId = select( 'core/editor' ).getCurrentPostId();
const postContent =
select( 'core/editor' ).getEditedPostAttribute( 'content' );
const buttonText =
'' === excerpt
? __( 'Generate excerpt', 'classifai' )
: __( 'Re-generate excerpt', 'classifai' );
const isPublishPanelOpen =
select( 'core/edit-post' ).isPublishSidebarOpened();

const buttonClick = async ( path ) => {
setIsLoading( true );
apiFetch( {
path,
method: 'POST',
data: { id: postId, content: postContent },
} ).then(
( res ) => {
onUpdateExcerpt( res );
setError( false );
setIsLoading( false );
},
( err ) => {
setError( err?.message );
setIsLoading( false );
}
);
};

return (
<div className="editor-post-excerpt">
<TextareaControl
Expand All @@ -57,30 +78,32 @@ function PostExcerpt( { excerpt, onUpdateExcerpt } ) {
<Button
className="classifai-post-excerpt"
variant={ 'secondary' }
disabled={ isLoading }
data-id={ postId }
onClick={ ( e ) =>
handleClick( {
button: e.target,
endpoint: '/classifai/v1/openai/generate-excerpt/',
callback: onUpdateExcerpt,
buttonText,
} )
onClick={ () =>
buttonClick( '/classifai/v1/openai/generate-excerpt/' )
}
>
{ buttonText }
</Button>
<span
className="spinner"
style={ { display: 'none', float: 'none' } }
></span>
<span
className="error"
style={ {
display: 'none',
color: '#bc0b0b',
paddingTop: '5px',
} }
></span>
{ isLoading && (
<span
className="spinner is-active"
style={ { float: 'none' } }
></span>
) }
{ error && ! isLoading && (
<span
className="error"
style={ {
color: '#bc0b0b',
display: 'inline-block',
paddingTop: '5px',
} }
>
{ error }
</span>
) }
</div>
);
}
Expand Down

0 comments on commit aa63866

Please sign in to comment.