Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c2f6752
Blocks: add skeleton for 2 new CoAuthor blocks
jeherve Dec 21, 2022
44989b3
Add new endpoint to fetch CoAuthor responses from WordPress.com.
jeherve Dec 21, 2022
b9938bc
Introduce new helper lib for CoAuthor
jeherve Dec 21, 2022
f365e85
Add changelog
jeherve Dec 21, 2022
8c7eeeb
Merge remote-tracking branch 'origin/trunk' into add/coauthor-block
jeherve Jan 4, 2023
d0d0e12
switch to the lib deployed on wpcom
artpi Jan 5, 2023
07ec87f
caching
artpi Jan 5, 2023
cf771cb
move blocks to experimental so that they are only on wpcom
artpi Jan 5, 2023
225dd16
Merge branch 'trunk' into add/coauthor-block
artpi Jan 5, 2023
56ecf13
fix endpoint
artpi Jan 5, 2023
813fa64
Coauthor image is fully ported to JP
artpi Jan 6, 2023
45ec5c5
css
artpi Jan 6, 2023
f1b1a14
coauthor block is working
artpi Jan 6, 2023
1b0f3c2
blocks now back at beta
artpi Jan 6, 2023
47b4cd7
block tweaks
artpi Jan 6, 2023
ae6a51e
transients
artpi Jan 6, 2023
f29b61b
better description
artpi Jan 6, 2023
0e8e076
only on atomic
artpi Jan 6, 2023
f68e6ee
translations and error msg
artpi Jan 9, 2023
8f7938a
error msgs
artpi Jan 9, 2023
a77ad5a
rename coauthor to jetpack ai
artpi Jan 10, 2023
73a9815
Caching makes more sense
artpi Jan 10, 2023
b676f0c
restore composer.lock
artpi Jan 10, 2023
9a71c9e
Fix for atomic sites
artpi Jan 10, 2023
c48485c
Merge branch 'trunk' into add/coauthor-block
artpi Jan 11, 2023
bd03dde
Merge remote-tracking branch 'origin/trunk' into add/coauthor-block
jeherve Jan 11, 2023
28eb28d
Switch to new shared utility.
jeherve Jan 11, 2023
9e7cbc8
Error handling
artpi Jan 11, 2023
8121709
Add missing textdomain / translations
jeherve Jan 11, 2023
4e73e7a
Merge branch 'add/coauthor-block' of github.com:Automattic/jetpack in…
jeherve Jan 11, 2023
f91e270
Remove deprecated attribute
jeherve Jan 11, 2023
b194cb6
[not verified] stuff
artpi Jan 11, 2023
b77b96e
Remove Jetpack from blcok names
artpi Jan 11, 2023
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
208 changes: 208 additions & 0 deletions projects/plugins/jetpack/_inc/lib/class-jetpack-ai-helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php
/**
* API helper for the AI blocks.
*
* @package automattic/jetpack
* @since $$next-version$$
*/

use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Status;

/**
* Class Jetpack_AI_Helper
*
* @since $$next-version$$
*/
class Jetpack_AI_Helper {
/**
* Allow new completion every X seconds. Will return cached result otherwise.
*
* @var int
*/
public static $text_completion_cooldown_seconds = 60;

/**
* Cache images for a prompt for a month.
*
* @var int
*/
public static $image_generation_cache_timeout = MONTH_IN_SECONDS;

/**
* Checks if a given request is allowed to get AI data from WordPress.com.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return true|WP_Error True if the request has access, WP_Error object otherwise.
*/
public static function get_status_permission_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable

/*
* This may need to be updated
* to take into account the different ways we can make requests
* (from a WordPress.com site, from a Jetpack site).
*/
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_forbidden',
__( 'Sorry, you are not allowed to access Jetpack AI help on this site.', 'jetpack' ),
array( 'status' => rest_authorization_required_code() )
);
}

return true;
}

