157 lines
5.1 KiB
PHP
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.";
|
|
}
|
|
}
|