openai_client = $openai_client ?: new AIGH_OpenAI_Client(); $this->serper_api_key = get_option('aigh_serper_api_key', ''); } protected function get_default_config() { return array( 'num_results' => 5, 'num_sources_to_fetch' => 3, 'search_query' => '', // Optional custom search query ); } 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', ''); // Use custom search query if provided, otherwise use topic $search_query = $this->get_config_value('search_query', ''); if (empty($search_query)) { $search_query = $this->extract_search_query($topic); } // Perform web search $num_results = $this->get_config_value('num_results', 5); $search_results = $this->search_web($search_query, $num_results); if (is_wp_error($search_results)) { $this->log_end($context, false); return $search_results; } if (empty($search_results)) { $this->set_in_context($context, 'web_search_results', array()); $this->set_in_context($context, 'web_search_brief', ''); $this->set_in_context($context, 'web_search_skipped', true); $this->log_end($context, true); return $context; } // Store raw search results $this->set_in_context($context, 'web_search_results', $search_results); // Fetch content from top results $num_sources = $this->get_config_value('num_sources_to_fetch', 3); $fetched_content = $this->fetch_top_results($search_results, $num_sources); // Create research brief from fetched content $brief = $this->create_search_brief($fetched_content, $search_results, $topic); $this->set_in_context($context, 'web_search_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, 'web_search_skipped', false); $this->log_end($context, true); return $context; } /** * Extract search-friendly query from topic */ private function extract_search_query($topic) { // If topic is short enough, use as-is if (strlen($topic) <= 100) { return $topic; } // Extract key phrases for longer topics $words = str_word_count($topic, 1); $keywords = array_slice($words, 0, 10); 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) // Request more to filter bad domains )), '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 $results = array(); $skip_domains = array( 'youtube.com', 'youtu.be', 'vimeo.com', 'dailymotion.com', 'nytimes.com', 'wsj.com', 'ft.com', 'bloomberg.com', 'economist.com', 'reddit.com', 'twitter.com', 'x.com', 'facebook.com', 'instagram.com', 'pinterest.com', 'tiktok.com', 'linkedin.com' ); if (!empty($data['organic'])) { foreach ($data['organic'] as $item) { $url = $item['link'] ?? ''; $domain = strtolower(wp_parse_url($url, PHP_URL_HOST)); // Skip blocked domains $should_skip = false; foreach ($skip_domains as $skip) { if (strpos($domain, $skip) !== false) { $should_skip = true; break; } } if ($should_skip) continue; $results[] = array( 'title' => $item['title'] ?? '', 'url' => $url, 'snippet' => $item['snippet'] ?? '', 'position' => $item['position'] ?? count($results) + 1 ); if (count($results) >= $num_results) break; } } // Include answer box if present if (!empty($data['answerBox'])) { $results['answer_box'] = array( 'title' => $data['answerBox']['title'] ?? '', 'answer' => $data['answerBox']['answer'] ?? $data['answerBox']['snippet'] ?? '' ); } // Include knowledge graph if present if (!empty($data['knowledgeGraph'])) { $results['knowledge_graph'] = array( 'title' => $data['knowledgeGraph']['title'] ?? '', 'description' => $data['knowledgeGraph']['description'] ?? '' ); } return $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 a research brief from search results */ private function create_search_brief($fetched_content, $search_results, $topic) { $brief = "=== WEB SEARCH RESULTS ===\n"; $brief .= "Topic: {$topic}\n\n"; // Add answer box if present if (!empty($search_results['answer_box'])) { $brief .= "### Quick Answer ###\n"; $brief .= $search_results['answer_box']['answer'] . "\n\n"; } // Add knowledge graph if present if (!empty($search_results['knowledge_graph'])) { $brief .= "### Overview ###\n"; $brief .= $search_results['knowledge_graph']['description'] . "\n\n"; } // Add search snippets $brief .= "### Search Snippets ###\n"; foreach ($search_results as $key => $result) { if (!is_numeric($key)) continue; $brief .= "• {$result['title']}: {$result['snippet']}\n"; } $brief .= "\n"; // Add fetched content if (!empty($fetched_content['content'])) { $brief .= "### Fetched Content ###\n\n"; foreach ($fetched_content['content'] as $idx => $item) { $num = $idx + 1; $brief .= "--- Source {$num}: {$item['source']} ---\n"; $brief .= "URL: {$item['url']}\n\n"; $brief .= $item['content'] . "\n\n"; } } $brief .= "=== END WEB SEARCH RESULTS ==="; return $brief; } public function get_config_schema() { return array( 'search_query' => array( 'type' => 'text', 'label' => 'Custom Search Query', 'description' => 'Leave empty to use the topic as search query', 'placeholder' => 'Optional custom search query' ), 'num_results' => array( 'type' => 'number', 'label' => 'Number of Results', 'description' => 'How many search results to retrieve', '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 ) ); } }