ensure_signals( $post_id ); $kws = AIT_AIPSEO_Keyword_Density::get_target_keywords( $post_id ); $post_type = get_post_type( $post_id ); $context = [ 'post_id' => $post_id, 'signals' => $signals, 'keywords' => array_slice( $kws, 0, 5 ), 'post_type' => $post_type, 'time' => date('Y-m-d H:i'), ]; $hash = md5( wp_json_encode( $context ) ); $cached = get_post_meta( $post_id, '_ait_aipseo_ai_fixit_tips', true ); if ( is_array( $cached ) && ! empty( $cached['hash'] ) && $cached['hash'] === $hash && ! empty( $cached['time'] ) && ( time() - (int) $cached['time'] ) < $ttl && ! empty( $cached['data'] ) ) { return array_slice( (array) $cached['data'], 0, max( 1, $limit ) ); } $result = $this->query_openai( $context, $limit ); if ( is_wp_error( $result ) ) { if ( class_exists( 'AIT_AIPSEO_Logger' ) ) { AIT_AIPSEO_Logger::log_error( 'AI fixit error', [ 'post_id' => $post_id, 'error' => $result->get_error_message() ] ); } return []; } $data = $this->normalize_list( $result ); $thumb_id = get_post_thumbnail_id( $post_id ); $alt = $thumb_id ? get_post_meta( $thumb_id, '_wp_attachment_image_alt', true ) : ''; if ( $thumb_id && empty( trim( $alt ) ) ) { $suggestion = sprintf( __( 'Your featured image is missing alt text. Consider installing the free AI Image Alt Text plugin to automatically generate alt text.', 'ai-powered-seo-suggestions' ), 'https://wordpress.org/plugins/ai-image-alt-text/' ); array_unshift( $data, $suggestion ); } update_post_meta( $post_id, '_ait_aipseo_ai_fixit_tips', [ 'hash' => $hash, 'time' => time(), 'data' => $data, ] ); return array_slice( $data, 0, max( 1, $limit ) ); } private function ensure_signals( int $post_id ): array { $cached = get_post_meta( $post_id, '_ait_aipseo_score', true ); if ( is_array( $cached ) && ! empty( $cached['signals'] ) ) { return $cached['signals']; } $analyzer = new AIT_AIPSEO_Analyzer(); $res = $analyzer->analyze( $post_id ); return is_array( $res['signals'] ?? null ) ? $res['signals'] : []; } private function query_openai( array $context, int $limit ) { $api_key = (string) AIT_AIPSEO_Settings::get( 'openai_api_key' ); if ( '' === trim( $api_key ) ) { return new WP_Error( 'aipseo_no_api_key', __( 'OpenAI API key is not set.', 'ai-powered-seo-suggestions' ) ); } $model = (string) AIT_AIPSEO_Settings::get( 'openai_model' ); $override = (string) AIT_AIPSEO_Settings::get( 'force_model_override' ); if ( '' != trim( $override ) ) { $model = $override; } $timeout = (int) ( AIT_AIPSEO_Settings::get( 'request_timeout' ) ?: 20 ); $sys = (string) apply_filters( 'ait_aipseo/ai_fixit_system_prompt', sprintf( 'You are an SEO assistant. Convert numeric on-page signals into specific, actionable suggestions for the editor. The suggestions should be geared towards a %s WordPress post. Use short, imperative sentences. Output ONLY a compact JSON array of strings (no numbering, no explanations).', $context['post_type'], ) ); $user = [ 'limit' => max( 1, $limit ), 'keywords' => $context['keywords'], 'signals' => $this->summarize_signals_for_prompt( (array) $context['signals'] ), ]; $body = [ 'model' => $model, 'messages' => [ [ 'role' => 'system', 'content' => $sys ], [ 'role' => 'user', 'content' => wp_json_encode( $user ) ], ] ]; if ( $model != 'gpt-5' ) { $body['temperature'] = 0.2; } $response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [ 'timeout' => $timeout, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, ], 'body' => wp_json_encode( $body ), ] ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $raw = wp_remote_retrieve_body( $response ); if ( $code < 200 || $code > 299 || '' === $raw ) { return new WP_Error( 'aipseo_openai_http', __( 'Unexpected response from OpenAI.', 'ai-powered-seo-suggestions' ) ); } $data = json_decode( $raw, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { return new WP_Error( 'aipseo_openai_json', __( 'Invalid JSON from OpenAI.', 'ai-powered-seo-suggestions' ) ); } $content = (string) ( $data['choices'][0]['message']['content'] ?? '' ); return $content; } private function normalize_list( $content ): array { if ( is_array( $content ) ) { return array_values( array_filter( array_map( 'sanitize_text_field', $content ) ) ); } $str = (string) $content; if ( preg_match( '/\[.*\]/s', $str, $m ) ) { $decoded = json_decode( $m[0], true ); if ( is_array( $decoded ) ) { return array_values( array_filter( array_map( 'sanitize_text_field', $decoded ) ) ); } } $parts = preg_split( '/[\n,]+/', $str ); return array_values( array_filter( array_map( function( $v ) { return sanitize_text_field( trim( (string) $v ) ); }, (array) $parts ) ) ); } private function summarize_signals_for_prompt( array $signals ): array { $out = []; foreach ( [ 'title','meta','headings','readability','links','images' ] as $k ) { $val = isset( $signals[ $k ] ) ? (float) $signals[ $k ] : 0.0; $out[ $k ] = $this->bucket( $val ); } return $out; } private function bucket( float $v ): string { if ( $v >= 0.8 ) return 'strong'; if ( $v >= 0.6 ) return 'okay'; if ( $v >= 0.4 ) return 'weak'; return 'poor'; } }