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

[WIP] Add AWS Amazon Personalize as a Provider for the Recommended Content Feature #790

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
},
"extra": {
"aws/aws-sdk-php": [
"Polly"
"Personalize",
"PersonalizeEvents",
"PersonalizeRuntime",
"Polly"
]
}
}
1,030 changes: 369 additions & 661 deletions composer.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"title": "Recommended Content",
"description": "Display content recommended by Azure AI Personalizer",
"description": "Display personalized content recommended by AI",
"textdomain": "classifai",
"name": "classifai/recommended-content-block",
"category": "classifai-blocks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ReactComponent as icon } from '../../../../assets/img/block-icon.svg';
registerBlockType( block.name, {
title: __( 'Recommended Content', 'classifai' ),
description: __(
'Display content recommended by Azure AI Personalizer',
'Display personalized content recommended by AI',
'classifai'
),
edit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
namespace Classifai\Blocks\RecommendedContentBlock;

use Classifai\Features\RecommendedContent;
use Classifai\Providers\Azure\Personalizer;
use function Classifai\get_asset_info;

/**
Expand Down Expand Up @@ -54,8 +53,8 @@ function register() {
function render_block_callback( array $attributes ): string {
// Render block in Gutenberg Editor.
if ( defined( 'REST_REQUEST' ) && \REST_REQUEST ) {
$personalizer = new Personalizer( false );
return $personalizer->render_recommended_content( $attributes );
$provider_instance = ( new RecommendedContent() )->get_feature_provider_instance();
return $provider_instance->render_recommended_content( $attributes );
}

// Render block in Front-end.
Expand Down
5 changes: 3 additions & 2 deletions includes/Classifai/Features/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,9 @@ public function render_input( array $args ) {
switch ( $type ) {
case 'text':
case 'password':
$attrs = ' value="' . esc_attr( $value ) . '"';
$class = 'regular-text';
$placeholder = $args['placeholder'] ?? '';
$attrs = ' value="' . esc_attr( $value ) . '" placeholder="' . esc_attr( $placeholder ) . '"';
$class = 'regular-text';
break;
case 'number':
$attrs = ' value="' . esc_attr( $value ) . '"';
Expand Down
202 changes: 181 additions & 21 deletions includes/Classifai/Features/RecommendedContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

use Classifai\Services\Personalizer as PersonalizerService;
use Classifai\Providers\Azure\Personalizer as PersonalizerProvider;
use Classifai\Providers\AWS\AmazonPersonalize as PersonalizeProvider;
use Classifai\Blocks;
use WP_REST_Server;
use WP_REST_Request;
use WP_Error;

/**
* Class RecommendedContent
Expand All @@ -27,10 +32,186 @@ public function __construct() {

// Contains just the providers this feature supports.
$this->supported_providers = [
PersonalizeProvider::ID => __( 'Amazon AWS Personalize', 'classifai' ),
PersonalizerProvider::ID => __( 'Microsoft Azure AI Personalizer', '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.
*
* This only runs if is_feature_enabled() returns true.
*/
public function feature_setup() {
// Register the block.
Blocks\setup();

// AJAX callback for rendering recommended content.
add_action( 'wp_ajax_classifai_render_recommended_content', [ $this, 'ajax_render_recommended_content' ] );
add_action( 'wp_ajax_nopriv_classifai_render_recommended_content', [ $this, 'ajax_render_recommended_content' ] );

add_action( 'save_post', [ $this, 'maybe_clear_transient' ] );
}

/**
* Register any needed endpoints.
*/
public function register_endpoints() {
register_rest_route(
'classifai/v1',
'personalizer/reward/(?P<itemId>\d+)',
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'rest_endpoint_callback' ],
'args' => [
'itemId' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'description' => esc_html__( 'Item ID to track', 'classifai' ),
],
'event' => [
'required' => false,
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
],
'type' => [
'type' => 'string',
],
],
'sanitize_callback' => 'sanitize_text_field',
'description' => esc_html__( 'Event details to track', 'classifai' ),
],
'rewarded' => [
'required' => false,
'type' => 'string',
'enum' => [
'0',
'1',
],
'default' => '0',
'sanitize_callback' => 'sanitize_text_field',
'description' => esc_html__( 'Reward value we want to send', 'classifai' ),
],
],
'permission_callback' => [ $this, 'permissions_check' ],
]
);
}

