serper_api_key = get_option('aigh_serper_api_key', ''); } protected function get_default_config() { return array( 'num_results' => 5, 'num_sources_to_fetch' => 3, 'query_patterns' => array( '"{topic}" expert says', '"{topic}" CEO interview', '"{topic}" according to analyst' ) ); } public function execute($context) { $this->log_start($context); // Check for Serper API key if (empty($this->serper_api_key)) { $this->log_end($context, false); return $this->create_error('no_api_key', 'Serper API key not configured. Please add it in Settings.'); } // Get topic from context $topic = $this->get_from_context($context, 'topic', ''); if (empty($topic)) { $this->log_end($context, false); return $this->create_error('no_topic', 'No topic found in context'); } // Build search queries $queries = $this->build_expert_queries($topic); // Execute searches $all_results = array(); $num_results = $this->get_config_value('num_results', 5); foreach ($queries as $query) { $results = $this->search_web($query, $num_results); if (is_wp_error($results)) { continue; } // Merge results, avoiding duplicates foreach ($results as $key => $result) { if (!is_numeric($key)) { if (!isset($all_results[$key])) { $all_results[$key] = $result; } continue; } // Check for duplicate URLs $url = $result['url'] ?? ''; $is_duplicate = false; foreach ($all_results as $existing) { if (is_array($existing) && isset($existing['url']) && $existing['url'] === $url) { $is_duplicate = true; break; } } if (!$is_duplicate) { $all_results[] = $result; } } } if (empty($all_results)) { $this->set_in_context($context, 'expert_quotes_results', array()); $this->set_in_context($context, 'expert_quotes_brief', ''); $this->set_in_context($context, 'expert_quotes_skipped', true); $this->log_end($context, true); return $context; } // Store raw search results $this->set_in_context($context, 'expert_quotes_results', $all_results); // Fetch content from top results $num_sources = $this->get_config_value('num_sources_to_fetch', 3); $fetched_content = $this->fetch_top_results($all_results, $num_sources); // Create expert quotes brief $brief = $this->create_expert_quotes_brief($fetched_content, $all_results, $topic); $this->set_in_context($context, 'expert_quotes_brief', $brief); // Append to existing research_brief if present $existing_brief = $this->get_from_context($context, 'research_brief', ''); if (!empty($existing_brief)) { $combined = $existing_brief . "\n\n" . $brief; $this->set_in_context($context, 'research_brief', $combined); } else { $this->set_in_context($context, 'research_brief', $brief); } $this->set_in_context($context, 'expert_quotes_skipped', false); $this->log_end($context, true); return $context; } /** * Build expert-focused search queries */ private function build_expert_queries($topic) { $queries = array(); $patterns = $this->get_config_value('query_patterns', array( '"{topic}" expert says', '"{topic}" CEO interview', '"{topic}" according to analyst' )); // Extract core topic (shorter version for better search) $core_topic = $this->extract_core_topic($topic); foreach ($patterns as $pattern) { $query = str_replace('{topic}', $core_topic, $pattern); $queries[] = $query; } return $queries; } /** * Extract core topic for search queries */ private function extract_core_topic($topic) { if (strlen($topic) <= 60) { return $topic; } $words = str_word_count($topic, 1); $keywords = array_slice($words, 0, 6); return implode(' ', $keywords); } /** * Search the web using Serper API */ private function search_web($query, $num_results = 5) { $response = wp_remote_post('https://google.serper.dev/search', array( 'headers' => array( 'X-API-KEY' => $this->serper_api_key, 'Content-Type' => 'application/json' ), 'body' => json_encode(array( 'q' => $query, 'num' => min($num_results * 2, 10) )), 'timeout' => 30 )); if (is_wp_error($response)) { return $response; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { return new WP_Error('serper_error', 'Serper API error: HTTP ' . $status_code); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return new WP_Error('json_error', 'Failed to parse Serper response'); } // Extract and filter results - prioritize expert/authority sources $results = array(); // Domains to skip $skip_domains = array( 'pinterest.com', 'youtube.com', 'facebook.com', 'twitter.com', 'instagram.com', 'tiktok.com', 'reddit.com', 'quora.com' ); // Priority domains for expert opinions $priority_domains = array( 'hbr.org', // Harvard Business Review 'forbes.com', // Forbes 'bloomberg.com', // Bloomberg 'wsj.com', // Wall Street Journal 'nytimes.com', // New York Times 'reuters.com', // Reuters 'bbc.com', // BBC 'cnbc.com', // CNBC 'techcrunch.com', // TechCrunch 'wired.com', // Wired 'inc.com', // Inc Magazine 'entrepreneur.com', // Entrepreneur 'fastcompany.com', // Fast Company 'theguardian.com' // The Guardian ); $priority_results = array(); $other_results = array(); if (!empty($data['organic'])) { foreach ($data['organic'] as $item) { $url = $item['link'] ?? ''; if (empty($url)) continue; // Parse domain $parsed = wp_parse_url($url); $domain = $parsed['host'] ?? ''; $domain = preg_replace('/^www\./', '', $domain); // Skip unwanted domains $skip = false; foreach ($skip_domains as $skip_domain) { if (strpos($domain, $skip_domain) !== false) { $skip = true; break; } } if ($skip) continue; $result = array( 'title' => $item['title'] ?? '', 'url' => $url, 'snippet' => $item['snippet'] ?? '', 'position' => $item['position'] ?? 0 ); // Check if priority domain $is_priority = false; foreach ($priority_domains as $priority) { if (strpos($domain, $priority) !== false) { $is_priority = true; break; } } if ($is_priority) { $priority_results[] = $result; } else { $other_results[] = $result; } } } // Combine priority first, then others $results = array_merge($priority_results, $other_results); $results = array_slice($results, 0, $num_results); // Re-index $indexed_results = array_values($results); // Include answer box if present if (!empty($data['answerBox'])) { $indexed_results['answer_box'] = array( 'title' => $data['answerBox']['title'] ?? '', 'answer' => $data['answerBox']['answer'] ?? $data['answerBox']['snippet'] ?? '' ); } // Include knowledge graph if present if (!empty($data['knowledgeGraph'])) { $indexed_results['knowledge_graph'] = array( 'title' => $data['knowledgeGraph']['title'] ?? '', 'description' => $data['knowledgeGraph']['description'] ?? '' ); } return $indexed_results; } /** * Fetch content from top search results */ private function fetch_top_results($search_results, $num_sources) { $fetched = array( 'sources' => array(), 'content' => array(), 'errors' => array() ); $fetched_count = 0; foreach ($search_results as $key => $result) { if (!is_numeric($key)) continue; if ($fetched_count >= $num_sources) break; if (empty($result['url'])) continue; $content = $this->fetch_url_content($result['url']); if (is_wp_error($content)) { $fetched['errors'][] = $result['url'] . ': ' . $content->get_error_message(); continue; } $fetched['sources'][] = array( 'title' => $result['title'], 'url' => $result['url'] ); $fetched['content'][] = array( 'source' => $result['title'], 'url' => $result['url'], 'content' => $content ); $fetched_count++; } return $fetched; } /** * Fetch and extract content from a URL */ private function fetch_url_content($url) { $response = wp_remote_get($url, array( 'timeout' => 15, 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' )); if (is_wp_error($response)) { return $response; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { return new WP_Error('fetch_error', 'HTTP ' . $status_code); } $html = wp_remote_retrieve_body($response); if (empty($html)) { return new WP_Error('empty_response', 'Empty response'); } // Extract text content $text = $this->extract_text_from_html($html); if (strlen($text) < 100) { return new WP_Error('insufficient_content', 'Content too short'); } // Truncate if too long $max_length = 8000; if (strlen($text) > $max_length) { $text = substr($text, 0, $max_length); $last_period = strrpos($text, '. '); if ($last_period !== false && $last_period > ($max_length * 0.8)) { $text = substr($text, 0, $last_period + 1); } $text .= "\n\n[Content truncated...]"; } return $text; } /** * Extract readable text from HTML */ private function extract_text_from_html($html) { // Remove unwanted elements $html = preg_replace('/]*>(.*?)<\/script>/is', '', $html); $html = preg_replace('/]*>(.*?)<\/style>/is', '', $html); $html = preg_replace('/]*>(.*?)<\/nav>/is', '', $html); $html = preg_replace('/]*>(.*?)<\/footer>/is', '', $html); $html = preg_replace('/]*>(.*?)<\/header>/is', '', $html); $html = preg_replace('/]*>(.*?)<\/aside>/is', '', $html); $html = preg_replace('//s', '', $html); // Try to find main content $extracted = ''; if (preg_match('/]*>(.*?)<\/article>/is', $html, $matches)) { $extracted = $matches[1]; } elseif (preg_match('/]*>(.*?)<\/main>/is', $html, $matches)) { $extracted = $matches[1]; } elseif (preg_match_all('/]*>(.{50,}?)<\/p>/is', $html, $matches)) { $extracted = implode("\n\n", $matches[0]); } else { $extracted = $html; } // Convert to text $extracted = preg_replace('/]*>(.*?)<\/h[1-6]>/is', "\n\n### $1 ###\n\n", $extracted); $extracted = preg_replace('/]*>(.*?)<\/li>/is', "• $1\n", $extracted); $extracted = preg_replace('//i', "\n", $extracted); $extracted = preg_replace('/<\/p>/i', "\n\n", $extracted); $text = wp_strip_all_tags($extracted); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/[ \t]+/', ' ', $text); $text = preg_replace('/\n\s*\n\s*\n+/', "\n\n", $text); return trim($text); } /** * Create an expert quotes brief from search results */ private function create_expert_quotes_brief($fetched_content, $search_results, $topic) { $brief = "=== EXPERT OPINIONS & QUOTES RESEARCH ===\n"; $brief .= "Topic: {$topic}\n\n"; // Add answer box if present if (!empty($search_results['answer_box'])) { $brief .= "### Key Insight ###\n"; $brief .= $search_results['answer_box']['answer'] . "\n\n"; } // Extract actual quotes from fetched content $extracted_quotes = array(); if (!empty($fetched_content['content'])) { foreach ($fetched_content['content'] as $item) { $quotes = $this->extract_quotes_from_text($item['content'], $item['source']); $extracted_quotes = array_merge($extracted_quotes, $quotes); } } // Add extracted quotes section if (!empty($extracted_quotes)) { $brief .= "### EXTRACTED EXPERT QUOTES (USE THESE IN YOUR ARTICLE) ###\n\n"; foreach ($extracted_quotes as $idx => $quote) { $num = $idx + 1; $brief .= "QUOTE {$num}:\n"; $brief .= "\"{$quote['quote']}\"\n"; if (!empty($quote['speaker'])) { $brief .= "— {$quote['speaker']}\n"; } $brief .= "Source: {$quote['source']}\n\n"; } } // Add search snippets focusing on quotes $brief .= "### Additional Expert Opinions Found ###\n"; foreach ($search_results as $key => $result) { if (!is_numeric($key)) continue; $snippet = $result['snippet'] ?? ''; $title = $result['title'] ?? ''; if (!empty($snippet)) { $brief .= "• {$title}: {$snippet}\n"; } } $brief .= "\n"; // Add sources list if (!empty($fetched_content['sources'])) { $brief .= "### Source URLs ###\n"; foreach ($fetched_content['sources'] as $source) { $brief .= "• {$source['title']}: {$source['url']}\n"; } $brief .= "\n"; } $brief .= "=== END EXPERT QUOTES RESEARCH ==="; return $brief; } /** * Extract quotes from text content */ private function extract_quotes_from_text($text, $source_name) { $quotes = array(); // Pattern 1: Text in quotation marks with speaker attribution // Matches: "quote" said/says Name or Name said/says "quote" $patterns = array( // "Quote," said Name '/"([^"]{20,300})"[,.]?\s*(?:said|says|stated|explained|noted|added|argued|suggested|believes|claimed|according to)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3})/i', // Name said "quote" '/([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3})\s+(?:said|says|stated|explained|noted|added|argued|suggested|believes|claimed)[,:]?\s*"([^"]{20,300})"/i', // According to Name, "quote" '/(?:according to|as)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3})[,:]?\s*"([^"]{20,300})"/i', ); foreach ($patterns as $pattern) { if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { // Determine which group is the quote vs the speaker if (strpos($pattern, '"([^"]{20,300})"') === 1) { // Quote comes first $quote = trim($match[1]); $speaker = trim($match[2] ?? ''); } else { // Speaker comes first $speaker = trim($match[1]); $quote = trim($match[2] ?? ''); } // Clean up and validate if (strlen($quote) >= 20 && strlen($quote) <= 500) { $quotes[] = array( 'quote' => $quote, 'speaker' => $speaker, 'source' => $source_name ); } } } } // Pattern 2: Standalone quotes in quotation marks (no clear speaker) if (count($quotes) < 3) { if (preg_match_all('/"([^"]{30,250})"/', $text, $matches)) { foreach ($matches[1] as $quote) { $quote = trim($quote); // Check it's not already captured and looks like a real quote $already_captured = false; foreach ($quotes as $existing) { if ($existing['quote'] === $quote) { $already_captured = true; break; } } if (!$already_captured && strlen($quote) >= 30 && strlen($quote) <= 300) { // Try to find speaker near this quote $speaker = $this->find_speaker_near_quote($text, $quote); $quotes[] = array( 'quote' => $quote, 'speaker' => $speaker, 'source' => $source_name ); } if (count($quotes) >= 5) break; } } } // Limit to best 5 quotes per source return array_slice($quotes, 0, 5); } /** * Try to find a speaker name near a quote in the text */ private function find_speaker_near_quote($text, $quote) { $quote_pos = strpos($text, $quote); if ($quote_pos === false) return ''; // Look in the 200 chars before and after the quote $start = max(0, $quote_pos - 200); $end = min(strlen($text), $quote_pos + strlen($quote) + 200); $context = substr($text, $start, $end - $start); // Look for Name patterns with titles $title_patterns = array( '/(?:CEO|CFO|CTO|COO|President|Director|Manager|Professor|Dr\.|Chief|Founder|Chairman|Analyst|Expert|Researcher)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})/i', '/([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2}),?\s+(?:CEO|CFO|CTO|COO|President|Director|Manager|Professor|Chief|Founder|Chairman|Analyst|Expert|Researcher)/i', ); foreach ($title_patterns as $pattern) { if (preg_match($pattern, $context, $match)) { return trim($match[1]); } } return ''; } public function get_config_schema() { return array( 'num_results' => array( 'type' => 'number', 'label' => 'Results per Query', 'description' => 'How many search results to retrieve per query', 'min' => 1, 'max' => 10, 'default' => 5 ), 'num_sources_to_fetch' => array( 'type' => 'number', 'label' => 'Sources to Fetch', 'description' => 'How many pages to fetch full content from', 'min' => 1, 'max' => 5, 'default' => 3 ) ); } }