PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / _inc / lib / class-jetpack-ai-helper.php
jetpack / _inc / lib Last commit date
admin-pages 1 week ago core-api 2 days ago debugger 1 month ago markdown 7 months ago class-jetpack-ai-helper.php 4 months ago class-jetpack-application-password-extras.php 3 months ago class-jetpack-blog-stats-helper.php 7 months ago class-jetpack-currencies.php 1 year ago class-jetpack-instagram-gallery-helper.php 1 month ago class-jetpack-mapbox-helper.php 3 years ago class-jetpack-newsletter-category-helper.php 7 months ago class-jetpack-podcast-feed-locator.php 1 month ago class-jetpack-podcast-helper.php 5 months ago class-jetpack-recommendations.php 1 month ago class-jetpack-spinner.php 2 months ago class-jetpack-top-posts-helper.php 1 week ago class.color.php 1 year ago class.core-rest-api-endpoints.php 1 week ago class.jetpack-automatic-install-skin.php 2 years ago class.jetpack-iframe-embed.php 3 years ago class.jetpack-password-checker.php 7 months ago class.jetpack-search-performance-logger.php 4 years ago class.media-extractor.php 1 month ago class.media-summary.php 1 week ago class.media.php 7 months ago components.php 7 months ago debugger.php 1 month ago icalendar-reader.php 1 month ago markdown.php 7 months ago plans.php 4 years ago plugins.php 4 years ago tonesque.php 7 months ago widgets.php 3 months ago
class-jetpack-ai-helper.php
474 lines
1 <?php
2 /**
3 * API helper for the AI blocks.
4 *
5 * @package automattic/jetpack
6 * @since 11.8
7 */
8
9 use Automattic\Jetpack\Connection\Client;
10 use Automattic\Jetpack\Connection\Manager;
11 use Automattic\Jetpack\Search\Plan as Search_Plan;
12 use Automattic\Jetpack\Status;
13 use Automattic\Jetpack\Status\Visitor;
14
15 /**
16 * Class Jetpack_AI_Helper
17 *
18 * @since 11.8
19 */
20 class Jetpack_AI_Helper {
21 /**
22 * Allow new completion every X seconds. Will return cached result otherwise.
23 *
24 * @var int
25 */
26 public static $text_completion_cooldown_seconds = 15;
27
28 /**
29 * Cache images for a prompt for a month.
30 *
31 * @var int
32 */
33 public static $image_generation_cache_timeout = MONTH_IN_SECONDS;
34
35 /**
36 * Cache AI-assistant feature for 60 seconds.
37 *
38 * @var int
39 */
40 public static $ai_assistant_feature_cache_timeout = 60;
41
42 /**
43 * Cache AI-assistant errors for ten seconds.
44 *
45 * @var int
46 */
47 public static $ai_assistant_feature_error_cache_timeout = 10;
48
49 /**
50 * Stores the number of JetpackAI calls in case we want to mark AI-assisted posts some way.
51 *
52 * @var int
53 */
54 public static $post_meta_with_ai_generation_number = '_jetpack_ai_calls';
55
56 /**
57 * Storing the error to prevent repeated requests to WPCOM after failure.
58 *
59 * @var null|WP_Error
60 */
61 private static $ai_assistant_failed_request = null;
62
63 /**
64 * Checks if a given request is allowed to get AI data from WordPress.com.
65 *
66 * @param WP_REST_Request $request Full details about the request.
67 *
68 * @return true|WP_Error True if the request has access, WP_Error object otherwise.
69 */
70 public static function get_status_permission_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
71
72 /*
73 * This may need to be updated
74 * to take into account the different ways we can make requests
75 * (from a WordPress.com site, from a Jetpack site).
76 */
77 if ( ! current_user_can( 'edit_posts' ) ) {
78 return new WP_Error(
79 'rest_forbidden',
80 __( 'Sorry, you are not allowed to access Jetpack AI help on this site.', 'jetpack' ),
81 array( 'status' => rest_authorization_required_code() )
82 );
83 }
84
85 return true;
86 }
87
88 /**
89 * Return true if these features should be active on the current site.
90 * Currently, it's limited to WPCOM Simple and Atomic.
91 */
92 public static function is_enabled() {
93 $default = false;
94
95 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
96 $default = true;
97 } elseif ( ( new Automattic\Jetpack\Status\Host() )->is_woa_site() ) {
98 $default = true;
99 }
100
101 /**
102 * Filter whether the AI features are enabled in the Jetpack plugin.
103 *
104 * @since 11.8
105 *
106 * @param bool $default Are AI features enabled? Defaults to false.
107 */
108 return apply_filters( 'jetpack_ai_enabled', $default );
109 }
110
111 /**
112 * Return true if the AI chat feature should be active on the current site.
113 *
114 * @todo IS_WPCOM (the endpoints need to be updated too).
115 *
116 * @return bool
117 */
118 public static function is_ai_chat_enabled() {
119 $default = false;
120
121 $connection = new Manager();
122 $plan = new Search_Plan();
123 if ( $connection->is_connected() && $plan->supports_search() ) {
124 $default = true;
125 }
126
127 /**
128 * Filter whether the AI chat feature is enabled in the Jetpack plugin.
129 *
130 * @since 12.6
131 *
132 * @param bool $default Is AI chat enabled? Defaults to false.
133 */
134 return apply_filters( 'jetpack_ai_chat_enabled', $default );
135 }
136
137 /**
138 * Get the name of the transient for image generation. Unique per prompt and allows for reuse of results for the same prompt across entire WPCOM.
139 * I expext "puppy" to always be from cache.
140 *
141 * @param string $prompt - Supplied prompt.
142 */
143 public static function transient_name_for_image_generation( $prompt ) {
144 return 'jetpack_openai_image_' . md5( $prompt );
145 }
146
147 /**
148 * Get the name of the transient for text completion. Unique per user, but not per text. Serves more as a cooldown.
149 */
150 public static function transient_name_for_completion() {
151 return 'jetpack_openai_completion_' . get_current_user_id(); // Cache for each user, so that other users dont get weird cached version from somebody else.
152 }
153
154 /**
155 * Get the name of the transient for AI assistance feature. Unique per user.
156 *
157 * @param int $blog_id - Blog ID to get the transient name for.
158 * @return string
159 */
160 public static function transient_name_for_ai_assistance_feature( $blog_id ) {
161 return 'jetpack_openai_ai_assistance_feature_' . $blog_id;
162 }
163
164 /**
165 * Mark the edited post as "touched" by AI stuff.
166 *
167 * @param int $post_id Post ID for which the content is being generated.
168 * @return void
169 */
170 private static function mark_post_as_ai_assisted( $post_id ) {
171 if ( ! $post_id ) {
172 return;
173 }
174 $previous = get_post_meta( $post_id, self::$post_meta_with_ai_generation_number, true );
175 if ( ! $previous ) {
176 $previous = 0;
177 } elseif ( ! is_numeric( $previous ) ) {
178 // Data corrupted, nothing to do.
179 return;
180 }
181 $new_value = intval( $previous ) + 1;
182 update_post_meta( $post_id, self::$post_meta_with_ai_generation_number, $new_value );
183 }
184
185 /**
186 * Get text back from WordPress.com based off a starting text.
187 *
188 * @param string $content The content provided to send to the AI.
189 * @param int $post_id Post ID for which the content is being generated.
190 * @param bool $skip_cache Skip cache and force a new request.
191 * @return mixed
192 */
193 public static function get_gpt_completion( $content, $post_id, $skip_cache = false ) {
194 $content = wp_strip_all_tags( $content );
195 $cache = get_transient( self::transient_name_for_completion() );
196 if ( $cache && ! $skip_cache ) {
197 return $cache;
198 }
199
200 if ( ( new Status() )->is_offline_mode() ) {
201 return new WP_Error(
202 'dev_mode',
203 __( 'Jetpack AI is not available in offline mode.', 'jetpack' )
204 );
205 }
206
207 $site_id = Manager::get_site_id();
208 if ( is_wp_error( $site_id ) ) {
209 return $site_id;
210 }
211
212 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
213 if ( ! class_exists( 'OpenAI' ) ) {
214 \require_lib( 'openai' );
215 }
216
217 // Set the content for chatGPT endpoint
218 $data = array(
219 array(
220 'role' => 'user',
221 'content' => $content,
222 ),
223 );
224
225 $openai = new OpenAI( 'openai', array( 'post_id' => $post_id ) );
226 $moderation_result = $openai->moderate(
227 implode(
228 ' ',
229 array_map(
230 function ( $msg ) {
231 return $msg['role'] === 'user' ? $msg['content'] : '';
232 },
233 $data
234 )
235 )
236 );
237
238 if ( is_wp_error( $moderation_result ) ) {
239 return $moderation_result;
240 }
241
242 $max_tokens = 480; // Default
243 $result = $openai->request_chat_completion( $data, $max_tokens );
244
245 if ( is_wp_error( $result ) ) {
246 return $result;
247 }
248
249 $response = $result->choices[0]->message->content;
250
251 // In case of Jetpack we are setting a transient on the WPCOM and not the remote site. I think the 'get_current_user_id' may default for the connection owner at this point but we'll deal with this later.
252 set_transient( self::transient_name_for_completion(), $response, self::$text_completion_cooldown_seconds );
253 self::mark_post_as_ai_assisted( $post_id );
254 return $response;
255 }
256
257 $response = Client::wpcom_json_api_request_as_user(
258 sprintf( '/sites/%d/jetpack-ai/completions', $site_id ),
259 2,
260 array(
261 'method' => 'post',
262 'headers' => array( 'content-type' => 'application/json' ),
263 ),
264 wp_json_encode(
265 array(
266 'content' => $content,
267 ),
268 JSON_UNESCAPED_SLASHES
269 ),
270 'wpcom'
271 );
272
273 if ( is_wp_error( $response ) ) {
274 return $response;
275 }
276
277 $data = json_decode( wp_remote_retrieve_body( $response ) );
278
279 if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
280 return new WP_Error( $data->code, $data->message, $data->data );
281 }
282
283 // Do not cache if it should be skipped.
284 if ( ! $skip_cache ) {
285 set_transient( self::transient_name_for_completion(), $data, self::$text_completion_cooldown_seconds );
286 }
287 self::mark_post_as_ai_assisted( $post_id );
288
289 return $data;
290 }
291
292 /**
293 * Get an array of image objects back from WordPress.com based off a prompt.
294 *
295 * @param string $prompt The prompt to generate images for.
296 * @param int $post_id Post ID for which the content is being generated.
297 * @return mixed
298 */
299 public static function get_dalle_generation( $prompt, $post_id ) {
300 $cache = get_transient( self::transient_name_for_image_generation( $prompt ) );
301 if ( $cache ) {
302 self::mark_post_as_ai_assisted( $post_id );
303 return $cache;
304 }
305
306 if ( ( new Status() )->is_offline_mode() ) {
307 return new WP_Error(
308 'dev_mode',
309 __( 'Jetpack AI is not available in offline mode.', 'jetpack' )
310 );
311 }
312
313 $site_id = Manager::get_site_id();
314 if ( is_wp_error( $site_id ) ) {
315 return $site_id;
316 }
317
318 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
319 if ( ! class_exists( 'OpenAI' ) ) {
320 \require_lib( 'openai' );
321 }
322
323 $result = ( new OpenAI( 'openai', array( 'post_id' => $post_id ) ) )->request_dalle_generation( $prompt );
324 if ( is_wp_error( $result ) ) {
325 return $result;
326 }
327 set_transient( self::transient_name_for_image_generation( $prompt ), $result, self::$image_generation_cache_timeout );
328 self::mark_post_as_ai_assisted( $post_id );
329 return $result;
330 }
331
332 $response = Client::wpcom_json_api_request_as_user(
333 sprintf( '/sites/%d/jetpack-ai/images/generations', $site_id ),
334 2,
335 array(
336 'method' => 'post',
337 'headers' => array( 'content-type' => 'application/json' ),
338 ),
339 wp_json_encode(
340 array(
341 'prompt' => $prompt,
342 ),
343 JSON_UNESCAPED_SLASHES
344 ),
345 'wpcom'
346 );
347
348 if ( is_wp_error( $response ) ) {
349 return $response;
350 }
351
352 $data = json_decode( wp_remote_retrieve_body( $response ) );
353
354 if ( wp_remote_retrieve_response_code( $response ) >= 400 ) {
355 return new WP_Error( $data->code, $data->message, $data->data );
356 }
357 set_transient( self::transient_name_for_image_generation( $prompt ), $data, self::$image_generation_cache_timeout );
358 self::mark_post_as_ai_assisted( $post_id );
359
360 return $data;
361 }
362
363 /**
364 * Get an object with useful data about the requests made to the AI.
365 *
366 * @return mixed
367 */
368 public static function get_ai_assistance_feature() {
369 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
370 // On WPCOM, we can get the ID from the site.
371 $blog_id = get_current_blog_id();
372 $has_ai_assistant_feature = \wpcom_site_has_feature( 'ai-assistant', $blog_id );
373
374 if ( ! class_exists( 'WPCOM\Jetpack_AI\Usage\Helper' ) ) {
375 if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php' ) ) {
376 require_once WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php';
377 } else {
378 return new WP_Error(
379 'jetpack_ai_usage_helper_not_found',
380 __( 'WPCOM\Jetpack_AI\Usage\Helper class not found.', 'jetpack' )
381 );
382 }
383 }
384
385 if ( ! class_exists( 'WPCOM\Jetpack_AI\Feature_Control' ) ) {
386 if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php' ) ) {
387 require_once WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php';
388 } else {
389 return new WP_Error(
390 'jetpack_ai_feature_control_not_found',
391 __( 'WPCOM\Jetpack_AI\Feature_Control class not found.', 'jetpack' )
392 );
393 }
394 }
395
396 // Determine the upgrade type
397 $upgrade_type = wpcom_is_vip( $blog_id ) ? 'vip' : 'default';
398
399 return array(
400 'has-feature' => $has_ai_assistant_feature,
401 'is-over-limit' => WPCOM\Jetpack_AI\Usage\Helper::is_over_limit( $blog_id ),
402 'requests-count' => WPCOM\Jetpack_AI\Usage\Helper::get_all_time_requests_count( $blog_id ),
403 'requests-limit' => WPCOM\Jetpack_AI\Usage\Helper::get_free_requests_limit( $blog_id ),
404 'usage-period' => WPCOM\Jetpack_AI\Usage\Helper::get_period_data( $blog_id ),
405 'site-require-upgrade' => WPCOM\Jetpack_AI\Usage\Helper::site_requires_upgrade( $blog_id ),
406 'upgrade-type' => $upgrade_type,
407 'upgrade-url' => WPCOM\Jetpack_AI\Usage\Helper::get_upgrade_url( $blog_id ),
408 'current-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_current_tier( $blog_id ),
409 'next-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_next_tier( $blog_id ),
410 'tier-plans' => WPCOM\Jetpack_AI\Usage\Helper::get_tier_plans_list(),
411 'tier-plans-enabled' => WPCOM\Jetpack_AI\Usage\Helper::ai_tier_plans_enabled(),
412 'costs' => WPCOM\Jetpack_AI\Usage\Helper::get_costs(),
413 'features-control' => WPCOM\Jetpack_AI\Feature_Control::get_features(),
414 );
415 }
416
417 // Outside of WPCOM, we need to fetch the data from the site.
418 $blog_id = Jetpack_Options::get_option( 'id' );
419
420 // Try to pick the AI Assistant feature from cache.
421 $transient_name = self::transient_name_for_ai_assistance_feature( $blog_id );
422 $cache = get_transient( $transient_name );
423 if ( $cache ) {
424 return $cache;
425 }
426
427 if ( null !== static::$ai_assistant_failed_request ) {
428 return static::$ai_assistant_failed_request;
429 }
430
431 $request_path = sprintf( '/sites/%d/jetpack-ai/ai-assistant-feature', $blog_id );
432
433 $wpcom_request = Client::wpcom_json_api_request_as_user(
434 $request_path,
435 'v2',
436 array(
437 'method' => 'GET',
438 'headers' => array(
439 'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
440 ),
441 'timeout' => 30,
442 ),
443 null,
444 'wpcom'
445 );
446
447 $response_code = wp_remote_retrieve_response_code( $wpcom_request );
448 if ( 200 === $response_code ) {
449 $ai_assistant_feature_data = json_decode( wp_remote_retrieve_body( $wpcom_request ), true );
450
451 // Cache the AI Assistant feature, for Jetpack sites.
452 set_transient( $transient_name, $ai_assistant_feature_data, self::$ai_assistant_feature_cache_timeout );
453
454 return $ai_assistant_feature_data;
455 } else {
456 $error = new WP_Error(
457 'failed_to_fetch_data',
458 esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
459 array(
460 'status' => $response_code,
461 'ts' => time(),
462 )
463 );
464
465 // Cache the AI Assistant feature error, for Jetpack sites, avoid API hammering.
466 set_transient( $transient_name, $error, self::$ai_assistant_feature_error_cache_timeout );
467
468 static::$ai_assistant_failed_request = $error;
469
470 return $error;
471 }
472 }
473 }
474