/**
* Check if a given request has access to send reward.
*
* This check ensures that we are properly authenticated.
* TODO: add additional checks here, maybe a nonce check or rate limiting?
*
* @return WP_Error|bool
*/
public function permissions_check() {
// Check if valid authentication is in place.
if ( ! $this->is_enabled() ) {
return new WP_Error( 'not_enabled', esc_html__( 'Recommended Content 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/personalizer/reward' ) === 0 ) {
return rest_ensure_response(
$this->run(
$request->get_param( 'itemId' ),
'reward',
[
'event' => $request->get_param( 'event' ),
'reward' => $request->get_param( 'rewarded' ),
]
)
);
}

return parent::rest_endpoint_callback( $request );
}

/**
* Render recommended content over AJAX.
*/
public function ajax_render_recommended_content() {
check_ajax_referer( 'classifai-recommended-block', 'security' );

if ( ! isset( $_POST['contentPostType'] ) || empty( $_POST['contentPostType'] ) ) {
esc_html_e( 'No results found.', 'classifai' );
exit();
}

$attributes = [
'displayLayout' => isset( $_POST['displayLayout'] ) ? sanitize_text_field( wp_unslash( $_POST['displayLayout'] ) ) : 'grid',
'contentPostType' => sanitize_text_field( wp_unslash( $_POST['contentPostType'] ) ),
'excludeId' => isset( $_POST['excludeId'] ) ? absint( $_POST['excludeId'] ) : 0,
'displayPostExcerpt' => isset( $_POST['displayPostExcerpt'] ) ? filter_var( wp_unslash( $_POST['displayPostExcerpt'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false,
'displayAuthor' => isset( $_POST['displayAuthor'] ) ? filter_var( wp_unslash( $_POST['displayAuthor'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false,
'displayPostDate' => isset( $_POST['displayPostDate'] ) ? filter_var( wp_unslash( $_POST['displayPostDate'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false,
'displayFeaturedImage' => isset( $_POST['displayFeaturedImage'] ) ? filter_var( wp_unslash( $_POST['displayFeaturedImage'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : true,
'addLinkToFeaturedImage' => isset( $_POST['addLinkToFeaturedImage'] ) ? filter_var( wp_unslash( $_POST['addLinkToFeaturedImage'] ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : false,
'columns' => isset( $_POST['columns'] ) ? absint( $_POST['columns'] ) : 3,
'numberOfItems' => isset( $_POST['numberOfItems'] ) ? absint( $_POST['numberOfItems'] ) : 3,
];

if ( isset( $_POST['taxQuery'] ) && ! empty( $_POST['taxQuery'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
foreach ( $_POST['taxQuery'] as $key => $value ) {
$attributes['taxQuery'][ $key ] = array_map( 'absint', $value );
}
}

$provider_instance = $this->get_feature_provider_instance();

echo $provider_instance->render_recommended_content( $attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

exit();
}

/**
* Maybe clear transients for recent actions.
*
* @param int $post_id Post Id.
*/
public function maybe_clear_transient( int $post_id ) {
global $wpdb;

$post_type = get_post_type( $post_id );

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$transients = $wpdb->get_col( $wpdb->prepare( "SELECT `option_name` FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_classifai_actions_' . $post_type . '%' ) );

// Delete all transients
if ( ! empty( $transients ) ) {
foreach ( $transients as $transient ) {
delete_transient( str_replace( '_transient_', '', $transient ) );
}
}
}

/**
* Get the description for the enable field.
*
Expand All @@ -51,27 +232,6 @@ public function get_feature_default_settings(): array {
];
}

/**
* Runs the feature.
*
* @param mixed ...$args Arguments required by the feature depending on the provider selected.
* @return mixed
*/
public function run( ...$args ) {
$settings = $this->get_settings();
$provider_id = $settings['provider'] ?? PersonalizerProvider::ID;
$provider_instance = $this->get_feature_provider_instance( $provider_id );
$result = '';

if ( PersonalizerProvider::ID === $provider_instance::ID ) {
/** @var PersonalizerProvider $provider_instance */
$result = call_user_func_array(
[ $provider_instance, 'personalizer_send_reward' ],
[ ...$args ]
);
}
}

/**
* Generates feature setting data required for migration from
* ClassifAI < 3.0.0 to 3.0.0
Expand Down
Loading
Loading