build_context( $post_id ); $hash = md5( wp_json_encode( $context ) ); $cached = get_post_meta( $post_id, '_ait_aipseo_ai_semantic_keywords', 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 keywords error', [ 'post_id' => $post_id, 'error' => $result->get_error_message() ] ); } return []; } $data = $this->normalize_list( $result ); update_post_meta( $post_id, '_ait_aipseo_ai_semantic_keywords', [ 'hash' => $hash, 'time' => time(), 'data' => $data, ] ); return array_slice( $data, 0, max( 1, $limit ) ); } private function build_context( int $post_id ): array { $post = get_post( $post_id ); $title = $post ? get_the_title( $post ) : ''; $content = $post ? (string) $post->post_content : ''; $headings = []; if ( $content ) { if ( preg_match_all( '/]*>(.*?)<\/h[1-3]>/', $content, $m ) ) { $headings = array_map( 'wp_strip_all_tags', $m[1] ); } } $kws = class_exists( 'AIT_AIPSEO_Keyword_Density' ) ? ( new AIT_AIPSEO_Keyword_Density() )->get_target_keywords( $post_id ) : []; return [ 'post_id' => $post_id, 'title' => trim( (string) $title ), 'headings' => array_slice( array_values( array_filter( array_map( 'trim', $headings ) ) ), 0, 10 ), 'keywords' => array_slice( $kws, 0, 5 ), 'time' => date('Y-m-d H:i'), ]; } 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_keywords_system_prompt', 'You are an SEO assistant. Given some data about a post, such as post title, headings, and target keyword(s), suggest short related terms/entities that would improve topical coverage. Output ONLY a compact JSON array of strings (no explanations).' ); $user = [ 'title' => $context['title'], 'headings' => $context['headings'], 'keywords' => $context['keywords'], 'limit' => max( 1, $limit ), ]; $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 ) ) ); } }