Store Customization > Fetch product images from the Pexels API (https://github.com/woocommerce/woocommerce-blocks/pull/11280)

* Introduce the ProductUpdater class.

* Update the image assignment for the default products content.

* Update the default products content.

* Introduce the generate_content and get_placeholder_products methods.

* Update the get_placeholder_products method and introduce the new create_new_product method.

* Verify the hash of the product content and compare it with the hash of the ai generated content to ensure we wont override products modified by the store owner.

* Add docblocks and update the structure for the ProductUpdater class.

* Pass the vertical images as a param for the PatternUpdater and the ProductUpdater.

* Update the provided variable to the PatternUpdater class.

* Update the ProductUpdater class to include the requirements for usage of the media_sideload_image method outside the wp-admin area (via API) and other adjustments to the products generation and hashes

* remove unnecessary calls to post/product images.

* Update the loop for generating products.

* Further adjustments to the Product Updater.

* Provide the business_description as a param for the generate_content ProductUpdates

* Update the default timeout for the AI connection; update the path to the default image. Add a limit of 6 when quering the total products created by the store owner.

* Add new neutral placeholder images for products and patterns.

* Update the prompt and the logic for the placeholder images.

* Remove placeholder images.

* Pass the AI connection as a param and update the get_images_for_pattern method to rely on the results returned from the Pexels API.

* update the product updater class to rely on Pexels images and add the ai_connection as a param.

* Remove the unused get_random_images method.

* Update the patterns endpoint.

* Delete unused ChatGPTClient class.

* Introduce the new Pexels class.

* Remove the Verticals references.

* Update the reference for the alt description for images.

* Update the scheduled action to populate patterns and products.

* Remove unused Verticals classes.

* Ensure the Pexels class already returns the array with the expected format for assignment to Patterns and Products.

* Introduce the select_image_src_based_on_format method.

* Increase max execution time

* Increase max execution time

* Update the request to rely on the WP.com external-media endpoint instead.

* Improve performance for product content update.

* Improve quality of images used in products and update queries.

* Update the products query.

* Move the media_sideload_image function dependencies to outside of the loop and add comment.

* Update text content.

* Merge base branch

* Introduce the should_update_dummy_product method.

* Update the method to be triggered on scheduled action to return true.

* Change the image format for products to improve performance.

* Make portrait the default fallback image format.

* Address code review.

* bail early if no business description provided.

* Add an extra safety check in case of query errors.

* Address CR.

* Raise the default memory limit.

* Update the prompt for the search term for images.

* Make sure the 'woocommerce_blocks_allow_ai_connection' option is set to true if the site is connected to AI.

* Update the prompt for AI generated content in patterns and initialize the images and alts for the patterns.

* Update the prompt for products and introduce the update_dummy_products method.

* Update the default number of images returned by Pexels.

* Update the default fallback for the expected image format.

* Update the prompt for selecting the images.

* Add a character limit to the testimonials.

* Increase timeout to fetch products data

* Address code review.
This commit is contained in:
Patricia Hillebrandt 2023-10-26 17:56:31 +02:00 committed by GitHub
parent 83b75cb2ea
commit 88b62a46fa
10 changed files with 352 additions and 416 deletions

View File

@ -1,11 +1,11 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
use Automattic\WooCommerce\Blocks\Verticals\Client;
use Automattic\WooCommerce\Blocks\Verticals\VerticalsSelector;
/**
* Registers patterns under the `./patterns/` directory and updates their content.
@ -53,10 +53,37 @@ class BlockPatterns {
add_action( 'init', array( $this, 'register_block_patterns' ) );
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'schedule_on_option_update' ), 10, 2 );
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'update_ai_connection_allowed_option' ), 10, 2 );
add_action( 'upgrader_process_complete', array( $this, 'schedule_on_plugin_update' ), 10, 2 );
add_action( 'woocommerce_update_patterns_content', array( $this, 'update_patterns_content' ) );
}
/**
* Make sure the 'woocommerce_blocks_allow_ai_connection' option is set to true if the site is connected to AI.
*
* @param string $option The option name.
* @param string $value The option value.
*
* @return bool
*/
public function update_ai_connection_allowed_option( $option, $value ): bool {
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return update_option( 'woocommerce_blocks_allow_ai_connection', false );
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return update_option( 'woocommerce_blocks_allow_ai_connection', false );
}
return update_option( 'woocommerce_blocks_allow_ai_connection', true );
}
/**
* Registers the block patterns and categories under `./patterns/`.
*/
@ -262,7 +289,7 @@ class BlockPatterns {
*
* @param string $value The new value saved for the add_option_woo_ai_describe_store_description option.
*
* @return bool|int|string|\WP_Error
* @return bool|string|\WP_Error
*/
public function update_patterns_content( $value ) {
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
@ -274,16 +301,40 @@ class BlockPatterns {
);
}
$vertical_id = ( new VerticalsSelector() )->get_vertical_id( $value );
$ai_connection = new Connection();
if ( is_wp_error( $vertical_id ) ) {
return $vertical_id;
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id->get_error_message();
}
$vertical_images = ( new Client() )->get_vertical_images( $vertical_id );
$business_description = get_option( VerticalsSelector::STORE_DESCRIPTION_OPTION_KEY );
$token = $ai_connection->get_jwt_token( $site_id );
( new PatternUpdater() )->generate_content( $vertical_images, $business_description );
( new ProductUpdater() )->generate_content( $vertical_images, $business_description );
if ( is_wp_error( $token ) ) {
return $token->get_error_message();
}
$business_description = get_option( 'woo_ai_describe_store_description' );
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
return $images->get_error_message();
}
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_patterns ) ) {
return $populate_patterns->get_error_message();
}
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
return $populate_products->get_error_message();
}
return true;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace Automattic\WooCommerce\Blocks\Images;
use Automattic\WooCommerce\Blocks\AI\Connection;
/**
* Pexels API client.
*
* @internal
*/
class Pexels {
/**
* The Pexels API endpoint.
*/
const EXTERNAL_MEDIA_PEXELS_ENDPOINT = '/wpcom/v2/external-media/list/pexels';
/**
* Returns the list of images for the given search criteria.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param string $business_description The business description.
*
* @return array|\WP_Error Array of images, or WP_Error if the request failed.
*/
public function get_images( $ai_connection, $token, $business_description ) {
$search_term = $this->define_search_term( $ai_connection, $token, $business_description );
if ( is_wp_error( $search_term ) ) {
return $search_term;
}
return $this->request( $search_term );
}
/**
* Define the search term to be used on Pexels using the AI endpoint.
*
* The search term is a shorter description of the business.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param string $business_description The business description.
*
* @return mixed|\WP_Error
*/
private function define_search_term( $ai_connection, $token, $business_description ) {
$prompt = sprintf( 'Based on the description "%s", provide a one-word product description for the store\'s item. Do not include any adjectives or descriptions of the qualities of the product. The returned word should be simple.', $business_description );
$response = $ai_connection->fetch_ai_response( $token, $prompt );
if ( is_wp_error( $response ) || ! isset( $response['completion'] ) ) {
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woo-gutenberg-products-block' ) );
}
return $response['completion'];
}
/**
* Make a request to the Pexels API.
*
* @param string $search_term The search term to use.
* @param int $per_page The number of images to return.
*
* @return array|\WP_Error The response body, or WP_Error if the request failed.
*/
private function request( string $search_term, int $per_page = 90 ) {
$request = new \WP_REST_Request( 'GET', self::EXTERNAL_MEDIA_PEXELS_ENDPOINT );
$request->set_param( 'search', esc_html( $search_term ) );
$request->set_param( 'number', $per_page );
$response = rest_do_request( $request );
$response_data = $response->get_data();
if ( $response->is_error() ) {
$error_msg = [
'code' => $response->get_status(),
'data' => $response_data,
];
return new \WP_Error( 'pexels_api_error', __( 'Request to the Pexels API failed.', 'woo-gutenberg-products-block' ), $error_msg );
}
$response = $response_data['media'] ?? $response_data;
if ( is_array( $response ) ) {
shuffle( $response );
return $response;
}
return array();
}
}