/**
* Get the name of the transient for image generation. Unique per prompt and allows for reuse of results for the same prompt across entire WPCOM.
* I expext "puppy" to always be from cache.
*
* @param string $prompt - Supplied prompt.
*/
public static function transient_name_for_image_generation( $prompt ) {
return 'jetpack_openai_image_' . md5( $prompt );
}

/**
* Get the name of the transient for text completion. Unique per user, but not per text. Serves more as a cooldown.
*/
public static function transient_name_for_completion() {
return 'jetpack_openai_completion_' . get_current_user_id(); // Cache for each user, so that other users dont get weird cached version from somebody else.
}

/**
* Get text back from WordPress.com based off a starting text.
*
* @param string $content The content that's already been typed in the block.
* @return mixed
*/
public static function get_gpt_completion( $content ) {
$content = wp_strip_all_tags( $content );
$cache = get_transient( self::transient_name_for_completion() );
if ( $cache ) {
return $cache;
}

if ( ( new Status() )->is_offline_mode() ) {
return new WP_Error(
'dev_mode',
__( 'Jetpack AI is not available in offline mode.', 'jetpack' )
);
}

$site_id = Manager::get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}

if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! class_exists( 'OpenAI' ) ) {
\require_lib( 'openai' );
}

$result = ( new OpenAI( 'openai' ) )->request_gpt_completion( $content );
if ( is_wp_error( $result ) ) {
return $result;
}
// In case of Jetpack we are setting a transient on the WPCOM and not the remote site. I think the 'get_current_user_id' may default for the connection owner at this point but we'll deal with this later.
set_transient( self::transient_name_for_completion(), $result, self::$text_completion_cooldown_seconds );
return $result;
}

$response = Client::wpcom_json_api_request_as_user(
sprintf( '/sites/%d/jetpack-ai/completions', $site_id ),
2,
array(
'method' => 'post',
'headers' => array( 'content-type' => 'application/json' ),
),
wp_json_encode(
array(
'content' => $content,
)
),
'wpcom'
);

if ( is_wp_error( $response ) ) {
return $response;
}

$data = json_decode( wp_remote_retrieve_body( $response ) );

if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
return new WP_Error( $data->code, $data->message, $data->data );
}
set_transient( self::transient_name_for_completion(), $data, self::$text_completion_cooldown_seconds );

return $data;
}

/**
* Get an array of image objects back from WordPress.com based off a prompt.
*
* @param string $prompt The prompt to generate images for.
* @return mixed
*/
public static function get_dalle_generation( $prompt ) {
$cache = get_transient( self::transient_name_for_image_generation( $prompt ) );
if ( $cache ) {
return $cache;
}

if ( ( new Status() )->is_offline_mode() ) {
return new WP_Error(
'dev_mode',
__( 'Jetpack AI is not available in offline mode.', 'jetpack' )
);
}

$site_id = Manager::get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}

if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! class_exists( 'OpenAI' ) ) {
\require_lib( 'openai' );
}

$result = ( new OpenAI( 'openai' ) )->request_dalle_generation( $prompt );
if ( is_wp_error( $result ) ) {
return $result;
}
set_transient( self::transient_name_for_image_generation( $prompt ), $result, self::$image_generation_cache_timeout );
return $result;
}

$response = Client::wpcom_json_api_request_as_user(
sprintf( '/sites/%d/jetpack-ai/images/generations', $site_id ),
2,
array(
'method' => 'post',
'headers' => array( 'content-type' => 'application/json' ),
),
wp_json_encode(
array(
'prompt' => $prompt,
)
),
'wpcom'
);

if ( is_wp_error( $response ) ) {
return $response;
}

$data = json_decode( wp_remote_retrieve_body( $response ) );

if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
return new WP_Error( $data->code, $data->message, $data->data );
}
set_transient( self::transient_name_for_image_generation( $prompt ), $data, self::$image_generation_cache_timeout );

