UpdateProducts.php
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
/**
* Pattern Images class.
*/
class UpdateProducts {
/**
* The dummy products.
*/
const DUMMY_PRODUCTS = [
[
'title' => 'Vintage Typewriter',
'image' => 'assets/images/pattern-placeholders/writing-typing-keyboard-technology-white-vintage.jpg',
'description' => 'A hit spy novel or a love letter? Anything you type using this vintage typewriter from the 20s is bound to make a mark.',
'price' => 90,
],
[
'title' => 'Leather-Clad Leisure Chair',
'image' => 'assets/images/pattern-placeholders/table-wood-house-chair-floor-window.jpg',
'description' => 'Sit back and relax in this comfy designer chair. High-grain leather and steel frame add luxury to your your leisure.',
'price' => 249,
],
[
'title' => 'Black and White Summer Portrait',
'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg',
'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.',
'price' => 115,
],
[
'title' => '3-Speed Bike',
'image' => 'assets/images/pattern-placeholders/road-sport-vintage-wheel-retro-old.jpg',
'description' => 'Zoom through the streets on this premium 3-speed bike. Manufactured and assembled in Germany in the 80s.',
'price' => 115,
],
[
'title' => 'Hi-Fi Headphones',
'image' => 'assets/images/pattern-placeholders/man-person-music-black-and-white-white-photography.jpg',
'description' => 'Experience your favorite songs in a new way with these premium hi-fi headphones.',
'price' => 125,
],
[
'title' => 'Retro Glass Jug (330 ml)',
'image' => 'assets/images/pattern-placeholders/drinkware-liquid-tableware-dishware-bottle-fluid.jpg',
'description' => 'Thick glass and a classic silhouette make this jug a must-have for any retro-inspired kitchen.',
'price' => 115,
],
];
/**
* Generate AI content and assign AI-managed images to Products.
*
* @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 array|WP_Error The generated content for the products. An error if the content could not be generated.
*/
public function generate_content( $ai_connection, $token, $images, $business_description ) {
if ( is_wp_error( $token ) ) {
return $token;
}
$images = ContentProcessor::verify_images( $images, $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
return $images;
}
if ( empty( $business_description ) ) {
return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
}
$dummy_products_to_update = $this->fetch_dummy_products_to_update();
if ( is_wp_error( $dummy_products_to_update ) ) {
return $dummy_products_to_update;
}
if ( empty( $dummy_products_to_update ) ) {
return array(
'product_content' => array(),
);
}
$products_information_list = $this->assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $images['images'] );
return $this->assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description, $images['search_term'] );
}
/**
* Return all dummy products that were not modified by the store owner.
*
* @return array|WP_Error An array with the dummy products that need to have their content updated by AI.
*/
public function fetch_dummy_products_to_update() {
$real_products = $this->fetch_product_ids();
$real_products_count = count( $real_products );
if ( is_array( $real_products ) && $real_products_count > 6 ) {
return array(
'product_content' => array(),
);
}
$dummy_products = $this->fetch_product_ids( 'dummy' );
$dummy_products_count = count( $dummy_products );
$products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] );
$products_to_create--;
}
// Identify dummy products that need to have their content updated.
$dummy_products_ids = $this->fetch_product_ids( 'dummy' );
if ( ! is_array( $dummy_products_ids ) ) {
return new \WP_Error( 'failed_to_fetch_dummy_products', __( 'Failed to fetch dummy products.', 'woocommerce' ) );
}
$dummy_products = array_map(
function ( $product ) {
return wc_get_product( $product->ID );
},
$dummy_products_ids
);
$dummy_products_to_update = [];
foreach ( $dummy_products as $dummy_product ) {
if ( ! $dummy_product instanceof \WC_Product ) {
continue;
}
$should_update_dummy_product = $this->should_update_dummy_product( $dummy_product );
if ( $should_update_dummy_product ) {
$dummy_products_to_update[] = $dummy_product;
}
}
return $dummy_products_to_update;
}
/**
* 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 );
$date_created = $dummy_product->get_date_created();
$date_modified = $dummy_product->get_date_modified();
if ( ! $date_created instanceof \WC_DateTime || ! $date_modified instanceof \WC_DateTime ) {
return false;
}
$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' );
$timestamp_created = strtotime( $formatted_date_created );
$timestamp_modified = strtotime( $formatted_date_modified );
$timestamp_current = time();
$dummy_product_recently_modified = abs( $timestamp_current - $timestamp_modified ) < 10;
$dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60;
if ( $current_product_hash === $ai_modified_product_hash || $dummy_product_not_modified || $dummy_product_recently_modified ) {
return true;
}
return false;
}
/**
* Creates a new product and assigns the _headstart_post meta to it.
*
* @param array $product_data The product data.
*
* @return bool|int|\WP_Error
*/
public function create_new_product( $product_data ) {
$product = new \WC_Product();
$image_src = plugins_url( $product_data['image'], dirname( __DIR__, 2 ) );
$image_alt = $product_data['title'];
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
$saved_product = $this->product_update( $product, $product_image_id, $product_data['title'], $product_data['description'], $product_data['price'] );
if ( is_wp_error( $saved_product ) ) {
return $saved_product;
}
return update_post_meta( $saved_product, '_headstart_post', true );
}
/**
* Return all existing products that have the _headstart_post meta assigned to them.
*
* @param string $type The type of products to fetch.
*
* @return array|null
*/
public function fetch_product_ids( string $type = 'user_created' ) {
global $wpdb;
if ( 'user_created' === $type ) {
return $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID NOT IN ( SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish' ) AND post_type = 'product' AND post_status = 'publish' LIMIT 6", '_headstart_post' ) );
}
return $wpdb->get_results( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish'", '_headstart_post' ) );
}
/**
* Return the hash for a product based on its name, description and image_id.
*
* @param \WC_Product $product The product.
*
* @return false|string
*/
public function get_hash_for_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
return md5( $product->get_name() . $product->get_description() . $product->get_image_id() );
}
/**
* Return the hash for a product that had its content AI-generated.
*
* @param \WC_Product $product The product.
*
* @return false|mixed
*/
public function get_hash_for_ai_modified_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
return get_post_meta( $product->get_id(), '_ai_generated_content', true );
}
/**
* Create a hash with the AI-generated content and save it as a meta for the product.
*
* @param \WC_Product $product The product.
*
* @return bool|int
*/
public function create_hash_for_ai_modified_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
$content = $this->get_hash_for_product( $product );
return update_post_meta( $product->get_id(), '_ai_generated_content', $content );
}
/**
* Update the product content with the AI-generated content.
*
* @param array $ai_generated_product_content The AI-generated product content.
*
* @return void|WP_Error
*/
public function update_product_content( $ai_generated_product_content ) {
if ( ! isset( $ai_generated_product_content['product_id'] ) ) {
return;
}
$product = wc_get_product( $ai_generated_product_content['product_id'] );
if ( ! $product instanceof \WC_Product ) {
return;
}
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;
}
$product_image_id = $this->product_image_upload( $product->get_id(), $ai_generated_product_content['image']['src'], $ai_generated_product_content['image']['alt'] );
$this->product_update( $product, $product_image_id, $ai_generated_product_content['title'], $ai_generated_product_content['description'], $ai_generated_product_content['price'] );
}
/**
* Upload the image for the product.
*
* @param int $product_id The product ID.
* @param string $image_src The image source.
* @param string $image_alt The image alt.
*
* @return int|string|WP_Error
*/
private function product_image_upload( $product_id, $image_src, $image_alt ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
// 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 to avoid any issues.
set_time_limit( 150 );
wp_raise_memory_limit( 'image' );
return media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
}
/**
* Assigns the default content for the products.
*
* @param array $dummy_products_to_update The dummy products to update.
* @param array $ai_selected_images The images' information.
*
* @return array[]
*/
public function assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $ai_selected_images ) {
$products_information_list = [];
$dummy_products_count = count( $dummy_products_to_update );
for ( $i = 0; $i < $dummy_products_count; $i ++ ) {
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
if ( wc_is_valid_url( $image_src ) ) {
$image_src = ContentProcessor::adjust_image_size( $image_src, 'products' );
}
$image_alt = $ai_selected_images[ $i ]['title'] ?? '';
$products_information_list[] = [
'title' => 'A product title',
'description' => 'A product description',
'price' => 'The product price',
'image' => [
'src' => $image_src,
'alt' => $image_alt,
],
'product_id' => $dummy_products_to_update[ $i ]->get_id(),
];
}
return $products_information_list;
}
/**
* Generate the product content.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param array $products_information_list The products information list.
* @param string $business_description The business description.
* @param string $search_term The search term.
*
* @return array|int|string|\WP_Error
*/
public function assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description, $search_term ) {
$business_description = ContentProcessor::summarize_business_description( $business_description, $ai_connection, $token, 100 );
if ( is_wp_error( $business_description ) ) {
return $business_description;
}
$prompts = [];
foreach ( $products_information_list as $product_information ) {
if ( ! empty( $product_information['image']['alt'] ) ) {
$prompts[] = sprintf( 'Considering that you are the owner of a store with the following description "%s", create the title for a product that is related to "%s" and to an image described as "%s". Do not include any adjectives or descriptions of the qualities of the product and always refer to objects or services, not humans.', $business_description, $search_term, $product_information['image']['alt'] );
} else {
$prompts[] = sprintf( 'You are the owner of a business described as: "%s". Create the title for a product that could be sold on your store. Do not include any adjectives or descriptions of the qualities of the product and always refer to objects or services, not humans.', $business_description );
}
}
$expected_results_format = [];
foreach ( $products_information_list as $index => $product ) {
$expected_results_format[ $index ] = [
'title' => '',
'price' => '',
];
}
$formatted_prompt = sprintf(
"Generate two-words titles and price for products using the following prompts for each one of them: '%s'. Ensure each entry is unique and does not repeat the given examples. It should be a number and it's not too low or too high for the corresponding product title being advertised. Convert the price to this currency: '%s'. Do not include backticks or the word json in the response. Here's an example of the expected output format in JSON: '%s'.",
wp_json_encode( $prompts ),
get_woocommerce_currency(),
wp_json_encode( $expected_results_format )
);
$ai_request_retries = 0;
$success = false;
while ( $ai_request_retries < 5 && ! $success ) {
$ai_request_retries ++;
$ai_response = $ai_connection->fetch_ai_response( $token, $formatted_prompt, 30 );
if ( is_wp_error( $ai_response ) ) {
continue;
}
if ( empty( $ai_response ) ) {
continue;
}
if ( ! isset( $ai_response['completion'] ) ) {
continue;
}
$completion = json_decode( $ai_response['completion'], true );
if ( ! is_array( $completion ) ) {
continue;
}
$diff = array_diff_key( $expected_results_format, $completion );
if ( ! empty( $diff ) ) {
continue;
}
$empty_results = false;
foreach ( $completion as $completion_item ) {
if ( empty( $completion_item ) ) {
$empty_results = true;
break;
}
}
if ( $empty_results ) {
continue;
}
foreach ( $products_information_list as $index => $product_information ) {
$products_information_list[ $index ]['title'] = str_replace( '"', '', $completion[ $index ]['title'] );
$products_information_list[ $index ]['price'] = $completion[ $index ]['price'];
}
$success = true;
}
if ( ! $success ) {
return new WP_Error( 'failed_to_fetch_ai_responses', __( 'Failed to fetch AI responses for products.', 'woocommerce' ) );
}
return array(
'product_content' => $products_information_list,
);
}
/**
* Reset the products content.
*/
public function reset_products_content() {
$dummy_products_to_update = $this->fetch_dummy_products_to_update();
$i = 0;
foreach ( $dummy_products_to_update as $product ) {
$image_src = plugins_url( self::DUMMY_PRODUCTS[ $i ]['image'], dirname( __DIR__, 2 ) );
$image_alt = self::DUMMY_PRODUCTS[ $i ]['title'];
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
$i++;
}
}
/**
* Update the product with the new content.
*
* @param \WC_Product $product The product.
* @param int $product_image_id The product image ID.
* @param string $product_title The product title.
* @param string $product_description The product description.
* @param int $product_price The product price.
*
* @return int|\WP_Error
*/
private function product_update( $product, $product_image_id, $product_title, $product_description, $product_price ) {
if ( ! $product instanceof \WC_Product ) {
return new WP_Error( 'invalid_product', __( 'Invalid product.', 'woocommerce' ) );
}
if ( ! is_wp_error( $product_image_id ) ) {
$product->set_image_id( $product_image_id );
} else {
wc_get_logger()->warning(
sprintf(
// translators: %s is a generated error message.
__( 'The image upload failed: "%s", creating the product without image', 'woocommerce' ),
$product_image_id->get_error_message()
),
);
}
$product->set_name( $product_title );
$product->set_description( $product_description );
$product->set_price( $product_price );
$product->set_regular_price( $product_price );
$product->set_slug( sanitize_title( $product_title ) );
$product->save();
$this->create_hash_for_ai_modified_product( $product );
return $product->get_id();
}
}