View File

@ -3,27 +3,12 @@
namespace Automattic\WooCommerce\Blocks\Patterns;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Verticals\Client;
use Automattic\WooCommerce\Blocks\Verticals\VerticalsSelector;
use WP_Error;
/**
* Pattern Images class.
*/
class PatternUpdater {
/**
* The AI Connection.
*
* @var Connection
*/
private $ai_connection;
/**
* Constructor.
*/
public function __construct() {
$this->ai_connection = new Connection();
}
/**
* The patterns content option name.
@ -33,23 +18,25 @@ class PatternUpdater {
/**
* Creates the patterns content for the given vertical.
*
* @param array|WP_Error $vertical_images The array of vertical images.
* @param string $business_description The business description.
* @param Connection $ai_connection The AI connection.
* @param string|WP_Error $token The JWT token.
* @param array|WP_Error $images The array of images.
* @param string $business_description The business description.
*
* @return bool|WP_Error
*/
public function generate_content( $vertical_images, $business_description ) {
if ( is_wp_error( $vertical_images ) ) {
return $vertical_images;
public function generate_content( $ai_connection, $token, $images, $business_description ) {
if ( is_wp_error( $images ) ) {
return $images;
}
$patterns_with_images = $this->get_patterns_with_images( $vertical_images );
$patterns_with_images = $this->get_patterns_with_images( $images );
if ( is_wp_error( $patterns_with_images ) ) {
return new WP_Error( 'failed_to_set_pattern_images', __( 'Failed to set the pattern images.', 'woo-gutenberg-products-block' ) );
}
$patterns_with_images_and_content = $this->get_patterns_with_content( $patterns_with_images, $business_description );
$patterns_with_images_and_content = $this->get_patterns_with_content( $ai_connection, $token, $patterns_with_images, $business_description );
if ( is_wp_error( $patterns_with_images_and_content ) ) {
return new WP_Error( 'failed_to_set_pattern_content', __( 'Failed to set the pattern content.', 'woo-gutenberg-products-block' ) );
@ -71,11 +58,11 @@ class PatternUpdater {
/**
* Returns the patterns with images.
*
* @param array $vertical_images The array of vertical images.
* @param array $selected_images The array of images.
*
* @return array|WP_Error The patterns with images.
*/
private function get_patterns_with_images( $vertical_images ) {
private function get_patterns_with_images( $selected_images ) {
$patterns_dictionary = $this->get_patterns_dictionary();
if ( is_wp_error( $patterns_dictionary ) ) {
@ -90,7 +77,7 @@ class PatternUpdater {
continue;
}
list($images, $alts) = $this->get_images_for_pattern( $pattern, $vertical_images );
list( $images, $alts ) = $this->get_images_for_pattern( $pattern, $selected_images );
if ( empty( $images ) ) {
$patterns_with_images[] = $pattern;
continue;
@ -116,20 +103,14 @@ class PatternUpdater {
/**
* Returns the patterns with AI generated content.
*
* @param array $patterns The array of patterns.
* @param string $business_description The business description.
* @param Connection $ai_connection The AI connection.
* @param string|WP_Error $token The JWT token.
* @param array $patterns The array of patterns.
* @param string $business_description The business description.
*
* @return array|WP_Error The patterns with AI generated content.
*/
private function get_patterns_with_content( array $patterns, string $business_description ) {
$site_id = $this->ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $this->ai_connection->get_jwt_token( $site_id );
private function get_patterns_with_content( $ai_connection, $token, $patterns, $business_description ) {
if ( is_wp_error( $token ) ) {
return $token;
}
@ -139,12 +120,12 @@ class PatternUpdater {
$prompts = array();
foreach ( $patterns_with_content as $pattern ) {
$prompt = sprintf( 'Given the following store description: "%s", and the following JSON file representing the content of the "%s" pattern: %s.\n', $business_description, $pattern['name'], wp_json_encode( $pattern['content'] ) );
$prompt .= "Replace the titles, descriptions and button texts in each 'default' key using the prompt in the corresponding 'ai_prompt' key by a text that is related to the previous store description (but not the exact text) and matches the 'ai_prompt', the length of each replacement should be similar to the 'default' text length. The response should be only a JSON string, with absolutely no intro or explanations.";
$prompt .= "Replace the titles, descriptions and button texts in each 'default' key using the prompt in the corresponding 'ai_prompt' key by a text that is related to the previous store description (but not the exact text) and matches the 'ai_prompt', the length of each replacement should be similar to the 'default' text length. The text should not be written in first-person. The response should be only a JSON string, with absolutely no intro or explanations.";
$prompts[] = $prompt;
}
$responses = $this->ai_connection->fetch_ai_responses( $token, $prompts );
$responses = $ai_connection->fetch_ai_responses( $token, $prompts );
foreach ( $responses as $key => $response ) {
// If the AI response is invalid, we skip the pattern and keep the default content.
@ -194,52 +175,53 @@ class PatternUpdater {
/**
* Returns the images for the given pattern.
*
* @param array $pattern The array representing the pattern.
* @param array $vertical_images The array of vertical images.
* @param array $pattern The array representing the pattern.
* @param array $selected_images The array of images.
*
* @return array An array containing an array of the images in the first position and their alts in the second.
*/
private function get_images_for_pattern( array $pattern, array $vertical_images ): array {
$alts = array();
private function get_images_for_pattern( array $pattern, array $selected_images ): array {
$images = array();
if ( count( $vertical_images ) < $pattern['images_total'] ) {
return array( $images, $alts );
}
foreach ( $vertical_images as $vertical_image ) {
if ( $pattern['images_format'] === $this->get_image_format( $vertical_image ) ) {
$images[] = str_replace( 'http://', 'https://', $vertical_image['guid'] );
$alts[] = $vertical_image['meta']['pexels_object']['alt'] ?? '';
$alts = array();
foreach ( $selected_images as $selected_image ) {
if ( ! isset( $selected_image['title'] ) ) {
continue;
}
if ( ! isset( $selected_image['URL'] ) ) {
continue;
}
if ( str_contains( '.jpeg', $selected_image['title'] ) ) {
continue;
}
$expected_image_format = $pattern['images_format'] ?? 'portrait';
$selected_image_format = $this->get_selected_image_format( $selected_image );
if ( $selected_image_format !== $expected_image_format ) {
continue;
}
$images[] = $selected_image['URL'];
$alts[] = $selected_image['title'];
}
return array( $images, $alts );
}
/**
* Returns the image format for the given vertical image.
* Returns the selected image format. Defaults to landscape.
*
* @param array $vertical_image The vertical image.
* @param array $selected_image The selected image to be assigned to the pattern.
*
* @return string The image format, or an empty string if the image format is invalid.
* @return string The selected image format.
*/
private function get_image_format( array $vertical_image ): string {
if ( ! isset( $vertical_image['width'] ) || ! isset( $vertical_image['height'] ) ) {
return '';
}
if ( 0 === $vertical_image['width'] || 0 === $vertical_image['height'] ) {
return '';
}
if ( $vertical_image['width'] === $vertical_image['height'] ) {
return 'square';
}
if ( $vertical_image['width'] < $vertical_image['height'] ) {
private function get_selected_image_format( $selected_image ) {
if ( ! isset( $selected_image['width'], $selected_image['height'] ) ) {
return 'portrait';
}
return 'landscape';
return $selected_image['width'] === $selected_image['height'] ? 'square' : ( $selected_image['width'] > $selected_image['height'] ? 'landscape' : 'portrait' );
}
}

View File

@ -75,20 +75,6 @@ class PatternsHelper {
return $image;
}
/**
* Returns an array of random images.
*
* @param array $images The pattern images.
* @param int $images_total The total number of images needed for the pattern.
*
* @return array The random images.
*/
private static function get_random_images( array $images, int $images_total ): array {
shuffle( $images );
return array_slice( $images, 0, $images_total );
}
/**
* Get the Patterns Dictionary.
*

View File

@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\Blocks\Patterns;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
/**
* Pattern Images class.
*/
@ -10,12 +12,14 @@ class ProductUpdater {
/**
* Generate AI content and assign AI-managed images to Products.
*
* @param array $vertical_images The vertical images.
* @param string $business_description The business description.
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param array $images The array of images.
* @param string $business_description The business description.
*
* @return bool|\WP_Error True if the content was generated successfully, WP_Error otherwise.
* @return bool|WP_Error True if the content was generated successfully, WP_Error otherwise.
*/
public function generate_content( $vertical_images, $business_description ) {
public function generate_content( $ai_connection, $token, $images, $business_description ) {
if ( empty( $business_description ) ) {
return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woo-gutenberg-products-block' ) );
}
@ -67,18 +71,13 @@ class ProductUpdater {
$dummy_products_to_update = [];
foreach ( $dummy_products as $dummy_product ) {
$current_product_hash = $this->get_hash_for_product( $dummy_product );
$ai_modified_product_hash = $this->get_hash_for_ai_modified_product( $dummy_product );
if ( ! $dummy_product instanceof \WC_Product ) {
continue;
}
$date_created = $dummy_product->get_date_created()->date( 'Y-m-d H:i:s' );
$date_modified = $dummy_product->get_date_modified()->date( 'Y-m-d H:i:s' );
$should_update_dummy_product = $this->should_update_dummy_product( $dummy_product );
$timestamp_created = strtotime( $date_created );
$timestamp_modified = strtotime( $date_modified );
$dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60;
if ( $current_product_hash === $ai_modified_product_hash || $dummy_product_not_modified ) {
if ( $should_update_dummy_product ) {
$dummy_products_to_update[] = $dummy_product;
}
}
@ -87,50 +86,91 @@ class ProductUpdater {
return true;
}
$ai_selected_products_images = $this->get_images_information( $vertical_images );
$ai_selected_products_images = $this->get_images_information( $images );
$products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images );
$responses = $this->generate_product_content( $products_information_list );
$response = $this->generate_product_content( $ai_connection, $token, $products_information_list );
foreach ( $responses as $key => $response ) {
if ( is_wp_error( $response ) ) {
return $response;
}
if ( is_wp_error( $response ) ) {
$error_msg = $response;
} elseif ( empty( $response ) || ! isset( $response['completion'] ) ) {
$error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woo-gutenberg-products-block' ) );
}
if ( empty( $response ) ) {
return new \WP_Error( 'empty_response', __( 'The response from the AI service was empty.', 'woo-gutenberg-products-block' ) );
}
if ( isset( $error_msg ) ) {
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
if ( ! isset( $response['completion'] ) ) {
return $error_msg;
}
$product_content = json_decode( $response['completion'], true );
if ( is_null( $product_content ) ) {
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
return new \WP_Error( 'invalid_json', __( 'The response from the AI service is not a valid JSON.', 'woo-gutenberg-products-block' ) );
}
// This is required to allow the usage of the media_sideload_image function outside the context of /wp-admin/.
// See https://developer.wordpress.org/reference/functions/media_sideload_image/ for more details.
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$this->update_dummy_products( $dummy_products_to_update, $product_content );
return true;
}
/**
* Update the dummy products with the content from the information list.
*
* @param array $dummy_products_to_update The dummy products to update.
* @param array $products_information_list The products information list.
*/
public function update_dummy_products( $dummy_products_to_update, $products_information_list ) {
$i = 0;
foreach ( $dummy_products_to_update as $dummy_product ) {
if ( ! isset( $products_information_list[ $i ] ) ) {
continue;
}
$product_content = json_decode( $response['completion'], true );
$this->update_product_content( $dummy_product, $products_information_list[ $i ] );
++$i;
}
}
if ( is_null( $product_content ) ) {
continue;
}
/**
* Verify if the dummy product should have its content generated and managed by AI.
*
* @param \WC_Product $dummy_product The dummy product.
*
* @return bool
*/
public function should_update_dummy_product( $dummy_product ): bool {
$current_product_hash = $this->get_hash_for_product( $dummy_product );
$ai_modified_product_hash = $this->get_hash_for_ai_modified_product( $dummy_product );
// This is required to allow the usage of the media_sideload_image function outside the context of /wp-admin/.
// See https://developer.wordpress.org/reference/functions/media_sideload_image/ for more details.
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$date_created = $dummy_product->get_date_created();
$date_modified = $dummy_product->get_date_modified();
$i = 0;
foreach ( $dummy_products_to_update as $dummy_product ) {
$this->update_product_content( $dummy_product, $product_content[ $i ] );
++$i;
}
if ( ! $date_created instanceof \WC_DateTime || ! $date_modified instanceof \WC_DateTime ) {
return false;
}
$update_option = update_option( 'last_business_description_with_ai_content_generated', $business_description );
$formatted_date_created = $dummy_product->get_date_created()->date( 'Y-m-d H:i:s' );
$formatted_date_modified = $dummy_product->get_date_modified()->date( 'Y-m-d H:i:s' );
if ( ! $update_option ) {
return new \WP_Error( 'update_option_failed', __( 'The option last_business_description_with_ai_content_generated could not be updated.', 'woo-gutenberg-products-block' ) );
$timestamp_created = strtotime( $formatted_date_created );
$timestamp_modified = strtotime( $formatted_date_modified );
$dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60;
if ( $current_product_hash === $ai_modified_product_hash || $dummy_product_not_modified ) {
return true;
}
return $update_option;
return false;
}
/**
@ -233,10 +273,11 @@ class ProductUpdater {
if ( ! isset( $ai_generated_product_content['image']['src'] ) || ! isset( $ai_generated_product_content['image']['alt'] ) || ! isset( $ai_generated_product_content['title'] ) || ! isset( $ai_generated_product_content['description'] ) ) {
return;
}
// Since the media_sideload_image function can take longer to complete
// the process of downloading the external image and uploading it
// to the media library, we need to ensure the request doesn't timeout.
// Since the media_sideload_image function is expensive and can take longer to complete
// the process of downloading the external image and uploading it to the media library,
// here we are increasing the time limit and the memory limit to avoid any issues.
set_time_limit( 60 );
wp_raise_memory_limit();
$product_image_id = media_sideload_image( $ai_generated_product_content['image']['src'], $product->get_id(), $ai_generated_product_content['image']['alt'], 'id' );
@ -303,39 +344,32 @@ class ProductUpdater {
/**
* Get the images information.
*
* @param array $vertical_images The vertical images.
* @param array $images The array of images.
*
* @return array
*/
public function get_images_information( $vertical_images ) {
if ( is_wp_error( $vertical_images ) ) {
public function get_images_information( $images ) {
if ( is_wp_error( $images ) ) {
return [
'src' => esc_url( 'images/block-placeholders/product-image-gallery.svg' ),
'src' => 'images/block-placeholders/product-image-gallery.svg',
'alt' => 'The placeholder for a product image.',
];
}
$count = 0;
$placeholder_images = [];
foreach ( $vertical_images as $vertical_image ) {
foreach ( $images as $image ) {
if ( $count >= 6 ) {
break;
}
if ( isset( $vertical_image['meta']['pexels_object']['src']['large'] ) ) {
$src = $vertical_image['meta']['pexels_object']['src']['large'];
$alt = $vertical_image['meta']['pexels_object']['alt'] ?? 'The placeholder for a product image.';
} elseif ( isset( $vertical_image['guid'] ) ) {
$src = $vertical_image['guid'];
$alt = $vertical_image['meta']['pexels_object']['alt'] ?? 'The placeholder for a product image.';
} else {
$src = 'images/pattern-placeholders/white-texture-floor-wall-gray-tile.jpg';
$alt = 'The placeholder for a product image.';
if ( ! isset( $image['title'] ) || ! isset( $image['thumbnails']['medium'] ) ) {
continue;
}
$placeholder_images[] = [
'src' => esc_url( $src ),
'alt' => esc_attr( $alt ),
'src' => esc_url( $image['thumbnails']['medium'] ),
'alt' => esc_attr( $image['title'] ),
];
++ $count;
@ -347,33 +381,21 @@ class ProductUpdater {
/**
* Generate the product content.
*
* @param array $products_default_content The default content for the products.
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param array $products_default_content The default content for the products.
*
* @return array|int|string|\WP_Error
*/
public function generate_product_content( $products_default_content ) {
$ai_connection = new \Automattic\WooCommerce\Blocks\AI\Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
public function generate_product_content( $ai_connection, $token, $products_default_content ) {
$store_description = get_option( 'woo_ai_describe_store_description' );
if ( ! $store_description ) {
return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate the content for your site.', 'woo-gutenberg-products-block' ) );
}
$prompt = [ sprintf( 'Given the following store description: "%1s" and the assigned value for the alt property in the json bellow, generate new titles and descriptions for each one of the products listed bellow and assign them as the new values for the json: %2s. Each one of the titles should be unique and no numbers are allowed. The response should be only a JSON string, with no intro or explanations.', $store_description, wp_json_encode( $products_default_content ) ) ];
$prompt = sprintf( 'Given the following business description: "%1s" and the assigned value for the alt property in the json bellow, generate new titles and descriptions for each one of the products listed bellow and assign them as the new values for the json: %2s. Each one of the titles should be unique and must be limited to 29 characters. The response should be only a JSON string, with no intro or explanations.', $store_description, wp_json_encode( $products_default_content ) );
return $ai_connection->fetch_ai_responses( $token, $prompt, 60 );
return $ai_connection->fetch_ai_response( $token, $prompt, 60 );
}
}

View File

@ -594,15 +594,15 @@
"descriptions": [
{
"default": "In the end the couch wasn't exactly what I was looking for but my experience with the Burrow team was excellent. First in providing a discount when the couch was delayed.",
"ai_prompt": "A description of the first testimonial"
"ai_prompt": "A description of the first testimonial. The testimonial must have less than 200 characters."
},
{
"default": "Great couch. color as advertise. seat is nice and firm. Easy to put together. Versatile. Bought one for my mother in law as well. And she loves hers!",
"ai_prompt": "A description of the second testimonial"
"ai_prompt": "A description of the second testimonial. The testimonial must have less than 200 characters."
},
{
"default": "I got the kind sofa. The look and feel is high quality, and I enjoy that it is a medium level of firmness. Assembly took a little longer than I expected, and it came in 4 boxes.",
"ai_prompt": "A description of the third testimonial"
"ai_prompt": "A description of the third testimonial. The testimonial must have less than 200 characters."
}
]
}
@ -622,7 +622,7 @@
"descriptions": [
{
"default": "In the end the couch wasn't exactly what I was looking for but my experience with the Burrow team was excellent. First in providing a discount when the couch was delayed, then timely feedback and updates as the...\n\n~ Anna W.",
"ai_prompt": "A description of the testimonial"
"ai_prompt": "A description of the testimonial. The testimonial must have less than 200 characters."
}
]
}

View File

@ -2,10 +2,10 @@
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
use Automattic\WooCommerce\Blocks\Verticals\Client;
use Automattic\WooCommerce\Blocks\Verticals\VerticalsSelector;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
@ -103,27 +103,58 @@ class Patterns extends AbstractRoute {
}
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
$vertical_id = ( new VerticalsSelector() )->get_vertical_id( $business_description );
if ( empty( $business_description ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
}
if ( is_wp_error( $vertical_id ) ) {
$response = $this->error_to_response( $vertical_id );
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
if ( $last_business_description === $business_description ) {
return rest_ensure_response(
$this->prepare_item_for_response(
[
'ai_content_generated' => true,
],
$request
)
);
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
$response = $this->error_to_response( $images );
} else {
$vertical_images = ( new Client() )->get_vertical_images( $vertical_id );
$populate_patterns = ( new PatternUpdater() )->generate_content( $vertical_images, $business_description );
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_patterns ) ) {
$response = $this->error_to_response( $populate_patterns );
}
$populate_products = ( new ProductUpdater() )->generate_content( $vertical_images, $business_description );
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
$response = $this->error_to_response( $populate_products );
}
if ( true === $populate_patterns && true === $populate_products ) {
update_option( 'last_business_description_with_ai_content_generated', $business_description );
}
}
if ( ! isset( $response ) ) {

View File

@ -1,16 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Verticals;
use WP_Error;
interface ChatGPTClient {
/**
* Returns a text completion from the GPT API.
*
* @param string $prompt The prompt to send to the GPT API.
*
* @return string|WP_Error The text completion, or WP_Error if the request failed.
*/
public function text_completion( string $prompt );
}

View File

@ -1,76 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Verticals;
/**
* Verticals API client.
*/
class Client {
const ENDPOINT = 'https://public-api.wordpress.com/wpcom/v2/site-verticals';
/**
* Make a request to the Verticals API.
*
* @param string $url The endpoint URL.
*
* @return array|\WP_Error The response body, or WP_Error if the request failed.
*/
private function request( string $url ) {
$response = wp_remote_get( $url );
$response_code = wp_remote_retrieve_response_code( $response );
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
$error_data = array();
if ( is_wp_error( $response ) ) {
$error_data['code'] = $response->get_error_code();
$error_data['message'] = $response->get_error_message();
}
if ( 200 !== $response_code ) {
$error_data['status'] = $response_code;
if ( isset( $response_body['message'] ) ) {
$error_data['message'] = $response_body['message'];
}
if ( isset( $response_body['code'] ) ) {
$error_data['code'] = $response_body['code'];
}
}
if ( ! empty( $error_data ) ) {
return new \WP_Error( 'verticals_api_error', __( 'Request to the Verticals API failed.', 'woo-gutenberg-products-block' ), $error_data );
}
return $response_body;
}
/**
* Returns a list of verticals that have images.
*
* @return array|\WP_Error Array of verticals, or WP_Error if the request failed.
*/
public function get_verticals() {
$response = $this->request( self::ENDPOINT );
if ( is_wp_error( $response ) ) {
return $response;
}
return array_filter(
$response,
function ( $vertical ) {
return $vertical['has_vertical_images'];
}
);
}
/**
* Returns the list of images for the given vertical ID.
*
* @param int $vertical_id The vertical ID.
*
* @return array|\WP_Error Array of images, or WP_Error if the request failed.
*/
public function get_vertical_images( int $vertical_id ) {
return $this->request( self::ENDPOINT . '/' . $vertical_id . '/images' );
}
}

View File

@ -1,141 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Verticals;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Verticals\Client as VerticalsAPIClient;
/**
* VerticalsSelector class.
*/
class VerticalsSelector {
public const STORE_DESCRIPTION_OPTION_KEY = 'woo_ai_describe_store_description';
/**
* The verticals API client.
*
* @var VerticalsAPIClient
*/
private $verticals_api_client;
/**
* The AI Connection.
*
* @var Connection
*/
private $ai_connection;
/**
* Constructor.
*/
public function __construct() {
$this->verticals_api_client = new VerticalsAPIClient();
$this->ai_connection = new Connection();
}
/**
* Gets the vertical id that better matches the business description using the GPT API.
*
* @param string $business_description The business description.
*
* @return string|\WP_Error The vertical id, or WP_Error if the request failed.
*/
public function get_vertical_id( $business_description = '' ) {
if ( empty( $business_description ) ) {
$business_description = $this->get_business_description();
}
if ( ! is_string( $business_description ) ) {
return new \WP_Error(
'missing_business_description',
__( 'The business description is required to generate the content for your site.', 'woo-gutenberg-products-block' )
);
}
$verticals = $this->verticals_api_client->get_verticals();
if ( is_wp_error( $verticals ) ) {
return $verticals;
}
$prompt = $this->build_prompt( $verticals, $business_description );
$site_id = $this->ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $this->ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$ai_response = $this->ai_connection->fetch_ai_response( $token, $prompt );
if ( is_wp_error( $ai_response ) ) {
return $ai_response;
}
if ( ! isset( $ai_response['completion'] ) ) {
return new \WP_Error( 'invalid_ai_response', __( 'The AI response is invalid.', 'woo-gutenberg-products-block' ) );
}
return $this->parse_answer( $ai_response['completion'] );
}
/**
* Get the business description from the AI settings in WooCommerce.
*
* @return string The business description.
*/
private function get_business_description(): string {
return get_option( self::STORE_DESCRIPTION_OPTION_KEY, '' );
}
/**
* Build the prompt to send to the GPT API.
*
* @param array $verticals The list of verticals.
* @param string $business_description The business description.
*
* @return string The prompt to send to the GPT API.
*/
private function build_prompt( array $verticals, string $business_description ): string {
$verticals = array_map(
function ( $vertical ) {
return "[ID=${vertical['id']}, Name=\"${vertical['name']}\"]";
},
$verticals
);
if ( empty( $verticals ) ) {
return '';
}
$verticals = implode( ', ', $verticals );
return sprintf(
'Filter the objects provided below and return the one that has a title that better matches this description of an online store with the following description: "%s". The response should include exclusively the ID of the object that better matches. The response should be a number, with absolutely no texts and without any explanations \n %s.',
$business_description,
$verticals
);
}
/**
* Parse the answer from the GPT API and return the id of the selected vertical.
*
* @param string $ai_response The answer from the GPT API.
*
* @return int|\WP_Error The id of the selected vertical.
*/
private function parse_answer( $ai_response ) {
$vertical_id = preg_replace( '/[^0-9]/', '', $ai_response );
if ( ! is_numeric( $vertical_id ) ) {
return new \WP_Error( 'invalid_ai_response', __( 'The AI response is invalid.', 'woo-gutenberg-products-block' ) );
}
return (int) $vertical_id;
}
}