log('Constructor called'); $this->openai_client = $openai_client; $this->serper_api_key = get_option('aigh_serper_api_key', ''); $this->log('Serper API key: ' . ($this->serper_api_key ? 'SET (' . strlen($this->serper_api_key) . ' chars)' : 'NOT SET')); } /** * Search the web using Serper API * * @param string $query Search query * @param int $num_results Number of results to fetch (max 10) * @return array|WP_Error Search results or error */ public function search_web($query, $num_results = 5) { $this->log('========== search_web() START =========='); $this->log('Query: ' . $query); $this->log('Num results: ' . $num_results); if (empty($this->serper_api_key)) { $this->log('ERROR: No Serper API key configured'); return new WP_Error('no_api_key', 'Serper API key not configured. Please add it in Settings.'); } $this->log('Making Serper API request...'); $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, 10) )), 'timeout' => 30 )); if (is_wp_error($response)) { $this->log('Serper API ERROR: ' . $response->get_error_message()); return $response; } $status_code = wp_remote_retrieve_response_code($response); $this->log('Serper API status: ' . $status_code); if ($status_code !== 200) { $body = wp_remote_retrieve_body($response); $this->log('Serper API error body: ' . $body); 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) { $this->log('JSON decode error: ' . json_last_error_msg()); return new WP_Error('json_error', 'Failed to parse Serper response'); } $this->log('Serper response parsed successfully'); // Extract organic results $results = array(); if (!empty($data['organic'])) { $this->log('Found ' . count($data['organic']) . ' organic results'); foreach ($data['organic'] as $idx => $item) { $results[] = array( 'title' => $item['title'] ?? '', 'url' => $item['link'] ?? '', 'snippet' => $item['snippet'] ?? '', 'position' => $item['position'] ?? ($idx + 1) ); $this->log('Result ' . ($idx + 1) . ': ' . ($item['title'] ?? 'No title') . ' - ' . ($item['link'] ?? 'No URL')); } } // Also check for knowledge graph / answer box if (!empty($data['answerBox'])) { $this->log('Found answer box'); $results['answer_box'] = array( 'title' => $data['answerBox']['title'] ?? '', 'answer' => $data['answerBox']['answer'] ?? $data['answerBox']['snippet'] ?? '' ); } if (!empty($data['knowledgeGraph'])) { $this->log('Found knowledge graph'); $results['knowledge_graph'] = array( 'title' => $data['knowledgeGraph']['title'] ?? '', 'description' => $data['knowledgeGraph']['description'] ?? '' ); } $this->log('========== search_web() END =========='); return $results; } /** * Research a topic using web search * Searches the web, fetches top results, creates research brief * * @param string $topic Topic to research * @param int $num_sources Number of sources to fetch content from * @return array|WP_Error Research data or error */ public function research_topic($topic, $num_sources = 3) { $this->log('========== research_topic() START =========='); $this->log('Topic: ' . $topic); $this->log('Num sources to fetch: ' . $num_sources); // FIX #3: Extract keywords from long prompts $search_query = $this->extract_search_query($topic); $this->log('Search query: ' . $search_query); // FIX #4: Request more results to compensate for skipped/failed URLs $search_results = $this->search_web($search_query, 10); if (is_wp_error($search_results)) { $this->log('Search failed: ' . $search_results->get_error_message()); return $search_results; } if (empty($search_results)) { $this->log('No search results found'); return new WP_Error('no_results', 'No search results found for: ' . $topic); } // FIX #1 & #2: Skip useless/blocked domains $skip_domains = array( 'youtube.com', 'youtu.be', 'vimeo.com', 'dailymotion.com', // Video sites 'nytimes.com', 'wsj.com', 'ft.com', 'bloomberg.com', 'economist.com', // Paywalled 'reddit.com', 'twitter.com', 'x.com', 'facebook.com', 'instagram.com', // Social 'pinterest.com', 'tiktok.com', 'linkedin.com' // More social ); $urls_to_fetch = array(); foreach ($search_results as $key => $result) { if (!is_numeric($key)) continue; if (empty($result['url'])) continue; $domain = strtolower(wp_parse_url($result['url'], PHP_URL_HOST)); $should_skip = false; foreach ($skip_domains as $skip) { if (strpos($domain, $skip) !== false) { $this->log('Skipping blocked domain: ' . $domain); $should_skip = true; break; } } if ($should_skip) continue; $urls_to_fetch[] = $result['url']; if (count($urls_to_fetch) >= $num_sources + 2) break; // Get extras in case some fail } $this->log('URLs to fetch: ' . count($urls_to_fetch)); // Build research data $research_data = array( 'sources' => array(), 'raw_content' => array(), 'errors' => array(), 'search_results' => $search_results ); // Collect snippets as backup $snippets_text = "=== SEARCH RESULT SNIPPETS ===\n\n"; foreach ($search_results as $key => $result) { if (!is_numeric($key)) continue; if (!empty($result['snippet'])) { $snippets_text .= "• " . $result['title'] . ": " . $result['snippet'] . "\n\n"; } } $this->log('Snippets text length: ' . strlen($snippets_text)); // Fetch URLs until we have enough sources $fetched_count = 0; foreach ($urls_to_fetch as $url) { if ($fetched_count >= $num_sources) break; $this->log('Fetching: ' . $url); $content = $this->fetch_url_content($url); if (is_wp_error($content)) { $this->log('Fetch failed: ' . $content->get_error_message()); $research_data['errors'][] = $url . ': ' . $content->get_error_message(); continue; } // Skip if content too short (probably blocked or useless) if (strlen($content) < 500) { $this->log('Content too short, skipping: ' . strlen($content) . ' chars'); $research_data['errors'][] = $url . ': Content too short'; continue; } $this->log('Fetch success: ' . strlen($content) . ' chars'); $title = wp_parse_url($url, PHP_URL_HOST); $title = preg_replace('/^www\./', '', $title); $research_data['sources'][] = array( 'title' => $title, 'url' => $url ); $research_data['raw_content'][] = array( 'source' => $title, 'url' => $url, 'content' => $content ); $fetched_count++; } // If no full content, use snippets if (empty($research_data['raw_content'])) { $this->log('No full content fetched, using snippets only'); $research_data['raw_content'][] = array( 'source' => 'Search Results', 'url' => 'multiple', 'content' => $snippets_text ); $research_data['sources'][] = array( 'title' => 'Google Search Results', 'url' => 'https://google.com/search?q=' . urlencode($search_query) ); } else { // Prepend snippets to first source $research_data['raw_content'][0]['content'] = $snippets_text . "\n\n=== FULL ARTICLE CONTENT ===\n\n" . $research_data['raw_content'][0]['content']; } $this->log('Final sources count: ' . count($research_data['sources'])); $this->log('Final errors count: ' . count($research_data['errors'])); $this->log('========== research_topic() END =========='); return $research_data; } /** * Extract search-friendly query from user prompt * FIX #3: Long prompts make bad search queries */ private function extract_search_query($topic) { // If already short enough, use as-is if (strlen($topic) <= 80) { return $topic; } // Remove common filler phrases $fillers = array( 'I need an article about', 'I need an article giving', 'I want an article about', 'Write an article about', 'Write me an article about', 'Create an article about', 'Please write about', 'Can you write about', 'I would like', 'giving a deep insight into', 'deep insight into', 'detailed analysis of', 'comprehensive guide to', 'listing main', 'including', 'and also', ); $query = $topic; foreach ($fillers as $filler) { $query = str_ireplace($filler, ' ', $query); } // Clean up whitespace $query = preg_replace('/\s+/', ' ', $query); $query = trim($query); // If still too long, take first 100 chars at word boundary if (strlen($query) > 100) { $query = substr($query, 0, 100); $last_space = strrpos($query, ' '); if ($last_space > 50) { $query = substr($query, 0, $last_space); } } $this->log('Original topic length: ' . strlen($topic)); $this->log('Extracted query length: ' . strlen($query)); return $query; } /** * Full web research pipeline: search -> fetch -> brief * * @param string $topic Topic to research * @return array Array with 'brief' and 'sources_count' or WP_Error */ public function full_web_research($topic) { $this->log('========== full_web_research() START =========='); $this->log('Topic: ' . $topic); // Research the topic $research_data = $this->research_topic($topic, 3); if (is_wp_error($research_data)) { return $research_data; } // Create brief $brief = $this->create_research_brief($research_data, $topic); if (empty($brief)) { return new WP_Error('no_brief', 'Failed to create research brief'); } $this->log('========== full_web_research() END =========='); return array( 'brief' => $brief, 'sources_count' => count($research_data['sources']), 'sources' => $research_data['sources'], 'errors' => $research_data['errors'] ); } /** * Fetch full content from a URL */ public function fetch_url_content($url) { $this->log('========== fetch_url_content() START =========='); $this->log('URL: ' . $url); $this->log('Making HTTP request with 30s timeout...'); $response = wp_remote_get($url, array( 'timeout' => 30, 'headers' => array( 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language' => 'en-US,en;q=0.5', ), 'redirection' => 5, 'sslverify' => false )); if (is_wp_error($response)) { $this->log('HTTP ERROR: ' . $response->get_error_message()); return new WP_Error('fetch_failed', 'Could not fetch URL: ' . $response->get_error_message()); } $status_code = wp_remote_retrieve_response_code($response); $this->log('HTTP Status: ' . $status_code); if ($status_code !== 200) { $this->log('Bad HTTP status - returning error'); return new WP_Error('bad_status', "HTTP Error {$status_code}"); } $html = wp_remote_retrieve_body($response); $this->log('Raw HTML received: ' . strlen($html) . ' chars'); if (empty($html)) { $this->log('Empty HTML body'); return new WP_Error('empty_content', 'URL returned empty content'); } // Clean HTML $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('/]*>(.*?)<\/noscript>/is', '', $html); $html = preg_replace('//s', '', $html); // Try to extract main content $extracted = ''; if (preg_match('/]*>(.*?)<\/article>/is', $html, $matches)) { $extracted = $matches[1]; $this->log('Found
: ' . strlen($extracted) . ' chars'); } if (empty($extracted) && preg_match('/]*>(.*?)<\/main>/is', $html, $matches)) { $extracted = $matches[1]; $this->log('Found
: ' . strlen($extracted) . ' chars'); } if (empty($extracted)) { $content_patterns = array( '/]+class=["\'][^"\']*\b(post-content|entry-content|article-content|article-body|post-body|content-body|main-content|page-content|story-body|story-content)[^"\']*["\'][^>]*>(.*?)<\/div>/is', ); foreach ($content_patterns as $pattern) { if (preg_match($pattern, $html, $matches)) { $extracted = $matches[2] ?? $matches[1]; $this->log('Pattern matched: ' . strlen($extracted) . ' chars'); break; } } } if (empty($extracted) && preg_match_all('/]*>(.{50,}?)<\/p>/is', $html, $matches)) { $extracted = implode("\n\n", $matches[0]); $this->log('Found ' . count($matches[0]) . ' paragraphs'); } if (empty($extracted)) { $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); $extracted = preg_replace('/<\/div>/i', "\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); $text = trim($text); // Limit to 8000 characters per source $max_length = 8000; if (strlen($text) > $max_length) { $text = substr($text, 0, $max_length); $lastPeriod = strrpos($text, '. '); if ($lastPeriod !== false && $lastPeriod > ($max_length * 0.8)) { $text = substr($text, 0, $lastPeriod + 1); } $text .= "\n\n[Content truncated...]"; } $this->log('Final text length: ' . strlen($text) . ' chars'); if (strlen($text) < 100) { $this->log('Content too short'); return new WP_Error('insufficient_content', 'Content too short'); } $this->log('========== fetch_url_content() END =========='); return $text; } /** * Research from user-provided URLs */ public function research_from_urls($urls_string) { $this->log('========== research_from_urls() START =========='); $this->log('Input: ' . $urls_string); // Parse URLs $urls = preg_split('/[\r\n,]+/', $urls_string); $urls = array_map('trim', $urls); $urls = array_filter($urls); if (empty($urls)) { return new WP_Error('no_urls', 'No URLs provided'); } $urls = array_slice($urls, 0, 5); $this->log('Processing ' . count($urls) . ' URLs'); $research_data = array( 'sources' => array(), 'raw_content' => array(), 'errors' => array() ); foreach ($urls as $url) { if (!filter_var($url, FILTER_VALIDATE_URL)) { $research_data['errors'][] = "Invalid URL: {$url}"; continue; } $content = $this->fetch_url_content($url); if (is_wp_error($content)) { $research_data['errors'][] = $url . ': ' . $content->get_error_message(); continue; } $title = wp_parse_url($url, PHP_URL_HOST); $title = preg_replace('/^www\./', '', $title); $research_data['sources'][] = array( 'title' => $title, 'url' => $url ); $research_data['raw_content'][] = array( 'source' => $title, 'url' => $url, 'content' => $content ); } if (empty($research_data['raw_content'])) { return new WP_Error('no_content', 'Could not fetch any URLs. Errors: ' . implode('; ', $research_data['errors'])); } $this->log('========== research_from_urls() END =========='); return $research_data; } /** * Create research brief from research data */ public function create_research_brief($research_data, $topic = '') { $this->log('========== create_research_brief() START =========='); if (empty($research_data['raw_content'])) { $this->log('No content to create brief from'); return ''; } $system_prompt = 'You are a research analyst. Extract EVERY specific fact, statistic, name, date, and example from the sources. Be exhaustive.'; $user_prompt = ''; if (!empty($topic)) { $user_prompt .= "Topic: {$topic}\n\n"; } $user_prompt .= "Extract ALL specific information from " . count($research_data['raw_content']) . " source(s):\n\n"; foreach ($research_data['raw_content'] as $index => $item) { $num = $index + 1; $user_prompt .= "=== SOURCE {$num}: {$item['source']} ===\n"; $user_prompt .= $item['content']; $user_prompt .= "\n\n"; } $user_prompt .= "=== EXTRACTION TASK ===\n\n"; $user_prompt .= "List EVERYTHING specific:\n"; $user_prompt .= "1. STATISTICS & NUMBERS (percentages, amounts, dates)\n"; $user_prompt .= "2. NAMES (people, companies, products)\n"; $user_prompt .= "3. QUOTES & STATEMENTS\n"; $user_prompt .= "4. EVENTS & DATES\n"; $user_prompt .= "5. EXAMPLES & CASE STUDIES\n\n"; $user_prompt .= "Be EXHAUSTIVE. Missing details = generic article.\n\n"; $user_prompt .= "Begin:"; $this->log('Calling OpenAI API...'); $brief = $this->openai_client->generate_text($system_prompt, $user_prompt, 0.2, 4000); if (is_wp_error($brief)) { $this->log('OpenAI ERROR: ' . $brief->get_error_message()); return ''; } $this->log('Brief generated: ' . strlen($brief) . ' chars'); $formatted = "=== RESEARCH BRIEF ===\n"; $formatted .= "Sources: " . count($research_data['sources']) . "\n\n"; foreach ($research_data['sources'] as $index => $source) { $formatted .= "[" . ($index + 1) . "] {$source['title']} - {$source['url']}\n"; } $formatted .= "\n=== EXTRACTED INFORMATION ===\n\n"; $formatted .= $brief; $formatted .= "\n\n=== END RESEARCH BRIEF ==="; $this->log('========== create_research_brief() END =========='); return $formatted; } }