Files
wp-agentic-writer/includes/class-keyword-suggester.php
2026-01-28 00:26:00 +07:00

157 lines
5.1 KiB
PHP

<?php
/**
* Keyword Suggester Helper
*
* @package WP_Agentic_Writer
* @since 0.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class WP_Agentic_Writer_Keyword_Suggester
*
* Helper class for AI-powered keyword suggestions.
*
* @since 0.1.0
*/
class WP_Agentic_Writer_Keyword_Suggester {
/**
* Suggest keywords based on outline and title.
*
* @since 0.1.0
* @param string $title Article title.
* @param array $sections Outline sections.
* @param string $language Language code.
* @param int $post_id Post ID for cost tracking.
* @return array|WP_Error Array with focus_keyword, secondary_keywords, reasoning, and cost.
*/
public static function suggest_keywords( $title, $sections, $language = 'english', $post_id = 0 ) {
$provider = WP_Agentic_Writer_OpenRouter_Provider::get_instance();
// Build outline text from sections
$outline_text = '';
if ( is_array( $sections ) ) {
foreach ( $sections as $section ) {
$section_title = $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 separate operation type
$cost = $response['cost'] ?? 0;
if ( $cost > 0 && $post_id > 0 ) {
do_action(
'wp_aw_after_api_request',
$post_id,
$response['model'] ?? 'unknown',
'suggest_keyword',
$response['input_tokens'] ?? 0,
$response['output_tokens'] ?? 0,
$cost
);
}
return array(
'focus_keyword' => $suggestions['focus_keyword'],
'secondary_keywords' => $suggestions['secondary_keywords'],
'reasoning' => $suggestions['reasoning'] ?? '',
'cost' => $cost,
);
}
/**
* 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.";
}
}