PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
← All changes | _inc/lib/class-jetpack-ai-helper.php +176 -41 12.6.4 → 16.3-a.5 View file →
@@ -11,8 +11,13 @@
11 11 use Automattic\Jetpack\Search\Plan as Search_Plan;
12 12 use Automattic\Jetpack\Status;
13 13 use Automattic\Jetpack\Status\Visitor;
14 14
15 +// Required directly rather than relying on the plugin bootstrap: on
16 +// WordPress.com Simple this helper is loaded outside load-jetpack.php
17 +// (see the GUIDELINES_BANNER_DISMISSED_META_KEY note below).
18 +require_once __DIR__ . '/class-jetpack-ai-settings.php';
19 +
15 20 /**
16 21 * Class Jetpack_AI_Helper
17 22 *
18 23 * @since 11.8
@@ -18,8 +23,23 @@
18 23 * @since 11.8
19 24 */
20 25 class Jetpack_AI_Helper {
21 26 /**
27 + * User meta key storing whether the user dismissed the Content Guidelines
28 + * AI empty-state banner. Written by the guidelines-banner-dismissed REST
29 + * endpoint and read by the Content Guidelines admin-page preload.
30 + *
31 + * Lives here rather than on the endpoint class because on WordPress.com
32 + * Simple the wpcom-endpoints classes are only loaded in REST requests,
33 + * while the preload needs the key during admin page loads.
34 + *
35 + * @since 16.1
36 + *
37 + * @var string
38 + */
39 + const GUIDELINES_BANNER_DISMISSED_META_KEY = 'jetpack_content_guidelines_ai_banner_dismissed';
40 +
41 + /**
22 42 * Allow new completion every X seconds. Will return cached result otherwise.
23 43 *
24 44 * @var int
25 45 */
@@ -32,8 +52,22 @@
32 52 */
33 53 public static $image_generation_cache_timeout = MONTH_IN_SECONDS;
34 54
35 55 /**
56 + * Cache AI-assistant feature for 60 seconds.
57 + *
58 + * @var int
59 + */
60 + public static $ai_assistant_feature_cache_timeout = 60;
61 +
62 + /**
63 + * Cache AI-assistant errors for ten seconds.
64 + *
65 + * @var int
66 + */
67 + public static $ai_assistant_feature_error_cache_timeout = 10;
68 +
69 + /**
36 70 * Stores the number of JetpackAI calls in case we want to mark AI-assisted posts some way.
37 71 *
38 72 * @var int
39 73 */
@@ -39,8 +73,15 @@
39 73 */
40 74 public static $post_meta_with_ai_generation_number = '_jetpack_ai_calls';
41 75
42 76 /**
77 + * Storing the error to prevent repeated requests to WPCOM after failure.
78 + *
79 + * @var null|WP_Error
80 + */
81 + private static $ai_assistant_failed_request = null;
82 +
83 + /**
43 84 * Checks if a given request is allowed to get AI data from WordPress.com.
44 85 *
45 86 * @param WP_REST_Request $request Full details about the request.
46 87 *
@@ -76,19 +117,55 @@
76 117 } elseif ( ( new Automattic\Jetpack\Status\Host() )->is_woa_site() ) {
77 118 $default = true;
78 119 }
79 120
80 - /**
81 - * Filter whether the AI features are enabled in the Jetpack plugin.
82 - *
83 - * @since 11.8
84 - *
85 - * @param bool $default Are AI features enabled? Defaults to false.
86 - */
87 - return apply_filters( 'jetpack_ai_enabled', $default );
121 + // The jetpack_ai_enabled filter runs inside the helper; the host and
122 + // master gates apply after the chain and cannot be filtered back on.
123 + return Jetpack_AI_Settings::is_ai_enabled( $default );
88 124 }
89 125
90 126 /**
127 + * Return true if the Content Guidelines AI surfaces should be active on the
128 + * current site.
129 + *
130 + * Same platforms as is_enabled() (WPCOM Simple and Atomic), plus WordPress
131 + * VIP sites. Kept separate from is_enabled() so widening Content Guidelines
132 + * to VIP does not also open the general AI proxy endpoint there.
133 + *
134 + * @since 16.2
135 + *
136 + * @return bool
137 + */
138 + public static function is_enabled_for_content_guidelines() {
139 + $default = false;
140 +
141 + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
142 + $default = true;
143 + } else {
144 + $host = new Automattic\Jetpack\Status\Host();
145 + if ( $host->is_woa_site() || $host->is_vip_site() ) {
146 + $default = true;
147 + }
148 + }
149 +
150 + // The jetpack_ai_enabled filter runs inside the helper; the host and
151 + // master gates apply after the chain and cannot be filtered back on.
152 + return Jetpack_AI_Settings::is_ai_enabled( $default );
153 + }
154 +
155 + /**
156 + * Whether the current user has dismissed the Content Guidelines AI
157 + * empty-state banner.
158 + *
159 + * @since 16.1
160 + *
161 + * @return bool
162 + */
163 + public static function is_guidelines_banner_dismissed() {
164 + return (bool) get_user_meta( get_current_user_id(), self::GUIDELINES_BANNER_DISMISSED_META_KEY, true );
165 + }
166 +
167 + /**
91 168 * Return true if the AI chat feature should be active on the current site.
92 169 *
93 170 * @todo IS_WPCOM (the endpoints need to be updated too).
94 171 *
@@ -130,8 +207,18 @@
130 207 return 'jetpack_openai_completion_' . get_current_user_id(); // Cache for each user, so that other users dont get weird cached version from somebody else.
131 208 }
132 209
133 210 /**
211 + * Get the name of the transient for AI assistance feature. Unique per user.
212 + *
213 + * @param int $blog_id - Blog ID to get the transient name for.
214 + * @return string
215 + */
216 + public static function transient_name_for_ai_assistance_feature( $blog_id ) {
217 + return 'jetpack_openai_ai_assistance_feature_' . $blog_id;
218 + }
219 +
220 + /**
134 221 * Mark the edited post as "touched" by AI stuff.
135 222 *
136 223 * @param int $post_id Post ID for which the content is being generated.
137 224 * @return void
@@ -190,10 +277,28 @@
190 277 'content' => $content,
191 278 ),
192 279 );
193 280
194 - $result = ( new OpenAI( 'openai', array( 'post_id' => $post_id ) ) )->request_chat_completion( $data );
281 + $openai = new OpenAI( 'openai', array( 'post_id' => $post_id ) );
282 + $moderation_result = $openai->moderate(
283 + implode(
284 + ' ',
285 + array_map(
286 + function ( $msg ) {
287 + return $msg['role'] === 'user' ? $msg['content'] : '';
288 + },
289 + $data
290 + )
291 + )
292 + );
195 293
294 + if ( is_wp_error( $moderation_result ) ) {
295 + return $moderation_result;
296 + }
297 +
298 + $max_tokens = 480; // Default
299 + $result = $openai->request_chat_completion( $data, $max_tokens );
300 +
196 301 if ( is_wp_error( $result ) ) {
197 302 return $result;
198 303 }
199 304
@@ -214,9 +319,10 @@
214 319 ),
215 320 wp_json_encode(
216 321 array(
217 322 'content' => $content,
218 - )
323 + ),
324 + JSON_UNESCAPED_SLASHES
219 325 ),
220 326 'wpcom'
221 327 );
222 328
@@ -288,9 +394,10 @@
288 394 ),
289 395 wp_json_encode(
290 396 array(
291 397 'prompt' => $prompt,
292 - )
398 + ),
399 + JSON_UNESCAPED_SLASHES
293 400 ),
294 401 'wpcom'
295 402 );
296 403
@@ -315,57 +422,69 @@
315 422 * @return mixed
316 423 */
317 424 public static function get_ai_assistance_feature() {
318 425 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
319 - $has_ai_assistant_feature = \wpcom_site_has_feature( 'ai-assistant' );
320 - if ( ! class_exists( 'OpenAI' ) ) {
321 - \require_lib( 'openai' );
322 - }
426 + // On WPCOM, we can get the ID from the site.
427 + $blog_id = get_current_blog_id();
428 + $has_ai_assistant_feature = \wpcom_site_has_feature( 'ai-assistant', $blog_id );
323 429
324 - if ( ! class_exists( 'OpenAI_Limit_Usage' ) ) {
325 - if ( is_readable( WP_PLUGIN_DIR . '/openai/openai-limit-usage.php' ) ) {
326 - require_once WP_PLUGIN_DIR . '/openai/openai-limit-usage.php';
430 + if ( ! class_exists( 'WPCOM\Jetpack_AI\Usage\Helper' ) ) {
431 + if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php' ) ) {
432 + require_once WP_CONTENT_DIR . '/lib/jetpack-ai/usage/helper.php';
327 433 } else {
328 434 return new WP_Error(
329 - 'openai_limit_usage_not_found',
330 - __( 'OpenAI_Limit_Usage class not found.', 'jetpack' )
435 + 'jetpack_ai_usage_helper_not_found',
436 + __( 'WPCOM\Jetpack_AI\Usage\Helper class not found.', 'jetpack' )
331 437 );
332 438 }
333 439 }
334 440
335 - if ( ! class_exists( 'OpenAI_Request_Count' ) ) {
336 - if ( is_readable( WP_PLUGIN_DIR . '/openai/openai-request-count.php' ) ) {
337 - require_once WP_PLUGIN_DIR . '/openai/openai-request-count.php';
441 + if ( ! class_exists( 'WPCOM\Jetpack_AI\Feature_Control' ) ) {
442 + if ( is_readable( WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php' ) ) {
443 + require_once WP_CONTENT_DIR . '/lib/jetpack-ai/feature-control.php';
338 444 } else {
339 445 return new WP_Error(
340 - 'openai_request_count_not_found',
341 - __( 'OpenAI_Request_Count class not found.', 'jetpack' )
446 + 'jetpack_ai_feature_control_not_found',
447 + __( 'WPCOM\Jetpack_AI\Feature_Control class not found.', 'jetpack' )
342 448 );
343 449 }
344 450 }
345 451
346 - $blog_id = get_current_blog_id();
347 - $is_over_limit = \OpenAI_Limit_Usage::is_blog_over_request_limit( $blog_id );
348 - $requests_limit = \OpenAI_Limit_Usage::get_free_requests_limit( $blog_id );
349 - $requests_count = \OpenAI_Request_Count::get_count( $blog_id );
350 -
351 - // Check if the site requires an upgrade.
352 - $require_upgrade = $is_over_limit && ! $has_ai_assistant_feature;
353 -
354 452 // Determine the upgrade type
355 453 $upgrade_type = wpcom_is_vip( $blog_id ) ? 'vip' : 'default';
356 454
357 455 return array(
358 456 'has-feature' => $has_ai_assistant_feature,
359 - 'is-over-limit' => $is_over_limit,
360 - 'requests-count' => $requests_count,
361 - 'requests-limit' => $requests_limit,
362 - 'site-require-upgrade' => $require_upgrade,
457 + 'is-over-limit' => WPCOM\Jetpack_AI\Usage\Helper::is_over_limit( $blog_id ),
458 + 'requests-count' => WPCOM\Jetpack_AI\Usage\Helper::get_all_time_requests_count( $blog_id ),
459 + 'requests-limit' => WPCOM\Jetpack_AI\Usage\Helper::get_free_requests_limit( $blog_id ),
460 + 'usage-period' => WPCOM\Jetpack_AI\Usage\Helper::get_period_data( $blog_id ),
461 + 'site-require-upgrade' => WPCOM\Jetpack_AI\Usage\Helper::site_requires_upgrade( $blog_id ),
363 462 'upgrade-type' => $upgrade_type,
463 + 'upgrade-url' => WPCOM\Jetpack_AI\Usage\Helper::get_upgrade_url( $blog_id ),
464 + 'current-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_current_tier( $blog_id ),
465 + 'next-tier' => WPCOM\Jetpack_AI\Usage\Helper::get_next_tier( $blog_id ),
466 + 'tier-plans' => WPCOM\Jetpack_AI\Usage\Helper::get_tier_plans_list(),
467 + 'tier-plans-enabled' => WPCOM\Jetpack_AI\Usage\Helper::ai_tier_plans_enabled(),
468 + 'costs' => WPCOM\Jetpack_AI\Usage\Helper::get_costs(),
469 + 'features-control' => WPCOM\Jetpack_AI\Feature_Control::get_features(),
364 470 );
365 471 }
366 472
367 - $blog_id = Jetpack_Options::get_option( 'id' );
473 + // Outside of WPCOM, we need to fetch the data from the site.
474 + $blog_id = Jetpack_Options::get_option( 'id' );
475 +
476 + // Try to pick the AI Assistant feature from cache.
477 + $transient_name = self::transient_name_for_ai_assistance_feature( $blog_id );
478 + $cache = get_transient( $transient_name );
479 + if ( $cache ) {
480 + return $cache;
481 + }
482 +
483 + if ( null !== static::$ai_assistant_failed_request ) {
484 + return static::$ai_assistant_failed_request;
485 + }
486 +
368 487 $request_path = sprintf( '/sites/%d/jetpack-ai/ai-assistant-feature', $blog_id );
369 488
370 489 $wpcom_request = Client::wpcom_json_api_request_as_user(
371 490 $request_path,
@@ -374,8 +493,9 @@
374 493 'method' => 'GET',
375 494 'headers' => array(
376 495 'X-Forwarded-For' => ( new Visitor() )->get_ip( true ),
377 496 ),
497 + 'timeout' => 30,
378 498 ),
379 499 null,
380 500 'wpcom'
381 501 );
@@ -381,14 +501,29 @@
381 501 );
382 502
383 503 $response_code = wp_remote_retrieve_response_code( $wpcom_request );
384 504 if ( 200 === $response_code ) {
385 - return json_decode( wp_remote_retrieve_body( $wpcom_request ), true );
505 + $ai_assistant_feature_data = json_decode( wp_remote_retrieve_body( $wpcom_request ), true );
506 +
507 + // Cache the AI Assistant feature, for Jetpack sites.
508 + set_transient( $transient_name, $ai_assistant_feature_data, self::$ai_assistant_feature_cache_timeout );
509 +
510 + return $ai_assistant_feature_data;
386 511 } else {
387 - return new WP_Error(
512 + $error = new WP_Error(
388 513 'failed_to_fetch_data',
389 514 esc_html__( 'Unable to fetch the requested data.', 'jetpack' ),
390 - array( 'status' => $response_code )
515 + array(
516 + 'status' => $response_code,
517 + 'ts' => time(),
518 + )
391 519 );
520 +
521 + // Cache the AI Assistant feature error, for Jetpack sites, avoid API hammering.
522 + set_transient( $transient_name, $error, self::$ai_assistant_feature_error_cache_timeout );
523 +
524 + static::$ai_assistant_failed_request = $error;
525 +
526 + return $error;
392 527 }
393 528 }
394 529 }