1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
|
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
use Automattic\WooCommerce\Blocks\AI\Connection; use WP_Error;
/** * Pattern Images class. * * @internal */ class UpdatePatterns {
/** * 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-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. * @param array|WP_Error $images The array of images. * @param string $business_description The business description. * * @return bool|WP_Error */ 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 ); $patterns_dictionary = PatternsHelper::get_patterns_dictionary();
if ( is_wp_error( $patterns_dictionary ) ) { return $patterns_dictionary; }
$patterns = $this->assign_selected_images_to_patterns( $patterns_dictionary, $images['images'] );
if ( is_wp_error( $patterns ) ) { return new WP_Error( 'failed_to_set_pattern_images', __( 'Failed to set the pattern images.', 'woocommerce' ) ); }
$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.', 'woocommerce' ) ); }
$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.', 'woocommerce' ) ); }
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( "You are an experienced writer. Given a business described as: '%s', generate content for the sections using the following prompts for each one of them: `'%s'`, always making sure that the expected number of characters is respected. 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. 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, 'json_object' );
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.', 'woocommerce' ) ); }
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'] = $this->sanitize_string( $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'] = $this->sanitize_string( $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'] = $this->sanitize_string( $ai_response_content[ $counter ] );
$counter ++; } } } } }
return $patterns; }
/** * Sanitize the string from the AI generated content. It removes double quotes that can cause issues when * decoding the patterns JSON. * * @param string $string The string to be sanitized. * * @return string The sanitized string. */ private function sanitize_string( $string ) { return str_replace( '"', '', $string ); }
/** * 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 ) { if ( ! $this->pattern_has_images( $pattern ) ) { $patterns_with_images[] = $pattern; continue; }
list( $images, $alts ) = $this->get_images_for_pattern( $pattern, $selected_images ); if ( empty( $images ) ) { $patterns_with_images[] = $pattern; continue; }
$pattern['images'] = $images;
$string = wp_json_encode( $pattern );
foreach ( $alts as $i => $alt ) { $alt = empty( $alt ) ? 'the text should be related to the store description but generic enough to adapt to any image' : $alt; $string = str_replace( "{image.$i}", $alt, $string ); }
$pattern = json_decode( $string, true );
$patterns_with_images[] = $pattern; }
return $patterns_with_images; }
/** * Get the Patterns Dictionary. * * @return mixed|WP_Error|null */ public static function get_patterns_dictionary() { $patterns_dictionary = PatternsDictionary::get();
if ( empty( $patterns_dictionary ) ) { return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) ); }
return $patterns_dictionary; }
/** * Returns whether the pattern has images. * * @param array $pattern The array representing the pattern. * * @return bool True if the pattern has images, false otherwise. */ private function pattern_has_images( array $pattern ): bool { return isset( $pattern['images_total'] ) && $pattern['images_total'] > 0; }
/** * Returns the images for the given pattern. * * @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 $selected_images ): array { $images = array(); $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; }
$selected_image_url = ContentProcessor::adjust_image_size( $selected_image['URL'], 'patterns' );
$images[] = $selected_image_url; $alts[] = $selected_image['title']; }
return array( $images, $alts ); }
/** * Returns the selected image format. Defaults to portrait. * * @param array $selected_image The selected image to be assigned to the pattern. * * @return string The selected image format. */ private function get_selected_image_format( $selected_image ) { if ( ! isset( $selected_image['width'], $selected_image['height'] ) ) { return 'portrait'; }
return $selected_image['width'] === $selected_image['height'] ? 'square' : ( $selected_image['width'] > $selected_image['height'] ? 'landscape' : 'portrait' ); } }
|