From 9b99154d721232b040515b4c6a616290969362e5 Mon Sep 17 00:00:00 2001 From: Patricia Hillebrandt Date: Mon, 20 Nov 2023 16:59:52 +0000 Subject: [PATCH] Customize your Store > Implement retry mechanism, enhance prompts and fix issue with AI content not being generated for new products (https://github.com/woocommerce/woocommerce-blocks/pull/11759) * Fine-tune the prompts for generating product titles. * Update the prompt for images. * Update the prompts for generating content for patterns. * Break down the pattern content generation into individual methods. * Add character limit to prompts. * Add character limit for a handful of titles. * Add character limit to the title of the product-collection-4-columns pattern. * Update the prompt to not add abbreviations * Introduce the refine_returned_images_results method for AI to filter the images returned by Pexels * Implement retry if the number of images returned by Pexels API is smaller than the number of images required by products and patterns. * Update the prompt for the images search term. * Fix AI image assignment for newly created products and improve performance. * Ensure no additional dummy products are created or updated in simultaneous requests. * Fix error on generating content for patterns for the first time and saving it to the post. * Return error if the connection with AI failed. * Update the prompt character limit for the Featured Category Triple pattern. * Implement retry if the API connection with AI fails. Update the prompt to remove the length instruction. Add the last_business_description check to prevent duplicated API calls. Set a transient for images in case the request fails. * Add retry for returning the refined search results. Update the prompt for defining the search term. Update error validation. * Update the structure for fetching patterns content and reduce the number of requests. * Break down the pattern ai content generation and assignment into multiple methods and introduce retry in case of failure. * Update the structure for applying the AI generated content to patterns. Update the fetch and validation of ai responses and update the prompt to ensure longer texts are returned. * Update the validation of the content returned by AI by verifying the array keys and ensuring the values are not empty. Additionally, the completion should be set. * If for some reason AI didn't return the complete list of expected generated content, use the same the previous text from the same pattern as a replacement and avoid making an extra request to fetch just fetch a possible missing result. * Update prompts for patterns. * Make a single request to fetch all products data, implement retry for products and validation for the response returned by AI. * Update the retries variable first within the patterns loop. * Narrow down the AI content generation to only patterns that are part of the Assembler. * Update the size of images and the prompt for AI generated content assigned to products. * Don't make request to AI if the user provided a single word for their business description. * Update the image assignment to products and remove the unused update_dummy_products method. * Update the prompt for generating content for patterns. * Update the prompt for products content generation and ensure that an error is returned if all requests fail. * Update the prompt for patterns. * Update the prompt for images. * Update the prompt for images. * Update prompt for button text. * Don't schedule action for updating patterns content if the business description wasn't updated. * add associative option when the local json is parsed * Ensure the expected list of results matches the number or products to be updated. * fix lint error * Address CR. --------- Co-authored-by: Luigi Teschio --- .../woocommerce-blocks/src/AI/Connection.php | 4 +- .../woocommerce-blocks/src/BlockPatterns.php | 6 + .../woocommerce-blocks/src/Images/Pexels.php | 155 ++++++- .../src/Patterns/PatternUpdater.php | 420 +++++++++++++++--- .../src/Patterns/PatternsHelper.php | 73 +-- .../src/Patterns/ProductUpdater.php | 258 ++++++----- .../src/Patterns/dictionary.json | 172 +++---- .../src/StoreApi/Routes/V1/AI/Product.php | 27 +- .../src/StoreApi/Routes/V1/AI/Products.php | 14 - 9 files changed, 777 insertions(+), 352 deletions(-) diff --git a/plugins/woocommerce-blocks/src/AI/Connection.php b/plugins/woocommerce-blocks/src/AI/Connection.php index 3ef48b36935..8e6f4908276 100644 --- a/plugins/woocommerce-blocks/src/AI/Connection.php +++ b/plugins/woocommerce-blocks/src/AI/Connection.php @@ -83,10 +83,10 @@ class Connection { $responses = Requests::request_multiple( $requests, array( 'timeout' => $timeout ) ); $processed_responses = array(); + foreach ( $responses as $key => $response ) { if ( is_wp_error( $response ) || is_a( $response, Exception::class ) ) { - $processed_responses[ $key ] = null; - continue; + return new WP_Error( 'failed-to-connect-with-the-ai-endpoint', esc_html__( 'Failed to connect with the AI endpoint: try again later.', 'woo-gutenberg-products-block' ) ); } $processed_responses[ $key ] = json_decode( $response->body, true ); diff --git a/plugins/woocommerce-blocks/src/BlockPatterns.php b/plugins/woocommerce-blocks/src/BlockPatterns.php index daad220348a..6512197b261 100644 --- a/plugins/woocommerce-blocks/src/BlockPatterns.php +++ b/plugins/woocommerce-blocks/src/BlockPatterns.php @@ -277,6 +277,12 @@ class BlockPatterns { * @param string $value The option value. */ public function schedule_on_option_update( $option, $value ) { + $last_business_description = get_option( 'last_business_description_with_ai_content_generated' ); + + if ( $last_business_description === $value ) { + return; + } + $this->schedule_patterns_content_update( $value ); } diff --git a/plugins/woocommerce-blocks/src/Images/Pexels.php b/plugins/woocommerce-blocks/src/Images/Pexels.php index 8c7a2840801..b755f2ba9f2 100644 --- a/plugins/woocommerce-blocks/src/Images/Pexels.php +++ b/plugins/woocommerce-blocks/src/Images/Pexels.php @@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\Images; use Automattic\WooCommerce\Blocks\AI\Connection; +use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater; /** * Pexels API client. @@ -26,13 +27,73 @@ class Pexels { * @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 ( str_word_count( $business_description ) === 1 ) { + $search_term = $business_description; + } else { + $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 ); + $required_images = $this->total_number_required_images(); + + if ( is_wp_error( $required_images ) ) { + return $required_images; + } + + $returned_images = $this->request( $search_term ); + + if ( is_wp_error( $returned_images ) ) { + return $returned_images; + } + + $refined_images = $this->refine_returned_images_results( $ai_connection, $token, $business_description, $returned_images ); + + if ( is_wp_error( $refined_images ) ) { + return $returned_images; + } + + $refined_images_count = count( $refined_images ); + + $i = 0; + $errors = array(); + while ( $refined_images_count < $required_images && $i < 5 ) { + $i ++; + $search_term = $this->define_search_term( $ai_connection, $token, $business_description ); + + if ( is_wp_error( $search_term ) ) { + $errors[] = $search_term; + continue; + } + + $images_to_add = $this->request( $search_term ); + + if ( is_wp_error( $images_to_add ) ) { + $errors[] = $images_to_add; + continue; + } + + $images_to_add = $this->refine_returned_images_results( $ai_connection, $token, $business_description, $images_to_add ); + + if ( is_wp_error( $images_to_add ) ) { + $errors[] = $images_to_add; + continue; + } + + $refined_images = array_merge( $refined_images, $images_to_add ); + } + + if ( $refined_images_count < $required_images && ! empty( $errors ) ) { + return new \WP_Error( 'ai_service_unavailable', __( 'AI Service is unavailable, try again later.', 'woo-gutenberg-products-block' ), $errors ); + } + + if ( empty( $refined_images ) ) { + return new \WP_Error( 'woocommerce_no_images_found', __( 'No images found.', 'woo-gutenberg-products-block' ) ); + } + + return $refined_images; } /** @@ -47,17 +108,73 @@ class Pexels { * @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 ); + $prompt = sprintf( 'Based on the description "%s", generate one word that precisely describe what this business is selling. The returned word should be as accurate as possible to describe the products sold, do not include any adjectives or descriptions of the qualities of the product. The generated word must exist in the english dictionary and not be a proper name, the returned word should be simple, do not add any explanations.', $business_description ); - $response = $ai_connection->fetch_ai_response( $token, $prompt ); + $response = $ai_connection->fetch_ai_response( $token, $prompt, 30 ); - if ( is_wp_error( $response ) || ! isset( $response['completion'] ) ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( ! isset( $response['completion'] ) ) { return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woo-gutenberg-products-block' ) ); } return $response['completion']; } + /** + * Refine the results returned by Pexels API. + * + * @param Connection $ai_connection The AI connection. + * @param string $token The JWT token. + * @param string $business_description The business description. + * @param array $returned_images The returned images. + * + * @return array|\WP_Error The refined images, or WP_Error if the request failed. + */ + private function refine_returned_images_results( $ai_connection, $token, $business_description, $returned_images ) { + $image_titles = array(); + foreach ( $returned_images as $returned_image ) { + if ( isset( $returned_image['title'] ) ) { + $image_titles[] = $returned_image['title']; + } + } + + $prompt = sprintf( 'Compare the following business description: "%s" with the image titles listed in the JSON below: if the image is not aligned with what the business is selling, delete the description from the list. Do not include any explanations or introductions to the response. The JSON is: %s', $business_description, wp_json_encode( $image_titles ) ); + + $response = $ai_connection->fetch_ai_response( $token, $prompt, 30 ); + + if ( is_wp_error( $response ) || ! isset( $response['completion'] ) ) { + return $returned_images; + } + + $filtered_image_titles = json_decode( $response['completion'] ); + + if ( ! is_array( $filtered_image_titles ) ) { + $response = $ai_connection->fetch_ai_response( $token, $prompt, 30 ); + + if ( is_wp_error( $response ) || ! isset( $response['completion'] ) ) { + return $returned_images; + } + + $filtered_image_titles = json_decode( $response['completion'] ); + } + + if ( ! is_array( $filtered_image_titles ) ) { + return new \WP_Error( 'ai_service_unavailable', __( 'AI Service is unavailable, try again later.', 'woo-gutenberg-products-block' ) ); + } + + // Remove the images that are not aligned with the business description. + foreach ( $returned_images as $returned_image ) { + if ( isset( $returned_image['title'] ) && ! in_array( $returned_image['title'], $filtered_image_titles, true ) ) { + unset( $returned_image ); + } + } + + return $returned_images; + } + /** * Make a request to the Pexels API. * @@ -66,11 +183,12 @@ class Pexels { * * @return array|\WP_Error The response body, or WP_Error if the request failed. */ - private function request( string $search_term, int $per_page = 90 ) { + private function request( string $search_term, int $per_page = 100 ) { $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 ); + $request->set_param( 'size', 'small' ); $response = rest_do_request( $request ); $response_data = $response->get_data(); @@ -94,4 +212,29 @@ class Pexels { return array(); } + + /** + * Total number of required images. + * + * @return array|\WP_Error The total number of required images, or WP_Error if the request failed. + */ + private function total_number_required_images() { + $patterns_dictionary = PatternUpdater::get_patterns_dictionary(); + + if ( is_wp_error( $patterns_dictionary ) ) { + return $patterns_dictionary; + } + + $required_images = 0; + foreach ( $patterns_dictionary as $pattern ) { + if ( isset( $pattern['images_total'] ) && $pattern['images_total'] > 0 ) { + $required_images += $pattern['images_total']; + } + } + + // Adding +6 images for the dummy products. + $required_images += 6; + + return $required_images; + } } diff --git a/plugins/woocommerce-blocks/src/Patterns/PatternUpdater.php b/plugins/woocommerce-blocks/src/Patterns/PatternUpdater.php index affbd20cbeb..bd9f149e5a3 100644 --- a/plugins/woocommerce-blocks/src/Patterns/PatternUpdater.php +++ b/plugins/woocommerce-blocks/src/Patterns/PatternUpdater.php @@ -11,7 +11,29 @@ use WP_Error; class PatternUpdater { /** - * Creates the patterns content for the given vertical. + * The patterns content option name. + */ + const WC_BLOCKS_PATTERNS_CONTENT = 'wc_blocks_patterns_content'; + + /** + * All patterns that are actively in use in the Assembler. + */ + const WC_PATTERNS_IN_THE_ASSEMBLER = [ + 'woocommerce-blocks/featured-category-triple', + 'woocommerce-blocks/hero-product-3-split', + 'woocommerce-blocks/hero-product-chessboard', + 'woocommerce-blocks/hero-product-split', + 'woocommerce-blocks/product-query-product-gallery', + 'woocommerce-blocks/product-collection-3-columns', + 'woocommerce-blocks/product-collection-4-columns', + 'woocommerce-blocks/product-collection-5-columns', + 'woocommerce-blocks/social-follow-us-in-social-media', + 'woocommerce-blocks/testimonials-3-columns', + 'woocommerce-blocks/product-collection-featured-products-5-columns', + ]; + + /** + * Generate AI content and assign AI-managed images to Patterns. * * @param Connection $ai_connection The AI connection. * @param string|WP_Error $token The JWT token. @@ -25,47 +47,343 @@ class PatternUpdater { return $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' ) ); + if ( is_wp_error( $token ) ) { + return $token; } - $patterns_with_images_and_content = $this->get_patterns_with_content( $ai_connection, $token, $patterns_with_images, $business_description ); + $last_business_description = get_option( 'last_business_description_with_ai_content_generated' ); - 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' ) ); + if ( $last_business_description === $business_description ) { + if ( is_string( $business_description ) && is_string( $last_business_description ) ) { + return true; + } else { + return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woo-gutenberg-products-block' ) ); + } } - $patterns_ai_data_post = PatternsHelper::get_patterns_ai_data_post(); - - if ( isset( $patterns_ai_data_post->post_content ) && json_decode( $patterns_ai_data_post->post_content ) === $patterns_with_images_and_content ) { - return true; + if ( 0 === count( $images ) ) { + $images = get_transient( 'woocommerce_ai_managed_images' ); } - $updated_content = PatternsHelper::upsert_patterns_ai_data_post( $patterns_with_images_and_content ); - - if ( is_wp_error( $updated_content ) ) { - return new WP_Error( 'failed_to_update_patterns_content', __( 'Failed to update patterns content.', 'woo-gutenberg-products-block' ) ); + if ( empty( $images ) ) { + return new WP_Error( 'no_images_found', __( 'No images found.', 'woo-gutenberg-products-block' ) ); } - return $updated_content; - } + // This is required in case something interrupts the execution of the script and the endpoint is called again on retry. + set_transient( 'woocommerce_ai_managed_images', $images, 60 ); - /** - * Returns the patterns with images. - * - * @param array $selected_images The array of images. - * - * @return array|WP_Error The patterns with images. - */ - private function get_patterns_with_images( $selected_images ) { - $patterns_dictionary = $this->get_patterns_dictionary(); + $patterns_dictionary = self::get_patterns_dictionary(); if ( is_wp_error( $patterns_dictionary ) ) { return $patterns_dictionary; } + $patterns = $this->assign_selected_images_to_patterns( $patterns_dictionary, $images ); + + if ( is_wp_error( $patterns ) ) { + return new WP_Error( 'failed_to_set_pattern_images', __( 'Failed to set the pattern images.', 'woo-gutenberg-products-block' ) ); + } + + $ai_generated_patterns_content = $this->generate_ai_content_for_patterns( $ai_connection, $token, $patterns, $business_description ); + + if ( is_wp_error( $ai_generated_patterns_content ) ) { + return new WP_Error( 'failed_to_set_pattern_content', __( 'Failed to set the pattern content.', 'woo-gutenberg-products-block' ) ); + } + + $patterns_ai_data_post = PatternsHelper::get_patterns_ai_data_post(); + + if ( isset( $patterns_ai_data_post->post_content ) && json_decode( $patterns_ai_data_post->post_content ) === $ai_generated_patterns_content ) { + return true; + } + + $updated_content = PatternsHelper::upsert_patterns_ai_data_post( $ai_generated_patterns_content ); + + if ( is_wp_error( $updated_content ) ) { + return new WP_Error( 'failed_to_update_patterns_content', __( 'Failed to update patterns content.', 'woo-gutenberg-products-block' ) ); + } + + return true; + } + + /** + * Returns the patterns with AI generated content. + * + * @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. + */ + public function generate_ai_content_for_patterns( $ai_connection, $token, $patterns, $business_description ) { + $prompts = $this->prepare_prompts( $patterns ); + $expected_results_format = $this->prepare_expected_results_format( $prompts ); + $formatted_prompts = $this->format_prompts_for_ai( $prompts, $business_description, $expected_results_format ); + $ai_responses = $this->fetch_and_validate_ai_responses( $ai_connection, $token, $formatted_prompts, $expected_results_format ); + + if ( is_wp_error( $ai_responses ) ) { + return $ai_responses; + } + + return $this->apply_ai_responses_to_patterns( $patterns, $ai_responses ); + } + + /** + * Prepares the prompts for the AI. + * + * @param array $patterns The array of patterns. + * + * @return array + */ + private function prepare_prompts( array $patterns ) { + $prompts = []; + $result = []; + $group_size = count( self::WC_PATTERNS_IN_THE_ASSEMBLER ); + $i = 1; + foreach ( $patterns as $pattern ) { + $slug = $pattern['slug'] ?? ''; + + if ( ! in_array( $slug, self::WC_PATTERNS_IN_THE_ASSEMBLER, true ) ) { + continue; + } + + $content = $pattern['content'] ?? ''; + $counter = 1; + $result[ $slug ] = []; + + if ( isset( $content['titles'] ) ) { + foreach ( $content['titles'] as $title ) { + $result[ $slug ][ $counter ++ ] = $title['ai_prompt']; + } + } + + if ( isset( $content['descriptions'] ) ) { + foreach ( $content['descriptions'] as $description ) { + $result[ $slug ][ $counter ++ ] = $description['ai_prompt']; + } + } + + if ( isset( $content['buttons'] ) ) { + foreach ( $content['buttons'] as $button ) { + $result[ $slug ][ $counter ++ ] = $button['ai_prompt']; + } + } + + $i ++; + + if ( $i === $group_size ) { + $prompts[] = $result; + $result = []; + $i = 1; + } + } + + return $prompts; + } + + /** + * Prepares the expected results format for the AI. + * + * @param array $prompts The array of prompts. + * + * @return array + */ + private function prepare_expected_results_format( array $prompts ) { + $expected_results_format = []; + foreach ( $prompts as $prompt ) { + $expected_result_format = []; + + foreach ( $prompt as $key => $values ) { + $expected_result_format[ $key ] = []; + + foreach ( $values as $sub_key => $sub_value ) { + $expected_result_format[ $key ][ $sub_key ] = ''; + } + } + + $expected_results_format[] = $expected_result_format; + } + + return $expected_results_format; + } + + /** + * Formats the prompts for the AI. + * + * @param array $prompts The array of prompts. + * @param string $business_description The business description. + * @param array $expected_results_format The expected results format. + * + * @return array + */ + private function format_prompts_for_ai( array $prompts, string $business_description, array $expected_results_format ) { + $i = 0; + $formatted_prompts = []; + foreach ( $prompts as $prompt ) { + $formatted_prompts[] = sprintf( + "Given the following description '%s' generate long texts for the sections using the following prompts for each one of them: `'%s'`. Ensure each entry is unique and does not repeat the given examples. The response should be an array of data in JSON format. Each element should be an object with the pattern name as the key, and the generated content as values. Do not include backticks or the word json in the response. Here's an example format: `'%s'`", + $business_description, + wp_json_encode( $prompt ), + wp_json_encode( $expected_results_format[ $i ] ) + ); + $i ++; + } + + return $formatted_prompts; + } + + /** + * Fetches and validates the AI responses. + * + * @param Connection $ai_connection The AI connection. + * @param string|WP_Error $token The JWT token. + * @param array $formatted_prompts The array of formatted prompts. + * @param array $expected_results_format The array of expected results format. + * + * @return array|mixed + */ + private function fetch_and_validate_ai_responses( $ai_connection, $token, $formatted_prompts, $expected_results_format ) { + $ai_request_retries = 0; + $ai_responses = []; + $success = false; + while ( $ai_request_retries < 5 && ! $success ) { + $ai_request_retries ++; + $ai_responses = $ai_connection->fetch_ai_responses( $token, $formatted_prompts, 60 ); + + if ( is_wp_error( $ai_responses ) ) { + continue; + } + + if ( empty( $ai_responses ) ) { + continue; + } + + $loops_success = []; + $i = 0; + foreach ( $ai_responses as $ai_response ) { + if ( ! isset( $ai_response['completion'] ) ) { + $loops_success[] = false; + continue; + } + + $completion = json_decode( $ai_response['completion'], true ); + + if ( ! is_array( $completion ) ) { + $loops_success[] = false; + continue; + } + + $diff = array_diff_key( $expected_results_format[ $i ], $completion ); + $i ++; + + if ( ! empty( $diff ) ) { + $loops_success[] = false; + continue; + } + + $empty_results = false; + foreach ( $completion as $completion_item ) { + foreach ( $completion_item as $value ) { + if ( empty( $value ) ) { + $empty_results = true; + } + } + } + + if ( $empty_results ) { + $loops_success[] = false; + continue; + } + + $loops_success[] = true; + } + + if ( ! in_array( false, $loops_success, true ) ) { + $success = true; + } + } + + if ( ! $success ) { + return new WP_Error( 'failed_to_fetch_ai_responses', __( 'Failed to fetch AI responses.', 'woo-gutenberg-products-block' ) ); + } + + return $ai_responses; + } + + /** + * Applies the AI responses to the patterns. + * + * @param array $patterns The array of patterns. + * @param array $ai_responses The array of AI responses. + * + * @return mixed + */ + private function apply_ai_responses_to_patterns( array $patterns, array $ai_responses ) { + foreach ( $patterns as $i => $pattern ) { + $pattern_slug = $pattern['slug']; + + if ( ! in_array( $pattern_slug, self::WC_PATTERNS_IN_THE_ASSEMBLER, true ) ) { + continue; + } + + foreach ( $ai_responses as $ai_response ) { + $ai_response = json_decode( $ai_response['completion'], true ); + + if ( isset( $ai_response[ $pattern_slug ] ) ) { + $ai_response_content = $ai_response[ $pattern_slug ]; + + $counter = 1; + if ( isset( $patterns[ $i ]['content']['titles'] ) ) { + foreach ( $patterns[ $i ]['content']['titles'] as $j => $title ) { + if ( ! isset( $ai_response_content[ $counter ] ) ) { + $ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? ''; + } + + $patterns[ $i ]['content']['titles'][ $j ]['default'] = $ai_response_content[ $counter ]; + + $counter ++; + } + } + + if ( isset( $patterns[ $i ]['content']['descriptions'] ) ) { + foreach ( $patterns[ $i ]['content']['descriptions'] as $k => $description ) { + if ( ! isset( $ai_response_content[ $counter ] ) ) { + $ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? ''; + } + + $patterns[ $i ]['content']['descriptions'][ $k ]['default'] = $ai_response_content[ $counter ]; + + $counter ++; + } + } + + if ( isset( $patterns[ $i ]['content']['buttons'] ) ) { + foreach ( $patterns[ $i ]['content']['buttons'] as $l => $button ) { + if ( ! isset( $ai_response_content[ $counter ] ) ) { + $ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? ''; + } + + $patterns[ $i ]['content']['buttons'][ $l ]['default'] = $ai_response_content[ $counter ]; + + $counter ++; + } + } + } + } + } + + return $patterns; + } + + /** + * Assign selected images to patterns. + * + * @param array $patterns_dictionary The array of patterns. + * @param array $selected_images The array of images. + * + * @return array|WP_Error The patterns with images. + */ + private function assign_selected_images_to_patterns( $patterns_dictionary, $selected_images ) { $patterns_with_images = array(); foreach ( $patterns_dictionary as $pattern ) { @@ -97,58 +415,12 @@ class PatternUpdater { return $patterns_with_images; } - /** - * Returns the patterns with AI generated content. - * - * @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( $ai_connection, $token, $patterns, $business_description ) { - if ( is_wp_error( $token ) ) { - return $token; - } - - $patterns_with_content = $patterns; - - $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 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 = $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. - if ( is_wp_error( $response ) || empty( $response ) ) { - continue; - } - - if ( ! isset( $response['completion'] ) ) { - continue; - } - - $pattern_content = json_decode( $response['completion'], true ); - if ( ! is_null( $pattern_content ) ) { - $patterns_with_content[ $key ]['content'] = $pattern_content; - } - } - - return $patterns_with_content; - } - /** * Get the Patterns Dictionary. * * @return mixed|WP_Error|null */ - private function get_patterns_dictionary() { + public static function get_patterns_dictionary() { $patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json'; if ( ! file_exists( $patterns_dictionary ) ) { diff --git a/plugins/woocommerce-blocks/src/Patterns/PatternsHelper.php b/plugins/woocommerce-blocks/src/Patterns/PatternsHelper.php index aacb525a987..792402c8738 100644 --- a/plugins/woocommerce-blocks/src/Patterns/PatternsHelper.php +++ b/plugins/woocommerce-blocks/src/Patterns/PatternsHelper.php @@ -74,10 +74,11 @@ class PatternsHelper { return $image; } + /** * Returns the post that has the generated data by the AI for the patterns. * - * @return WP_Post|null + * @return \WP_Post|null */ public static function get_patterns_ai_data_post() { $arg = array( @@ -90,7 +91,7 @@ class PatternsHelper { $query = new \WP_Query( $arg ); $posts = $query->get_posts(); - return isset( $posts[0] ) ? $posts[0] : null; + return $posts[0] ?? null; } /** @@ -122,41 +123,61 @@ class PatternsHelper { * * @param string|null $pattern_slug The pattern slug. * - * @return mixed|WP_Error|null + * @return array|WP_Error Returns pattern dictionary or WP_Error on failure. */ public static function get_patterns_dictionary( $pattern_slug = null ) { - - $patterns_ai_data_post = self::get_patterns_ai_data_post(); - - if ( isset( $patterns_ai_data_post ) ) { - $patterns_dictionary = json_decode( $patterns_ai_data_post->post_content, true ); - if ( empty( $pattern_slug ) ) { - return $patterns_dictionary; - } - - foreach ( $patterns_dictionary as $pattern_dictionary ) { - if ( $pattern_dictionary['slug'] === $pattern_slug ) { - return $pattern_dictionary; - } - } - } - $patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json'; if ( ! file_exists( $patterns_dictionary_file ) ) { return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woo-gutenberg-products-block' ) ); } - $patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) ); + $default_patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) ); - if ( ! empty( $pattern_slug ) ) { - foreach ( $patterns_dictionary as $pattern_dictionary ) { - if ( $pattern_dictionary['slug'] === $pattern_slug ) { - return $pattern_dictionary; - } + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woo-gutenberg-products-block' ) ); + } + + $patterns_ai_data_post = self::get_patterns_ai_data_post(); + $patterns_dictionary = ''; + if ( ! empty( $patterns_ai_data_post->post_content ) ) { + $patterns_dictionary = json_decode( $patterns_ai_data_post->post_content, true ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woo-gutenberg-products-block' ) ); } } - return $patterns_dictionary; + if ( ( $patterns_dictionary === $default_patterns_dictionary || empty( $patterns_dictionary ) ) && $pattern_slug ) { + return self::find_pattern_by_slug( $default_patterns_dictionary, $pattern_slug ); + } elseif ( $pattern_slug && is_array( $patterns_dictionary ) ) { + return self::find_pattern_by_slug( $patterns_dictionary, $pattern_slug ); + } elseif ( is_array( $patterns_dictionary ) ) { + return $patterns_dictionary; + } + + return $default_patterns_dictionary; + } + + /** + * Searches for a pattern by slug in a given dictionary. + * + * @param array $patterns_dictionary The patterns' dictionary. + * @param string $slug The slug to search for. + * + * @return array|null Returns the pattern if found, otherwise null. + */ + private static function find_pattern_by_slug( $patterns_dictionary, $slug ) { + foreach ( $patterns_dictionary as $pattern ) { + if ( ! is_array( $pattern ) ) { + continue; + } + + if ( $pattern['slug'] === $slug ) { + return $pattern; + } + } + + return null; } } diff --git a/plugins/woocommerce-blocks/src/Patterns/ProductUpdater.php b/plugins/woocommerce-blocks/src/Patterns/ProductUpdater.php index ee13c515e49..e2b8113e6e6 100644 --- a/plugins/woocommerce-blocks/src/Patterns/ProductUpdater.php +++ b/plugins/woocommerce-blocks/src/Patterns/ProductUpdater.php @@ -12,14 +12,22 @@ class ProductUpdater { /** * Generate AI content and assign AI-managed images to Products. * - * @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. + * @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( $images ) ) { + return $images; + } + + if ( is_wp_error( $token ) ) { + return $token; + } + if ( empty( $business_description ) ) { return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woo-gutenberg-products-block' ) ); } @@ -36,26 +44,15 @@ class ProductUpdater { } } - $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 ); + $dummy_products_to_update = $this->fetch_dummy_products_to_update(); - $response = $this->generate_product_content( $ai_connection, $token, $products_information_list ); - - 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 ( is_wp_error( $dummy_products_to_update ) ) { + return $dummy_products_to_update; } - if ( isset( $error_msg ) ) { - return $error_msg; - } + $products_information_list = $this->assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $images ); - $product_content = json_decode( $response['completion'], true ); - - return array( - 'product_content' => $product_content, - ); + return $this->assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description ); } /** @@ -64,23 +61,18 @@ class ProductUpdater { * @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 = $this->fetch_product_ids(); + $real_products_count = count( $real_products ); - if ( is_array( $real_products ) && count( $real_products ) > 0 ) { + if ( is_array( $real_products ) && $real_products_count > 6 ) { return array( 'product_content' => array(), ); } - $dummy_products = $this->fetch_product_ids( 'dummy' ); - - if ( ! is_array( $dummy_products ) ) { - return new \WP_Error( 'failed_to_fetch_dummy_products', __( 'Failed to fetch dummy products.', 'woo-gutenberg-products-block' ) ); - } - - $dummy_products_count = count( $dummy_products ); - $expected_dummy_products_count = 6; - $products_to_create = max( 0, $expected_dummy_products_count - $dummy_products_count ); + $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(); @@ -89,7 +81,6 @@ class ProductUpdater { // 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.', 'woo-gutenberg-products-block' ) ); } @@ -140,10 +131,12 @@ class ProductUpdater { $timestamp_created = strtotime( $formatted_date_created ); $timestamp_modified = strtotime( $formatted_date_modified ); + $timestamp_current = time(); - $dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60; + $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 ) { + if ( $current_product_hash === $ai_modified_product_hash || $dummy_product_not_modified || $dummy_product_recently_modified ) { return true; } @@ -175,9 +168,9 @@ class ProductUpdater { * * @param string $type The type of products to fetch. * - * @return array + * @return array|null */ - public function fetch_product_ids( $type = 'user_created' ) { + public function fetch_product_ids( string $type = 'user_created' ) { global $wpdb; if ( 'user_created' === $type ) { @@ -237,12 +230,17 @@ class ProductUpdater { /** * Update the product content with the AI-generated content. * - * @param \WC_Product $product The product. - * @param array $ai_generated_product_content The AI-generated content. + * @param array $ai_generated_product_content The AI-generated product content. * * @return string|void */ - public function update_product_content( $product, $ai_generated_product_content ) { + 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; } @@ -251,6 +249,9 @@ class ProductUpdater { return; } + $product->set_name( $ai_generated_product_content['title'] ); + $product->set_description( $ai_generated_product_content['description'] ); + require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; @@ -258,7 +259,8 @@ class ProductUpdater { // 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( 60 ); + set_time_limit( 150 ); + wp_raise_memory_limit( 'image' ); $product_image_id = media_sideload_image( $ai_generated_product_content['image']['src'], $product->get_id(), $ai_generated_product_content['image']['alt'], 'id' ); @@ -266,10 +268,7 @@ class ProductUpdater { return $product_image_id->get_error_message(); } - $product->set_name( $ai_generated_product_content['title'] ); - $product->set_description( $ai_generated_product_content['description'] ); $product->set_image_id( $product_image_id ); - $product->save(); $this->create_hash_for_ai_modified_product( $product ); @@ -278,85 +277,30 @@ class ProductUpdater { /** * Assigns the default content for the products. * - * @param array $ai_selected_products_images The images information. + * @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_information_list( $ai_selected_products_images ) { - $default_image = [ - 'src' => esc_url( plugins_url( 'woocommerce-blocks/images/block-placeholders/product-image-gallery.svg' ) ), - 'alt' => 'The placeholder for a product image.', - ]; + 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'] ?? plugins_url( 'woocommerce-blocks/images/block-placeholders/product-image-gallery.svg' ); + $image_alt = $ai_selected_images[ $i ]['title'] ?? ''; - return [ - [ + $products_information_list[] = [ 'title' => 'A product title', 'description' => 'A product description', - 'image' => $ai_selected_products_images[0] ?? $default_image, - ], - [ - 'title' => 'A product title', - 'description' => 'A product description', - 'image' => $ai_selected_products_images[1] ?? $default_image, - ], - [ - 'title' => 'A product title', - 'description' => 'A product description', - 'image' => $ai_selected_products_images[2] ?? $default_image, - ], - [ - 'title' => 'A product title', - 'description' => 'A product description', - 'image' => $ai_selected_products_images[3] ?? $default_image, - ], - [ - 'title' => 'A product title', - 'description' => 'A product description', - 'image' => $ai_selected_products_images[4] ?? $default_image, - ], - [ - 'title' => 'A product title', - 'description' => 'A product description', - 'image' => $ai_selected_products_images[5] ?? $default_image, - ], - ]; - } - - /** - * Get the images information. - * - * @param array $images The array of images. - * - * @return array - */ - public function get_images_information( $images ) { - if ( is_wp_error( $images ) ) { - return [ - 'src' => 'images/block-placeholders/product-image-gallery.svg', - 'alt' => 'The placeholder for a product image.', + 'image' => [ + 'src' => esc_url( $image_src ), + 'alt' => esc_attr( $image_alt ), + ], + 'product_id' => $dummy_products_to_update[ $i ]->get_id(), ]; } - $count = 0; - $placeholder_images = []; - foreach ( $images as $image ) { - if ( $count >= 6 ) { - break; - } - - if ( ! isset( $image['title'] ) || ! isset( $image['thumbnails']['medium'] ) ) { - continue; - } - - $placeholder_images[] = [ - 'src' => esc_url( $image['thumbnails']['medium'] ), - 'alt' => esc_attr( $image['title'] ), - ]; - - ++ $count; - } - - return $placeholder_images; + return $products_information_list; } /** @@ -364,19 +308,91 @@ class ProductUpdater { * * @param Connection $ai_connection The AI connection. * @param string $token The JWT token. - * @param array $products_default_content The default content for the products. + * @param array $products_information_list The products information list. + * @param string $business_description The business description. * * @return array|int|string|\WP_Error */ - 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' ) ); + public function assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description ) { + if ( empty( $business_description ) ) { + return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate content for your site.', 'woo-gutenberg-products-block' ) ); } - $prompt = sprintf( 'Given the following business description: "%1s" and the assigned value for the alt property in the JSON below, generate new titles and descriptions for each one of the products listed below 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 ) ); + $prompts = []; + foreach ( $products_information_list as $product_information ) { + if ( ! empty( $product_information['image']['alt'] ) ) { + $prompts[] = sprintf( 'Generate a product name that exactly matches the following image description: "%s" and also is related to the following business description: "%s". Do not include any adjectives or descriptions of the qualities of the product and always refer to objects or services, not humans. The returned result should not refer to people, only objects.', $product_information['image']['alt'], $business_description ); + } else { + $prompts[] = sprintf( 'Generate a product name that matches the following business description: "%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 ); + } + } - return $ai_connection->fetch_ai_response( $token, $prompt, 60 ); + $expected_results_format = []; + foreach ( $products_information_list as $index => $product ) { + $expected_results_format[ $index ] = ''; + } + + $formatted_prompt = sprintf( + "Generate two-words titles for products using the following prompts for each one of them: '%s'. Ensure each entry is unique and does not repeat the given examples. Do not include backticks or the word json in the response. Here's an example format: '%s'.", + wp_json_encode( $prompts ), + 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 ] ); + } + + $success = true; + } + + if ( ! $success ) { + return new WP_Error( 'failed_to_fetch_ai_responses', __( 'Failed to fetch AI responses for products.', 'woo-gutenberg-products-block' ) ); + } + + return array( + 'product_content' => $products_information_list, + ); } } diff --git a/plugins/woocommerce-blocks/src/Patterns/dictionary.json b/plugins/woocommerce-blocks/src/Patterns/dictionary.json index 0e9c7c8df4d..419764f30c5 100644 --- a/plugins/woocommerce-blocks/src/Patterns/dictionary.json +++ b/plugins/woocommerce-blocks/src/Patterns/dictionary.json @@ -8,23 +8,23 @@ "titles": [ { "default": "Save up to 60%", - "ai_prompt": "A title advertising the sale" + "ai_prompt": "A four words title advertising the sale" } ], "descriptions": [ { "default": "Holiday Sale", - "ai_prompt": "A label with the sale name" + "ai_prompt": "A two words label with the sale name" }, { "default": "Make the day special with our collection of discounted products.", - "ai_prompt": "The main description of the sale" + "ai_prompt": "The main description of the sale with at least 65 characters" } ], "buttons": [ { "default": "Shop Holiday Sale", - "ai_prompt": "The button text to go to the sale page" + "ai_prompt": "A 3 words button text to go to the sale page" } ] } @@ -36,7 +36,7 @@ "descriptions": [ { "default": "Select products", - "ai_prompt": "A description of the products on sale" + "ai_prompt": "A two words description of the products on sale" } ] } @@ -50,7 +50,7 @@ "descriptions": [ { "default": "Select products", - "ai_prompt": "A description of the products on sale" + "ai_prompt": "A two words description of the products on sale" } ] } @@ -64,7 +64,7 @@ "titles": [ { "default": "Announcing our newest collection", - "ai_prompt": "The title of the featured category: {image.0}" + "ai_prompt": "The four words title of the featured category related to the following image description: {image.0}" } ] } @@ -78,15 +78,15 @@ "titles": [ { "default": "Cupcakes", - "ai_prompt": "The title of the first featured category: {image.0}" + "ai_prompt": "A one word title for the featured category related to the following image description: {image.0}" }, { "default": "Sweet Danish", - "ai_prompt": "The title of the second featured category: {image.1}" + "ai_prompt": "A two words title for the featured category related to the following image description: {image.1}" }, { "default": "Warm Bread", - "ai_prompt": "The title of the third featured category: {image.2}" + "ai_prompt": "A two words title for the featured category related to the following image description: {image.2}" } ] } @@ -100,25 +100,25 @@ "titles": [ { "default": "Fresh & tasty goods", - "ai_prompt": "The title of the featured products" + "ai_prompt": "The title of the featured products with at least 20 characters" } ], "descriptions": [ { "default": "Sweet Organic Lemons", - "ai_prompt": "The description of the first featured products: {image.0}" + "ai_prompt": "The three words description of the featured product related to the following image description: {image.0}" }, { "default": "Fresh Organic Tomatoes", - "ai_prompt": "The description of the second featured products: {image.1}" + "ai_prompt": "The three words description of the featured product related to the following image description: {image.1}" }, { "default": "Fresh Lettuce (Washed)", - "ai_prompt": "The description of the third featured products: {image.2}" + "ai_prompt": "The three words description of the featured product related to the following image description: {image.2}" }, { "default": "Russet Organic Potatoes", - "ai_prompt": "The description of the fourth featured products: {image.3}" + "ai_prompt": "The three words description of the featured product related to the following image description: {image.3}" } ] } @@ -132,33 +132,33 @@ "titles": [ { "default": "New in: 3-in-1 parka", - "ai_prompt": "An impact phrase that advertises the displayed product: {image.0}. The title must have less than 30 characters" + "ai_prompt": "Generate a 3 words title that highlights one of the qualities of the product being sold" }, { "default": "Waterproof Membrane", - "ai_prompt": "A title describing the first displayed product feature. The title must have only 2 or 3 words." + "ai_prompt": "Generate a two words title that highlights one of the qualities of the product being sold" }, { "default": "Expert Craftsmanship", - "ai_prompt": "A title describing the second displayed product feature. The title must have only 2 or 3 words." + "ai_prompt": "Generate a two words title that highlights one of the qualities of the product being sold" }, { "default": "Durable Fabric", - "ai_prompt": "A title describing the third displayed product feature. The title must have only 2 or 3 words." + "ai_prompt": "Generate a two words title that highlights one of the qualities of the product being sold" } ], "descriptions": [ { "default": "Never worry about the weather again. Keep yourself dry, warm, and looking stylish.", - "ai_prompt": "A description of the first displayed product feature. The description must have less than 120 characters." + "ai_prompt": "Craft a catchy product description emphasizing one of its qualities with at least 80 characters" }, { "default": "Our products are made with expert craftsmanship and attention to detail.", - "ai_prompt": "A description of the second displayed product feature. The description must have less than 120 characters." + "ai_prompt": "Craft a catchy product description emphasizing one of its qualities with at least 80 characters" }, { "default": "We use only the highest-quality materials in our products, ensuring that they look great.", - "ai_prompt": "A description of the third displayed product feature. The description must have less than 120 characters." + "ai_prompt": "Craft a catchy product description emphasizing one of its qualities with at least 80 characters" } ] } @@ -172,29 +172,29 @@ "titles": [ { "default": "Quality Materials", - "ai_prompt": "A title describing the first displayed product feature. The title must have less than 20 characters." + "ai_prompt": "A two words title describing the first displayed product feature" }, { "default": "Expert Craftsmanship", - "ai_prompt": "A title describing the second displayed product feature. The title must have less than 20 characters." + "ai_prompt": "A two words title describing the second displayed product feature" }, { "default": "Customer Satisfaction", - "ai_prompt": "A title describing the fourth displayed product feature. The title must have less than 20 characters." + "ai_prompt": "A two words title describing the fourth displayed product feature" } ], "descriptions": [ { "default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.", - "ai_prompt": "A description of the first displayed product feature. The text must have less than 120 characters." + "ai_prompt": "A description of the product feature with at least 115 characters" }, { "default": "Our products are made with expert craftsmanship and attention to detail, ensuring that every stitch and seam is perfect.", - "ai_prompt": "A description of the second displayed product feature. The text must have less than 120 characters." + "ai_prompt": "A description of the product feature with at least 115 characters" }, { "default": "Our top priority is customer satisfaction, and we stand behind our products 100%. ", - "ai_prompt": "A description of the fourth displayed product feature. The text must have less than 120 characters." + "ai_prompt": "A description of the product feature with at least 115 characters" } ] } @@ -208,7 +208,7 @@ "titles": [ { "default": "Get cozy this fall with knit sweaters", - "ai_prompt": "An impact phrase that advertises the displayed product: {image.0}" + "ai_prompt": "An impact phrase that advertises the product the store is selling with at least 35 characters" } ] } @@ -222,19 +222,19 @@ "titles": [ { "default": "Just arrived", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 10 characters" } ], "descriptions": [ { "default": "Our early autumn collection is here.", - "ai_prompt": "A description of the product collection" + "ai_prompt": "A description of the product collection with at least 35 characters" } ], "buttons": [ { "default": "Shop now", - "ai_prompt": "The button text to go to the product collection page" + "ai_prompt": "A two words button text to go to the product collection page" } ] } @@ -248,13 +248,13 @@ "titles": [ { "default": "Brand New for the Holidays", - "ai_prompt": "An impact phrase that advertises the displayed product collection: {image.0}" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: {image.0}" } ], "descriptions": [ { "default": "Check out our brand new collection of holiday products and find the right gift for anyone.", - "ai_prompt": "A description of the product collection" + "ai_prompt": "A description of the product collection with at least 90 characters" } ] } @@ -266,7 +266,7 @@ "titles": [ { "default": "This week's popular products", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 30 characters" } ] } @@ -280,21 +280,21 @@ "titles": [ { "default": "Tech gifts under $100", - "ai_prompt": "An impact phrase that advertises the first product collection: {image.0}, {image.1}" + "ai_prompt": "An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: {image.0}, {image.1}" }, { "default": "For the gamers", - "ai_prompt": "An impact phrase that advertises the second product collection: {image.2}, {image.3}" + "ai_prompt": "An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: {image.2}, {image.3}" } ], "buttons": [ { "default": "Shop tech", - "ai_prompt": "The button text to go to the first product collection page" + "ai_prompt": "A two words button text to go to the product collection page" }, { "default": "Shop games", - "ai_prompt": "The button text to go to the second product collection page" + "ai_prompt": "A two words button text to go to the product collection page" } ] } @@ -306,13 +306,13 @@ "titles": [ { "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" } ], "buttons": [ { "default": "More new products", - "ai_prompt": "The button text to go to the product collection page" + "ai_prompt": "The button text to go to the product collection page with at least 15 characters" } ] } @@ -324,7 +324,7 @@ "titles": [ { "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" } ] } @@ -336,7 +336,7 @@ "titles": [ { "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" } ] } @@ -348,7 +348,7 @@ "titles": [ { "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters" } ] } @@ -360,7 +360,7 @@ "titles": [ { "default": "Our newest arrivals", - "ai_prompt": "An impact phrase that advertises the displayed product collection" + "ai_prompt": "An impact phrase with that advertises the product collection with at least 20 characters" } ] } @@ -372,19 +372,19 @@ "titles": [ { "default": "Fan favorites", - "ai_prompt": "An impact phrase that advertises the features products" + "ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters" } ], "descriptions": [ { "default": "Get ready to start the season right. All the fan favorites in one place at the best price.", - "ai_prompt": "A description of the featured products" + "ai_prompt": "A description of the featured products with at least 90 characters" } ], "buttons": [ { "default": "Shop All", - "ai_prompt": "The button text to go to the featured products page" + "ai_prompt": "A two words button text to go to the featured products page" } ] } @@ -398,51 +398,51 @@ "titles": [ { "default": "The Eden Jacket", - "ai_prompt": "An title that advertises the displayed product: {image.0}" + "ai_prompt": "A three words title that advertises a product related to the following image description: {image.0}" }, { "default": "100% Woolen", - "ai_prompt": "A title that advertises the first product feature" + "ai_prompt": "A two words title that advertises a product feature" }, { "default": "Fits your wardrobe", - "ai_prompt": "A title that advertises the second product feature" + "ai_prompt": "A three words title that advertises a product feature" }, { "default": "Versatile", - "ai_prompt": "A title that advertises the third product feature" + "ai_prompt": "An one word title that advertises a product feature" }, { "default": "Normal Fit", - "ai_prompt": "A title that advertises the fourth product feature" + "ai_prompt": "A two words title that advertises a product feature" } ], "descriptions": [ { "default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.", - "ai_prompt": "A description of the displayed product: {image.0}" + "ai_prompt": "The description of a product with at least 65 characters related to the following image: {image.0}" }, { "default": "Reflect your fashionable style.", - "ai_prompt": "A description of the first product feature" + "ai_prompt": "The description of a product feature with at least 30 characters" }, { "default": "Half tuck into your pants or layer over.", - "ai_prompt": "A description of the second product feature" + "ai_prompt": "The description of a product feature with at least 30 characters" }, { "default": "Button-down front for any type of mood or look.", - "ai_prompt": "A description of the third product feature" + "ai_prompt": "The description of a product feature with at least 30 characters" }, { "default": "42% Cupro 34% Linen 24% Viscose", - "ai_prompt": "A description of the fourth product feature" + "ai_prompt": "The description of a product feature with at least 30 characters" } ], "buttons": [ { "default": "View product", - "ai_prompt": "The button text to go to the product page" + "ai_prompt": "A two words button text to go to the product page" } ] } @@ -454,19 +454,19 @@ "titles": [ { "default": "Outdoor Furniture & Accessories", - "ai_prompt": "An impact phrase that advertises the first product collection" + "ai_prompt": "An impact phrase that advertises the first product collection with at least 30 characters" }, { "default": "Summer Dinning", - "ai_prompt": "An impact phrase that advertises the second product collection" + "ai_prompt": "An impact phrase that advertises the second product collection with at least 20 characters" }, { "default": "Women's Styles", - "ai_prompt": "An impact phrase that advertises the third product collection" + "ai_prompt": "An impact phrase that advertises the third product collection with at least 20 characters" }, { "default": "Kids' Styles", - "ai_prompt": "An impact phrase that advertises the fourth product collection" + "ai_prompt": "An impact phrase that advertises the fourth product collection with at least 20 characters" } ] } @@ -480,7 +480,7 @@ "titles": [ { "default": "Chairs", - "ai_prompt": "An impact phrase that advertises a products: {image.0}" + "ai_prompt": "A single word that advertises the product and is related to the following image description: {image.0}" } ] } @@ -494,7 +494,7 @@ "titles": [ { "default": "Follow us on social media", - "ai_prompt": "An phrase that advertises the social media accounts" + "ai_prompt": "A phrase that advertises the social media accounts of the store with at least 25 characters" } ] } @@ -508,35 +508,35 @@ "titles": [ { "default": "The goods", - "ai_prompt": "An impact phrase that advertises the products" + "ai_prompt": "A two words impact phrase that advertises the products" }, { "default": "Created with love and care in Australia", - "ai_prompt": "An impact phrase that advertises the products: {image.0}" + "ai_prompt": "An impact phrase that advertises the products with at least 40 characters and related to the following image description: {image.0}" }, { "default": "About us", - "ai_prompt": "An impact phrase that advertises the brand" + "ai_prompt": "A two words impact phrase that advertises the brand" }, { "default": "Marl is an independent studio and artisanal gallery", - "ai_prompt": "An impact phrase that advertises the brand: {image.1}" + "ai_prompt": "An impact phrase that advertises the brand with at least 50 characters related to the following image description: {image.1}" } ], "descriptions": [ { "default": "All items are 100% hand-made, using the potter’s wheel or traditional techniques.\n\nTimeless style.\n\nEarthy, organic feel.\n\nEnduring quality.\n\nUnique, one-of-a-kind pieces.", - "ai_prompt": "A description of the products" + "ai_prompt": "A description of the products with at least 180 characters" }, { "default": "We specialize in limited collections of handmade tableware. We collaborate with restaurants and cafes to create unique items that complement the menu perfectly. Please get in touch if you want to know more about our process and pricing.", - "ai_prompt": "A description of the products" + "ai_prompt": "A description of the products with at least 180 characters" } ], "buttons": [ { "default": "Learn more", - "ai_prompt": "The button text to go to the product page" + "ai_prompt": "A two words button text to go to the product page" } ] } @@ -548,33 +548,33 @@ "titles": [ { "default": "What our customers say", - "ai_prompt": "A title that advertises the set of testimonials" + "ai_prompt": "A title that advertises the set of testimonials with a maximum of 25 characters" }, { - "default": "Great experience", - "ai_prompt": "A title that advertises the first testimonial" + "default": "I had a great experience", + "ai_prompt": "A title that advertises a customer product review with a maximum 25 characters" }, { "default": "LOVE IT", - "ai_prompt": "A title that advertises the second testimonial" + "ai_prompt": "A title that advertises a customer product review with a maximum 10 characters" }, { "default": "Awesome couch", - "ai_prompt": "A title that advertises the third testimonial" + "ai_prompt": "A title that advertises a customer product review with a maximum 15 characters" } ], "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. The testimonial must have less than 200 characters." + "ai_prompt": "A customer product review with at least 170 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. The testimonial must have less than 200 characters." + "ai_prompt": "A customer product review with at least 170 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. The testimonial must have less than 200 characters." + "ai_prompt": "A customer product review with at least 170 characters" } ] } @@ -588,13 +588,13 @@ "titles": [ { "default": "Great experience", - "ai_prompt": "A title that advertises the testimonial" + "ai_prompt": "A two words title that advertises the testimonial" } ], "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...", - "ai_prompt": "A description of the testimonial. The testimonial must have less than 200 characters." + "ai_prompt": "A description of the testimonial with at least 225 characters" } ] } @@ -608,19 +608,19 @@ "titles": [ { "default": "100% natural denim", - "ai_prompt": "A description for a product" + "ai_prompt": "A description for a product with at least 20 characters" } ], "descriptions": [ { "default": "Only the finest goes into our products. You deserve it.", - "ai_prompt": "An impact phrase that advertises the products" + "ai_prompt": "An impact phrase that advertises the products with at least 55 characters" } ], "buttons": [ { "default": "Shop jeans", - "ai_prompt": "The button text to go to the shop page" + "ai_prompt": "A two words button text to go to the shop page" } ] } @@ -632,7 +632,7 @@ "titles": [ { "default": "Shop new arrivals", - "ai_prompt": "An impact phrase that advertises the newest additions to the store" + "ai_prompt": "An impact phrase that advertises the newest additions to the store with at least 20 characters" } ] } diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Product.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Product.php index cf99f28af8c..36af63129b5 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Product.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Product.php @@ -69,10 +69,10 @@ class Product extends AbstractRoute { * @return bool|string|\WP_Error|\WP_REST_Response */ protected function get_route_post_response( \WP_REST_Request $request ) { - $product_updater = new ProductUpdater(); - $dummy_products = $product_updater->fetch_dummy_products_to_update(); + $product_updater = new ProductUpdater(); + $product_information = $request['products_information'] ?? array(); - if ( empty( $dummy_products ) ) { + if ( empty( $product_information ) ) { return rest_ensure_response( array( 'ai_content_generated' => true, @@ -80,26 +80,7 @@ class Product extends AbstractRoute { ); } - $index = $request['index']; - if ( ! is_numeric( $index ) ) { - return rest_ensure_response( - array( - 'ai_content_generated' => false, - ) - ); - } - - $products_information = $request['products_information'] ?? array(); - - if ( ! isset( $dummy_products[ $index ] ) ) { - return rest_ensure_response( - array( - 'ai_content_generated' => false, - ) - ); - } - - $product_updater->update_product_content( $dummy_products[ $index ], $products_information ); + $product_updater->update_product_content( $product_information ); return rest_ensure_response( array( diff --git a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Products.php b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Products.php index 72024429a2d..55788115e5b 100644 --- a/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Products.php +++ b/plugins/woocommerce-blocks/src/StoreApi/Routes/V1/AI/Products.php @@ -89,20 +89,6 @@ class Products extends AbstractRoute { $business_description = get_option( 'woo_ai_describe_store_description' ); } - $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, - 'product_content' => null, - ], - $request - ) - ); - } - $ai_connection = new Connection(); $site_id = $ai_connection->get_site_id();