| 1 |
<?php |
| 2 |
/** |
| 3 |
* Helper functions for the AI plugin. |
| 4 |
* |
| 5 |
* @package WordPress\AI |
| 6 |
*/ |
| 7 |
|
| 8 |
declare( strict_types=1 ); |
| 9 |
|
| 10 |
namespace WordPress\AI; |
| 11 |
|
| 12 |
use Throwable; |
| 13 |
use WordPress\AI_Client\AI_Client; |
| 14 |
|
| 15 |
/** |
| 16 |
* Purposely using return instead of exit here. |
| 17 |
* |
| 18 |
* This file is loaded via the composer files directive. |
| 19 |
* When tools like PHPCS and PHPStan run, they include |
| 20 |
* our composer autoloader and that will then load this file, |
| 21 |
* causing the script to exit and not function properly. |
| 22 |
*/ |
| 23 |
if ( ! defined( 'ABSPATH' ) ) { |
| 24 |
return; |
| 25 |
} |
| 26 |
|
| 27 |
/** |
| 28 |
* Normalizes the content by cleaning it and removing unwanted HTML tags. |
| 29 |
* |
| 30 |
* @since 0.1.0 |
| 31 |
* |
| 32 |
* @param string $content The content to normalize. |
| 33 |
* @return string The normalized content. |
| 34 |
*/ |
| 35 |
function normalize_content( string $content ): string { |
| 36 |
/** |
| 37 |
* Hook to filter content before cleaning it. |
| 38 |
* |
| 39 |
* @since 0.1.0 |
| 40 |
* |
| 41 |
* @param string $post_content The post content. |
| 42 |
* |
| 43 |
* @return string The filtered Post content. |
| 44 |
*/ |
| 45 |
$content = (string) apply_filters( 'ai_experiments_pre_normalize_content', $content ); |
| 46 |
|
| 47 |
// Strip HTML entities. |
| 48 |
$content = preg_replace( '/&#?[a-z0-9]{2,8};/i', '', $content ); |
| 49 |
|
| 50 |
// Replace HTML linebreaks with newlines. |
| 51 |
$content = preg_replace( '#<br\s?/?>#', "\n\n", (string) $content ); |
| 52 |
|
| 53 |
// Strip all HTML tags. |
| 54 |
$content = wp_strip_all_tags( (string) $content ); |
| 55 |
|
| 56 |
// Remove unrendered shortcode tags. |
| 57 |
$content = preg_replace( '#\[.+\](.+)\[/.+\]#', '$1', $content ); |
| 58 |
|
| 59 |
/** |
| 60 |
* Filters the normalized content to allow for additional cleanup. |
| 61 |
* |
| 62 |
* @since 0.1.0 |
| 63 |
* |
| 64 |
* @param string $content The normalized content. |
| 65 |
* |
| 66 |
* @return string The filtered normalized content. |
| 67 |
*/ |
| 68 |
$content = (string) apply_filters( 'ai_experiments_normalize_content', (string) $content ); |
| 69 |
|
| 70 |
return trim( $content ); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Returns the context for the given post ID. |
| 75 |
* |
| 76 |
* @since 0.1.0 |
| 77 |
* |
| 78 |
* @param int $post_id The ID of the post to get the context for. |
| 79 |
* @return array<string, string> The context for the given post ID. |
| 80 |
*/ |
| 81 |
function get_post_context( int $post_id ): array { |
| 82 |
$context = array(); |
| 83 |
|
| 84 |
// Get the post details using the get-post-details ability. |
| 85 |
$details_ability = wp_get_ability( 'ai/get-post-details' ); |
| 86 |
if ( $details_ability ) { |
| 87 |
$details = $details_ability->execute( array( 'post_id' => $post_id ) ); |
| 88 |
|
| 89 |
if ( is_array( $details ) ) { |
| 90 |
$context = array_merge( $context, $details ); |
| 91 |
|
| 92 |
if ( isset( $context['content'] ) ) { |
| 93 |
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound |
| 94 |
$context['content'] = normalize_content( (string) apply_filters( 'the_content', $context['content'] ) ); |
| 95 |
} |
| 96 |
|
| 97 |
if ( isset( $context['type'] ) ) { |
| 98 |
$context['content_type'] = $context['type']; |
| 99 |
unset( $context['type'] ); |
| 100 |
} |
| 101 |
|
| 102 |
// Remove any empty context values. |
| 103 |
$context = array_filter( $context ); |
| 104 |
} |
| 105 |
} |
| 106 |
|
| 107 |
// Get the post terms using the get-terms ability. |
| 108 |
$terms_ability = wp_get_ability( 'ai/get-post-terms' ); |
| 109 |
if ( $terms_ability ) { |
| 110 |
$terms = $terms_ability->execute( array( 'post_id' => $post_id ) ); |
| 111 |
|
| 112 |
if ( $terms && ! is_wp_error( $terms ) ) { |
| 113 |
$grouped_terms = array(); |
| 114 |
|
| 115 |
foreach ( $terms as $term ) { |
| 116 |
$grouped_terms[ $term->taxonomy ][] = $term->name; |
| 117 |
} |
| 118 |
|
| 119 |
$context = array_merge( |
| 120 |
$context, |
| 121 |
array_map( |
| 122 |
static fn( array $term_names ): string => implode( ', ', $term_names ), |
| 123 |
$grouped_terms |
| 124 |
) |
| 125 |
); |
| 126 |
} |
| 127 |
} |
| 128 |
|
| 129 |
return $context; |
| 130 |
} |
| 131 |
|
| 132 |
/** |
| 133 |
* Returns the preferred models. |
| 134 |
* |
| 135 |
* @since 0.1.0 |
| 136 |
* |
| 137 |
* @return array<int, array{string, string}> The preferred models. |
| 138 |
*/ |
| 139 |
function get_preferred_models(): array { |
| 140 |
$preferred_models = array( |
| 141 |
array( |
| 142 |
'anthropic', |
| 143 |
'claude-haiku-4-5', |
| 144 |
), |
| 145 |
array( |
| 146 |
'google', |
| 147 |
'gemini-2.5-flash', |
| 148 |
), |
| 149 |
array( |
| 150 |
'openai', |
| 151 |
'gpt-4o-mini', |
| 152 |
), |
| 153 |
array( |
| 154 |
'openai', |
| 155 |
'gpt-4.1', |
| 156 |
), |
| 157 |
); |
| 158 |
|
| 159 |
/** |
| 160 |
* Filters the preferred models. |
| 161 |
* |
| 162 |
* @since 0.1.0 |
| 163 |
* |
| 164 |
* @param array<int, array{string, string}> $preferred_models The preferred models. |
| 165 |
* @return array<int, array{string, string}> The filtered preferred models. |
| 166 |
*/ |
| 167 |
return (array) apply_filters( 'ai_experiments_preferred_models', $preferred_models ); |
| 168 |
} |
| 169 |
|
| 170 |
/** |
| 171 |
* Returns the preferred image models. |
| 172 |
* |
| 173 |
* @since 0.2.0 |
| 174 |
* |
| 175 |
* @return array<int, array{string, string}> The preferred image models. |
| 176 |
*/ |
| 177 |
function get_preferred_image_models(): array { |
| 178 |
$preferred_models = array( |
| 179 |
array( |
| 180 |
'google', |
| 181 |
'gemini-3-pro-image-preview', |
| 182 |
), |
| 183 |
array( |
| 184 |
'google', |
| 185 |
'gemini-2.5-flash-image', |
| 186 |
), |
| 187 |
array( |
| 188 |
'google', |
| 189 |
'imagen-4.0-generate-001', |
| 190 |
), |
| 191 |
array( |
| 192 |
'openai', |
| 193 |
'gpt-image-1', |
| 194 |
), |
| 195 |
array( |
| 196 |
'openai', |
| 197 |
'dall-e-3', |
| 198 |
), |
| 199 |
); |
| 200 |
|
| 201 |
/** |
| 202 |
* Filters the preferred image models. |
| 203 |
* |
| 204 |
* @since 0.2.0 |
| 205 |
* |
| 206 |
* @param array<int, array{string, string}> $preferred_models The preferred image models. |
| 207 |
* @return array<int, array{string, string}> The filtered preferred image models. |
| 208 |
*/ |
| 209 |
return (array) apply_filters( 'ai_experiments_preferred_image_models', $preferred_models ); |
| 210 |
} |
| 211 |
|
| 212 |
/** |
| 213 |
* Checks if we have AI credentials set. |
| 214 |
* |
| 215 |
* @since 0.1.0 |
| 216 |
* |
| 217 |
* @return bool True if we have AI credentials, false otherwise. |
| 218 |
*/ |
| 219 |
function has_ai_credentials(): bool { |
| 220 |
$credentials = get_option( 'wp_ai_client_provider_credentials', array() ); |
| 221 |
|
| 222 |
// If there are no credentials, return false. |
| 223 |
if ( ! is_array( $credentials ) || empty( $credentials ) ) { |
| 224 |
return false; |
| 225 |
} |
| 226 |
|
| 227 |
// If all of the AI keys are empty, return false; otherwise, return true. |
| 228 |
return ! empty( |
| 229 |
array_filter( |
| 230 |
$credentials, |
| 231 |
static function ( $api_key ): bool { |
| 232 |
return is_string( $api_key ) && '' !== $api_key; |
| 233 |
} |
| 234 |
) |
| 235 |
); |
| 236 |
} |
| 237 |
|
| 238 |
/** |
| 239 |
* Checks if we have valid AI credentials. |
| 240 |
* |
| 241 |
* @since 0.1.0 |
| 242 |
* |
| 243 |
* @return bool True if we have valid AI credentials, false otherwise. |
| 244 |
*/ |
| 245 |
function has_valid_ai_credentials(): bool { |
| 246 |
// If we have no AI credentials, return false. |
| 247 |
if ( ! has_ai_credentials() ) { |
| 248 |
return false; |
| 249 |
} |
| 250 |
|
| 251 |
/** |
| 252 |
* Filters whether valid AI credentials are available. |
| 253 |
* |
| 254 |
* Allows overriding the credentials check, useful for testing. |
| 255 |
* |
| 256 |
* @since 0.1.0 |
| 257 |
* |
| 258 |
* @param bool|null $has_valid_credentials Whether valid credentials are available. Return null to use default check. |
| 259 |
* @return bool|null True if valid credentials are available, false otherwise, or null to use default check. |
| 260 |
*/ |
| 261 |
$valid = apply_filters( 'ai_experiments_pre_has_valid_credentials_check', null ); |
| 262 |
if ( null !== $valid ) { |
| 263 |
return (bool) $valid; |
| 264 |
} |
| 265 |
|
| 266 |
// See if we have credentials that give us access to generate text. |
| 267 |
try { |
| 268 |
return AI_Client::prompt( 'Test' )->is_supported_for_text_generation(); |
| 269 |
} catch ( Throwable $t ) { |
| 270 |
return false; |
| 271 |
} |
| 272 |
} |
| 273 |
|