provider; // Build outline text from sections $outline_text = ''; if ( is_array( $sections ) ) { foreach ( $sections as $section ) { // Support both 'heading' (new format) and 'title' (legacy) $section_title = $section['heading'] ?? $section['title'] ?? ''; if ( ! empty( $section_title ) ) { $outline_text .= "- {$section_title}\n"; } } } if ( empty( $outline_text ) ) { return new WP_Error( 'no_outline', 'No outline available for keyword analysis' ); } // Build language-specific instruction $language_instruction = self::build_language_instruction( $language ); // Build prompt for keyword suggestion $prompt = "Analyze this article outline and suggest optimal SEO keywords.\n\n"; $prompt .= "ARTICLE TITLE:\n{$title}\n\n"; $prompt .= "OUTLINE SECTIONS:\n{$outline_text}\n\n"; $prompt .= "TASK:\n"; $prompt .= "1. Suggest ONE focus keyword (2-4 words) that best represents the main topic\n"; $prompt .= "2. Suggest 3-5 secondary keywords (related terms, variations, or supporting topics)\n"; $prompt .= "3. Provide brief reasoning (1-2 sentences) for your suggestions\n\n"; $prompt .= "REQUIREMENTS:\n"; $prompt .= "- Focus keyword should be specific and searchable\n"; $prompt .= "- Secondary keywords should complement the focus keyword\n"; $prompt .= "- Consider search intent and user queries\n"; $prompt .= "- Keywords should match the outline content\n\n"; $prompt .= "{$language_instruction}\n\n"; $prompt .= "Respond ONLY with valid JSON in this exact format:\n"; $prompt .= "{\n"; $prompt .= " \"focus_keyword\": \"your suggested focus keyword\",\n"; $prompt .= " \"secondary_keywords\": [\"keyword1\", \"keyword2\", \"keyword3\"],\n"; $prompt .= " \"reasoning\": \"Brief explanation of why these keywords are optimal\"\n"; $prompt .= "}\n\n"; $prompt .= "Do not include any text outside the JSON structure."; $messages = array( array( 'role' => 'user', 'content' => $prompt, ), ); // Use planning model for keyword suggestion (fast and cheap) $response = $provider->chat( $messages, array( 'temperature' => 0.3 ), 'planning' ); if ( is_wp_error( $response ) ) { return $response; } $content = trim( $response['content'] ?? '' ); // Try to extract JSON from response $json_start = strpos( $content, '{' ); $json_end = strrpos( $content, '}' ); if ( false === $json_start || false === $json_end ) { return new WP_Error( 'invalid_response', 'AI response is not valid JSON' ); } $json_string = substr( $content, $json_start, $json_end - $json_start + 1 ); $suggestions = json_decode( $json_string, true ); if ( null === $suggestions || ! is_array( $suggestions ) ) { return new WP_Error( 'parse_error', 'Failed to parse keyword suggestions' ); } // Validate required fields if ( empty( $suggestions['focus_keyword'] ) || empty( $suggestions['secondary_keywords'] ) ) { return new WP_Error( 'incomplete_suggestions', 'Keyword suggestions are incomplete' ); } // Ensure secondary_keywords is an array if ( ! is_array( $suggestions['secondary_keywords'] ) ) { $suggestions['secondary_keywords'] = array(); } // Track cost with full nine-argument contract including provider attribution. $cost = $response['cost'] ?? 0; if ( $cost > 0 && $post_id > 0 ) { $actual_provider = 'unknown'; $provider_name = ''; // Extract provider info from provider_result. if ( is_object( $provider_result ) && isset( $provider_result->actual_provider ) ) { $actual_provider = $provider_result->actual_provider; $provider_name = is_object( $provider ) ? get_class( $provider ) : 'unknown'; } // Get session ID for this post if available. $session_id = ''; if ( $post_id > 0 ) { $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); $session = $manager->get_session_by_post_id( $post_id ); if ( $session && isset( $session['session_id'] ) ) { $session_id = $session['session_id']; } } do_action( 'wp_aw_after_api_request', $post_id, $response['model'] ?? 'unknown', 'suggest_keyword', $response['input_tokens'] ?? 0, $response['output_tokens'] ?? 0, $cost, $actual_provider, $session_id, 'success' ); } return array( 'focus_keyword' => $suggestions['focus_keyword'], 'secondary_keywords' => $suggestions['secondary_keywords'], 'reasoning' => $suggestions['reasoning'] ?? '', 'cost' => $cost, 'provider_result' => $provider_result, 'model' => $response['model'] ?? '', ); } /** * Build language instruction for keyword suggestion. * * @since 0.1.0 * @param string $language Language code. * @return string Language instruction. */ private static function build_language_instruction( $language ) { $language = trim( (string) $language ); // If auto or empty, match article language if ( empty( $language ) || 'auto' === strtolower( $language ) ) { return 'Suggest keywords in the same language as the article topic and outline.'; } // Pass any language name directly - AI understands all languages return "IMPORTANT: Suggest keywords in {$language}. Consider {$language} search queries and terms used by {$language} speakers."; } }