settings = $settings ?? new Settings(); $this->cache = new Cache_Manager((int) $this->settings->get('cache_duration', 3600)); } /** * Initialize AI manager * * @return void */ public function init(): void { // Initialize AI client based on settings add_action('init', [$this, 'initialize_client']); // Schedule cache cleanup add_action('thinkrank_daily_cleanup', [$this, 'cleanup_cache']); // Add AJAX handlers for AI requests add_action('wp_ajax_thinkrank_generate_metadata', [$this, 'ajax_generate_metadata']); add_action('wp_ajax_thinkrank_test_api_connection', [$this, 'ajax_test_connection']); } /** * Initialize AI client * * @return void */ public function initialize_client(): void { $provider = $this->settings->get('ai_provider', 'openai'); try { switch ($provider) { case 'openai': $api_key = $this->settings->get('openai_api_key'); if ($api_key) { $model = $this->settings->get('openai_model', 'gpt-5-nano'); $available_models = $this->get_available_providers()['openai']['models']; if (!in_array($model, $available_models, true)) { $model = 'gpt-5-nano'; } // Use 120-second timeout for complex AI operations $timeout = 120; $this->client = new OpenAI_Client($api_key, $model, $timeout); // OpenAI client created successfully } break; case 'claude': $api_key = $this->settings->get('claude_api_key'); if ($api_key) { $model = $this->settings->get('claude_model', 'claude-3-7-sonnet-latest'); $available_models = $this->get_available_providers()['claude']['models']; if (!in_array($model, $available_models, true)) { $model = 'claude-3-7-sonnet-latest'; } // Use 120-second timeout for complex AI operations $timeout = 120; $this->client = new Claude_Client($api_key, $model, $timeout); // Claude client created successfully } break; case 'gemini': $api_key = $this->settings->get('gemini_api_key'); if ($api_key) { $model = $this->settings->get('gemini_model', 'gemini-2.5-flash'); $available_models = $this->get_available_providers()['gemini']['models']; if (!in_array($model, $available_models, true)) { $model = 'gemini-2.5-flash'; } // Use 120-second timeout for complex AI operations $timeout = 120; $this->client = new Gemini_Client($api_key, $model, $timeout); } break; default: throw new \Exception("Unsupported AI provider: {$provider}"); } } catch (\Exception $e) { // AI client initialization failed, will be handled later } } /** * Force re-initialization of client (useful after settings change) * * @return void */ public function reinitialize_client(): void { $this->client = null; $this->initialize_client(); } /** * Get the AI client instance * * @return OpenAI_Client|Claude_Client|null AI client instance * @throws \Exception If client cannot be initialized */ public function get_client() { // Initialize client if not already done if (!$this->client) { $this->initialize_client(); } // If still not available, throw error if (!$this->client) { throw new \Exception('AI client not initialized. Please configure your API key.'); } return $this->client; } /** * Generate SEO metadata for content * * @param string $content Content to analyze * @param array $options Generation options * @return array Generated metadata * @throws \Exception If generation fails */ public function generate_seo_metadata(string $content, array $options = []): array { // Check if client is available, try to initialize if not if (!$this->client) { $this->initialize_client(); // If still not available, throw error if (!$this->client) { throw new \Exception('AI client not initialized. Please configure your API key.'); } } // Check rate limits if (!$this->check_rate_limit()) { throw new \Exception('Rate limit exceeded. Please try again later.'); } // Get current user for logging $user_id = get_current_user_id(); // Generate cache key $cache_key = $this->cache->generate_content_key($content, $options); // Check cache first $cached_result = $this->cache->get($cache_key); if ($cached_result !== null) { return $cached_result['data']; } try { // Generate metadata using AI $metadata = $this->client->generate_seo_metadata($content, $options); // Ensure user has configured their API key $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Cache the result $this->cache->set($cache_key, $metadata); // Log usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $this->client ? $this->client->get_model() : null; $ai_text = $metadata['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'SEO Metadata', $metadata['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($metadata['_ai_text']); return $metadata; } catch (\Exception $e) { throw $e; } } /** * Analyze content for SEO optimization * * @param string $content Content to analyze * @param array $metadata Existing metadata * @return array Analysis results * @throws \Exception If analysis fails */ public function analyze_content(string $content, array $metadata = []): array { if (!$this->client) { throw new \Exception('AI client not initialized'); } $user_id = get_current_user_id(); // Ensure user has configured their API key $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Check rate limits if (!$this->check_rate_limit($user_id, 'content_analysis')) { throw new \Exception('Rate limit exceeded. Please try again later.'); } // Check cache first $cache_key = 'content_analysis_' . md5($content . serialize($metadata)); $cached_result = $this->cache->get($cache_key); if ($cached_result) { return $cached_result; } try { // Analyze content using AI $analysis = $this->client->analyze_content($content, $metadata); // Cache the result $this->cache->set($cache_key, $analysis); // Log usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $this->client ? $this->client->get_model() : null; $ai_text = $analysis['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'Content Analysis', $analysis['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($analysis['_ai_text']); return $analysis; } catch (\Exception $e) { throw $e; } } /** * Test API connection * * @return array Test result */ public function test_api_connection(): array { if (!$this->client) { return [ 'success' => false, 'message' => 'AI client not initialized. Please configure your API key.', ]; } try { $success = $this->client->test_connection(); return [ 'success' => $success, 'message' => $success ? 'API connection successful!' : 'API connection failed. Please check your API key.', ]; } catch (\Exception $e) { return [ 'success' => false, 'message' => 'Connection test failed: ' . $e->getMessage(), ]; } } /** * Optimize site identity using AI * * @since 1.0.0 * * @param array $site_data Site data to optimize * @param array $options Optimization options * @return array Optimization results * @throws \Exception If optimization fails */ public function optimize_site_identity(array $site_data, array $options = []): array { // Validate input if (empty($site_data)) { throw new \Exception('Site data cannot be empty'); } $user_id = get_current_user_id(); // Ensure user has configured their API key $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Generate cache key using existing pattern $cache_key = 'site_identity_' . md5(serialize($site_data) . serialize($options)) . '_' . $user_id; // Check existing cache infrastructure $cached_result = $this->cache->get($cache_key); if ($cached_result !== null && !empty($cached_result['optimized_data'])) { return $cached_result; } // Check rate limiting if (!$this->check_rate_limit()) { throw new \Exception('Rate limit exceeded for AI optimization requests.'); } // Get AI client $client = $this->get_client(); if (!$client) { throw new \Exception('AI client not initialized. Please check your API key configuration.'); } // Perform AI optimization $optimization_results = $client->optimize_site_identity($site_data, $options); // Validate that we got meaningful results if (empty($optimization_results) || empty($optimization_results['optimized_data'])) { throw new \Exception('AI optimization returned empty results. Please try again.'); } // Add metadata $optimization_results['ai_model'] = $client->get_model(); $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai'); $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s'); $optimization_results['user_id'] = $user_id; // Cache the results (24 hours) $this->cache->set($cache_key, $optimization_results, 86400); // Record usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null); $ai_text = $optimization_results['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'Site Identity Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($optimization_results['_ai_text']); return $optimization_results; } /** * Optimize LLMs.txt content using AI * * @since 1.0.0 * * @param array $website_data Website data to optimize * @param array $options Optimization options * @return array Optimization results * @throws \Exception If optimization fails */ public function optimize_llms_txt(array $website_data, array $options = []): array { // Validate input if (empty($website_data)) { throw new \Exception('Website data cannot be empty'); } $user_id = get_current_user_id(); // Ensure user has configured their API key $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Generate cache key $cache_key = 'llms_txt_' . md5(serialize($website_data) . serialize($options)) . '_' . $user_id; // Check cache first $cached_result = $this->cache->get($cache_key); if ($cached_result !== null && !empty($cached_result['optimized_data'])) { return $cached_result; } // Check rate limiting if (!$this->check_rate_limit()) { throw new \Exception('Rate limit exceeded for AI optimization requests.'); } // Get AI client $client = $this->get_client(); if (!$client) { throw new \Exception('AI client not initialized. Please check your API key configuration.'); } // Perform AI optimization $optimization_results = $client->optimize_llms_txt($website_data, $options); // Validate that we got meaningful results if (empty($optimization_results) || empty($optimization_results['optimized_data'])) { throw new \Exception('AI optimization returned empty results. Please try again.'); } // Add metadata $optimization_results['ai_model'] = $client->get_model(); $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai'); $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s'); $optimization_results['user_id'] = $user_id; // Cache the results (24 hours) $this->cache->set($cache_key, $optimization_results, 86400); // Record usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null); $ai_text = $optimization_results['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'LLMs.txt Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($optimization_results['_ai_text']); return $optimization_results; } /** * Get available AI providers * * @return array Available providers */ public function get_available_providers(): array { return [ 'openai' => [ 'name' => 'OpenAI', 'description' => 'GPT‑5 series and GPT‑4o', 'models' => ['gpt-5-nano', 'gpt-5-mini', 'gpt-5', 'gpt-4o'], 'requires_key' => true, ], 'claude' => [ 'name' => 'Claude (Anthropic)', 'description' => 'Claude 4 and 3.7 models', 'models' => ['claude-sonnet-4-0', 'claude-opus-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'], 'requires_key' => true, ], 'gemini' => [ 'name' => 'Google Gemini', 'description' => 'Gemini 2.5 and 2.0 models', 'models' => ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro', 'gemini-2.0-flash', 'gemini-1.5-flash'], 'requires_key' => true, ], ]; } /** * Get current provider status * * @return array Provider status */ public function get_provider_status(): array { $provider = $this->settings->get('ai_provider', 'openai'); $api_key = $this->settings->get($provider . '_api_key'); return [ 'provider' => $provider, 'configured' => !empty($api_key), 'connected' => $this->client !== null, ]; } /** * AJAX handler for generating metadata * * @return void */ public function ajax_generate_metadata(): void { // Verify nonce $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')); if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) { wp_die('Security check failed'); } // Check permissions if (!current_user_can('edit_posts')) { wp_die('Insufficient permissions'); } $content = sanitize_textarea_field(wp_unslash($_POST['content'] ?? '')); $options = [ 'target_keyword' => sanitize_text_field(wp_unslash($_POST['target_keyword'] ?? '')), 'content_type' => sanitize_text_field(wp_unslash($_POST['content_type'] ?? 'blog_post')), 'tone' => sanitize_text_field(wp_unslash($_POST['tone'] ?? 'professional')), ]; try { $metadata = $this->generate_seo_metadata($content, $options); wp_send_json_success([ 'metadata' => $metadata, 'message' => __('SEO metadata generated successfully!', 'thinkrank'), ]); } catch (\Exception $e) { wp_send_json_error([ 'message' => $e->getMessage(), ]); } } /** * AJAX handler for testing API connection * * @return void */ public function ajax_test_connection(): void { // Verify nonce $nonce = sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')); if (!wp_verify_nonce($nonce, 'thinkrank_ai_nonce')) { wp_die('Security check failed'); } // Check permissions if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } $result = $this->test_api_connection(); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * Check rate limits * * @return bool True if within limits */ private function check_rate_limit(): bool { $user_id = get_current_user_id(); $max_requests = $this->settings->get('max_requests_per_minute', 10); $current_time = time(); $window_start = $current_time - 60; // 1 minute window // Clean old entries $this->rate_limits = array_filter( $this->rate_limits, function($timestamp) use ($window_start) { return $timestamp > $window_start; } ); // Count requests for this user $user_requests = array_filter( $this->rate_limits, function($timestamp, $key) use ($user_id) { return strpos($key, "user_{$user_id}_") === 0; }, ARRAY_FILTER_USE_BOTH ); if (count($user_requests) >= $max_requests) { return false; } // Add current request $this->rate_limits["user_{$user_id}_{$current_time}"] = $current_time; return true; } /** * Log AI usage with actual model information * * @param int $user_id User ID * @param string $action Action performed * @param int $tokens_used Tokens consumed * @param string|null $actual_model Actual model used (from AI response) * @param string|null $raw_response Raw AI response for debugging * @return void */ private function log_ai_usage(int $user_id, string $action, int $tokens_used, ?string $actual_model = null, ?string $raw_response = null): void { global $wpdb; $table_name = $wpdb->prefix . 'thinkrank_ai_usage'; // Prepare metadata with actual model information and raw response $metadata = []; if ($actual_model) { $metadata['actual_model'] = $actual_model; } if ($raw_response) { $metadata['raw_response'] = $raw_response; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- AI usage logging requires direct database access $wpdb->insert( $table_name, [ 'user_id' => $user_id, 'action' => $action, 'tokens_used' => $tokens_used, 'provider' => $this->settings->get('ai_provider', 'openai'), 'metadata' => !empty($metadata) ? wp_json_encode($metadata) : null, 'created_at' => current_time('mysql'), ], ['%d', '%s', '%d', '%s', '%s', '%s'] ); } /** * Cleanup expired cache entries * * @return void */ public function cleanup_cache(): void { $this->cache->clean_expired(); } /** * Optimize homepage meta content using AI (copying Site Identity pattern exactly) * * @since 1.0.0 * * @param array $content_data Meta content data to optimize * @param array $options Optimization options * @return array Optimization results * @throws \Exception If optimization fails */ public function optimize_homepage_meta(array $content_data, array $options = []): array { // Validate input if (empty($content_data)) { throw new \Exception('Content data cannot be empty'); } $user_id = get_current_user_id(); // Ensure user has configured their API key (copying Site Identity pattern) $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Generate cache key using existing pattern $cache_key = 'homepage_meta_' . md5(serialize($content_data) . serialize($options)) . '_' . $user_id; // Check existing cache infrastructure $cached_result = $this->cache->get($cache_key); if ($cached_result !== null && !empty($cached_result['optimized_data'])) { return $cached_result; } // Check rate limiting if (!$this->check_rate_limit()) { throw new \Exception('Rate limit exceeded for AI optimization requests.'); } // Get AI client $client = $this->get_client(); if (!$client) { throw new \Exception('AI client not initialized. Please check your API key configuration.'); } // Perform AI optimization $optimization_results = $client->optimize_homepage_meta($content_data, $options); // Validate that we got meaningful results if (empty($optimization_results) || empty($optimization_results['optimized_data'])) { throw new \Exception('AI optimization returned empty results. Please try again.'); } // Add metadata $optimization_results['ai_model'] = $client->get_model(); $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai'); $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s'); $optimization_results['user_id'] = $user_id; // Cache the results (24 hours) $this->cache->set($cache_key, $optimization_results, 86400); // Record usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null); $ai_text = $optimization_results['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'Homepage Meta Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($optimization_results['_ai_text']); return $optimization_results; } /** * Optimize homepage hero content using AI (copying Site Identity pattern exactly) * * @since 1.0.0 * * @param array $hero_data Hero content data to optimize * @param array $options Optimization options * @return array Optimization results * @throws \Exception If optimization fails */ public function optimize_homepage_hero(array $hero_data, array $options = []): array { // Validate input if (empty($hero_data)) { throw new \Exception('Hero data cannot be empty'); } $user_id = get_current_user_id(); // Ensure user has configured their API key (copying Site Identity pattern) $user_has_api_key = !empty($this->settings->get('openai_api_key')) || !empty($this->settings->get('claude_api_key')) || !empty($this->settings->get('gemini_api_key')); if (!$user_has_api_key) { throw new \Exception('Please configure your OpenAI, Claude, or Gemini API key in ThinkRank settings to use AI features.'); } // Generate cache key using existing pattern $cache_key = 'homepage_hero_' . md5(serialize($hero_data) . serialize($options)) . '_' . $user_id; // Check existing cache infrastructure $cached_result = $this->cache->get($cache_key); if ($cached_result !== null && !empty($cached_result['optimized_data'])) { return $cached_result; } // Check rate limiting if (!$this->check_rate_limit()) { throw new \Exception('Rate limit exceeded for AI optimization requests.'); } // Get AI client $client = $this->get_client(); if (!$client) { throw new \Exception('AI client not initialized. Please check your API key configuration.'); } // Perform AI optimization $optimization_results = $client->optimize_homepage_hero($hero_data, $options); // Validate that we got meaningful results if (empty($optimization_results) || empty($optimization_results['optimized_data'])) { throw new \Exception('AI optimization returned empty results. Please try again.'); } // Add metadata $optimization_results['ai_model'] = $client->get_model(); $optimization_results['provider'] = $this->settings->get('ai_provider', 'openai'); $optimization_results['generated_at'] = gmdate('Y-m-d H:i:s'); $optimization_results['user_id'] = $user_id; // Cache the results (24 hours) $this->cache->set($cache_key, $optimization_results, 86400); // Record usage with actual model information and raw AI text (Content Brief pattern) $actual_model = $optimization_results['ai_model'] ?? ($client ? $client->get_model() : null); $ai_text = $optimization_results['_ai_text'] ?? null; $this->log_ai_usage($user_id, 'Homepage Hero Optimization', $optimization_results['tokens_used'] ?? 0, $actual_model, $ai_text); // Remove AI text from returned data to keep it clean unset($optimization_results['_ai_text']); return $optimization_results; } }