return $data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/**
* REST API endpoint for the Jetpack AI blocks.
*
* @package automattic/jetpack
* @since $$next-version$$
*/

/**
* Class WPCOM_REST_API_V2_Endpoint_AI
*/
class WPCOM_REST_API_V2_Endpoint_AI extends WP_REST_Controller {
/**
* Namespace prefix.
*
* @var string
*/
public $namespace = 'wpcom/v2';

/**
* Endpoint base route.
*
* @var string
*/
public $rest_base = 'jetpack-ai';

/**
* WPCOM_REST_API_V2_Endpoint_AI constructor.
*/
public function __construct() {
$this->is_wpcom = false;

if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$this->is_wpcom = true;
$this->wpcom_is_wpcom_only_endpoint = true;
} elseif ( ! ( new Automattic\Jetpack\Status\Host() )->is_woa_site() ) {
// If this is not an atomic site, we want to bail and not even load the endpoint for now.
return;
}

if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
require_once JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
}

add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}

/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
$this->rest_base . '/completions',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'request_gpt_completion' ),
'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ),
),
'args' => array(
'content' => array( 'required' => true ),
'token' => array( 'required' => false ),
),
)
);
register_rest_route(
$this->namespace,
$this->rest_base . '/images/generations',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'request_dalle_generation' ),
'permission_callback' => array( 'Jetpack_AI_Helper', 'get_status_permission_check' ),
),
'args' => array(
'prompt' => array( 'required' => true ),
'token' => array( 'required' => false ),
),
)
);
}

/**
* Get completions for a given text.
*
* @param WP_REST_Request $request The request.
*/
public function request_gpt_completion( $request ) {
return Jetpack_AI_Helper::get_gpt_completion( $request['content'] );
}

/**
* Get image generations for a given prompt.
*
* @param WP_REST_Request $request The request.
*/
public function request_dalle_generation( $request ) {
return Jetpack_AI_Helper::get_dalle_generation( $request['prompt'] );
}
}

wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_AI' );
4 changes: 4 additions & 0 deletions projects/plugins/jetpack/changelog/add-ai-blocks
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Introduce AI-powered blocks for WordPress.com customers.
53 changes: 53 additions & 0 deletions projects/plugins/jetpack/extensions/blocks/ai-image/ai-image.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* Jetpack AI Image Block.
*
* @since $$next-version$$
*
* @package automattic/jetpack
*/

namespace Automattic\Jetpack\Extensions\AIImage;

use Automattic\Jetpack\Blocks;
use Automattic\Jetpack\Status\Host;
use Jetpack_Gutenberg;

const FEATURE_NAME = 'ai-image';
const BLOCK_NAME = 'jetpack/' . FEATURE_NAME;

/**
* Registers our block for use in Gutenberg
* This is done via an action so that we can disable
* registration if we need to.
*/
function register_block() {
if ( ( defined( 'IS_WPCOM' ) && IS_WPCOM ) || ( new Host() )->is_woa_site() ) {
Blocks::jetpack_register_block(
BLOCK_NAME,
array( 'render_callback' => __NAMESPACE__ . '\load_assets' )
);
}
}
add_action( 'init', __NAMESPACE__ . '\register_block' );

/**
* Jetpack AI image block registration/dependency declaration.
*
* @param array $attr Array containing the Jetpack AI image block attributes.
* @param string $content String containing the Jetpack AI image block content.
*
* @return string
*/
function load_assets( $attr, $content ) {
/*
* Enqueue necessary scripts and styles.
*/
Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME );

return sprintf(
'<div class="%1$s">%2$s</div>',
esc_attr( Jetpack_Gutenberg::block_classes( FEATURE_NAME, $attr ) ),
$content
);
}
10 changes: 10 additions & 0 deletions projects/plugins/jetpack/extensions/blocks/ai-image/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default {
content: {
type: 'string',
source: 'text',
},
requestedPrompt: {
type: 'string',
default: false,
},
};
Loading