588 lines
16 KiB
PHP
588 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* WordPress AI Client Integration
|
|
*
|
|
* Provides backward-compatible AI functionality that leverages WordPress 7.0's
|
|
* native AI Client SDK when available, with fallback to legacy implementation.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Check if WordPress AI Client SDK is available
|
|
*
|
|
* @return bool True if wp_ai_client_prompt() function exists.
|
|
*/
|
|
function wpaw_is_wp_ai_client_available() {
|
|
return function_exists( 'wp_ai_client_prompt' );
|
|
}
|
|
|
|
/**
|
|
* Check if WordPress AI Client supports text generation
|
|
*
|
|
* @return bool True if text generation is supported.
|
|
*/
|
|
function wpaw_wp_ai_supports_text() {
|
|
if ( ! wpaw_is_wp_ai_client_available() ) {
|
|
return false;
|
|
}
|
|
|
|
$builder = wp_ai_client_prompt( 'test' );
|
|
return $builder->is_supported_for_text_generation();
|
|
}
|
|
|
|
/**
|
|
* Check if WordPress AI Client supports image generation
|
|
*
|
|
* @return bool True if image generation is supported.
|
|
*/
|
|
function wpaw_wp_ai_supports_images() {
|
|
if ( ! wpaw_is_wp_ai_client_available() ) {
|
|
return false;
|
|
}
|
|
|
|
$builder = wp_ai_client_prompt( 'test' );
|
|
return $builder->is_supported_for_image_generation();
|
|
}
|
|
|
|
/**
|
|
* Check if WordPress AI Client supports speech generation
|
|
*
|
|
* @return bool True if speech generation is supported.
|
|
*/
|
|
function wpaw_wp_ai_supports_speech() {
|
|
if ( ! wpaw_is_wp_ai_client_available() ) {
|
|
return false;
|
|
}
|
|
|
|
$builder = wp_ai_client_prompt( 'test' );
|
|
return $builder->is_supported_for_speech_generation();
|
|
}
|
|
|
|
/**
|
|
* WPAW_WP_AI_Client class
|
|
*
|
|
* Provides unified AI interface with WordPress 7.0 integration.
|
|
* Falls back to legacy providers when core AI is unavailable.
|
|
*/
|
|
class WPAW_WP_AI_Client {
|
|
|
|
/**
|
|
* Singleton instance
|
|
*
|
|
* @var WPAW_WP_AI_Client
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Whether WordPress AI Client is available
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $core_available;
|
|
|
|
/**
|
|
* Model preferences for different tasks
|
|
*
|
|
* @var array
|
|
*/
|
|
private $model_preferences = array(
|
|
'chat' => array( 'claude-sonnet-4-20250514', 'gpt-4o', 'gemini-2.5-flash' ),
|
|
'clarity' => array( 'claude-haiku-4-20250514', 'gpt-4o-mini', 'gemini-2.0-flash' ),
|
|
'planning' => array( 'claude-sonnet-4-20250514', 'gpt-4o', 'gemini-2.5-flash' ),
|
|
'writing' => array( 'claude-sonnet-4-20250514', 'gpt-4o', 'gemini-2.5-flash' ),
|
|
'refinement' => array( 'claude-haiku-4-20250514', 'gpt-4o-mini', 'gemini-2.0-flash' ),
|
|
'seo' => array( 'claude-sonnet-4-20250514', 'gpt-4o', 'gemini-2.5-flash' ),
|
|
'title' => array( 'claude-haiku-4-20250514', 'gpt-4o-mini', 'gemini-2.0-flash' ),
|
|
);
|
|
|
|
/**
|
|
* Temperature settings for different tasks
|
|
*
|
|
* @var array
|
|
*/
|
|
private $temperature_settings = array(
|
|
'chat' => 0.7,
|
|
'clarity' => 0.3,
|
|
'planning' => 0.6,
|
|
'writing' => 0.7,
|
|
'refinement' => 0.5,
|
|
'seo' => 0.5,
|
|
'title' => 0.5,
|
|
);
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*
|
|
* @return WPAW_WP_AI_Client
|
|
*/
|
|
public static function get_instance() {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->core_available = wpaw_is_wp_ai_client_available();
|
|
}
|
|
|
|
/**
|
|
* Check if using WordPress AI Client (true) or legacy (false)
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function using_core() {
|
|
return $this->core_available;
|
|
}
|
|
|
|
/**
|
|
* Get available AI mode
|
|
*
|
|
* @return string 'core', 'openrouter', or 'local'
|
|
*/
|
|
public function get_ai_mode() {
|
|
if ( $this->core_available && wpaw_wp_ai_supports_text() ) {
|
|
return 'core';
|
|
}
|
|
|
|
$settings = get_option( 'wp_agentic_writer_settings', array() );
|
|
$provider = $settings['default_provider'] ?? 'openrouter';
|
|
|
|
if ( $provider === 'local_backend' && class_exists( 'WP_Agentic_Writer_Local_Backend_Provider' ) ) {
|
|
$local = new WP_Agentic_Writer_Local_Backend_Provider();
|
|
if ( $local->is_configured() ) {
|
|
return 'local';
|
|
}
|
|
}
|
|
|
|
return 'openrouter';
|
|
}
|
|
|
|
/**
|
|
* Generate text using WordPress AI Client or fallback
|
|
*
|
|
* @param string $prompt The prompt text.
|
|
* @param array $options Additional options (task_type, temperature, max_tokens).
|
|
* @return string|WP_Error Generated text or error.
|
|
*/
|
|
public function generate_text( $prompt, $options = array() ) {
|
|
$task_type = $options['task_type'] ?? 'chat';
|
|
$temperature = $options['temperature'] ?? ( $this->temperature_settings[ $task_type ] ?? 0.7 );
|
|
$max_tokens = $options['max_tokens'] ?? 4096;
|
|
|
|
// Try WordPress AI Client first
|
|
if ( $this->core_available && wpaw_wp_ai_supports_text() ) {
|
|
$models = $this->model_preferences[ $task_type ] ?? $this->model_preferences['chat'];
|
|
|
|
$builder = wp_ai_client_prompt()
|
|
->with_text( $prompt )
|
|
->using_temperature( $temperature )
|
|
->using_max_tokens( $max_tokens )
|
|
->using_model_preference( ...$models );
|
|
|
|
$result = $builder->generate_text();
|
|
|
|
if ( ! is_wp_error( $result ) ) {
|
|
// Track usage if cost tracker available
|
|
if ( class_exists( 'WP_Agentic_Writer_Cost_Tracker' ) ) {
|
|
$cost = $this->estimate_cost( $result->get_usage() ?? array(), $models[0] );
|
|
WP_Agentic_Writer_Cost_Tracker::get_instance()->record_usage_full(
|
|
$options['post_id'] ?? 0,
|
|
$models[0], // actual model used
|
|
$task_type,
|
|
$result->get_usage()['input_tokens'] ?? 0,
|
|
$result->get_usage()['output_tokens'] ?? 0,
|
|
$cost,
|
|
'core', // WP AI Client provider
|
|
$options['session_id'] ?? '',
|
|
'success'
|
|
);
|
|
}
|
|
|
|
return $result->get_text();
|
|
}
|
|
|
|
error_log( 'WP Agentic Writer: Core AI failed, falling back to legacy. Error: ' . $result->get_error_message() );
|
|
}
|
|
|
|
// Fallback to legacy implementation
|
|
return $this->generate_text_legacy( $prompt, $options );
|
|
}
|
|
|
|
/**
|
|
* Generate text using legacy provider
|
|
*
|
|
* @param string $prompt The prompt text.
|
|
* @param array $options Additional options.
|
|
* @return string|WP_Error Generated text or error.
|
|
*/
|
|
public function generate_text_legacy( $prompt, $options = array() ) {
|
|
$task_type = $options['task_type'] ?? 'chat';
|
|
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( $task_type );
|
|
$provider = $provider_result->provider;
|
|
|
|
$messages = array(
|
|
array(
|
|
'role' => 'user',
|
|
'content' => $prompt,
|
|
),
|
|
);
|
|
|
|
$params = array(
|
|
'temperature' => $options['temperature'] ?? ( $this->temperature_settings[ $task_type ] ?? 0.7 ),
|
|
'max_tokens' => $options['max_tokens'] ?? 4096,
|
|
);
|
|
|
|
$response = $provider->chat( $messages, $params, $task_type );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
// Track usage
|
|
if ( class_exists( 'WP_Agentic_Writer_Cost_Tracker' ) ) {
|
|
$cost = $response['cost'] ?? 0;
|
|
WP_Agentic_Writer_Cost_Tracker::get_instance()->record_usage_full(
|
|
$options['post_id'] ?? 0,
|
|
$provider_result->selected_provider . '/' . ($response['model'] ?? 'unknown'),
|
|
$task_type,
|
|
$response['input_tokens'] ?? 0,
|
|
$response['output_tokens'] ?? 0,
|
|
$cost,
|
|
$provider_result->actual_provider,
|
|
$options['session_id'] ?? '',
|
|
'success'
|
|
);
|
|
}
|
|
|
|
return $response['content'] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Generate text with streaming callback
|
|
*
|
|
* @param string $prompt The prompt text.
|
|
* @param callable $callback Callback function for each chunk.
|
|
* @param array $options Additional options.
|
|
* @return bool True on success.
|
|
*/
|
|
public function generate_text_streaming( $prompt, $callback, $options = array() ) {
|
|
$task_type = $options['task_type'] ?? 'chat';
|
|
|
|
// Note: WordPress AI Client doesn't support streaming yet
|
|
// Use legacy provider for streaming
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task( $task_type );
|
|
$provider = $provider_result->provider;
|
|
|
|
if ( method_exists( $provider, 'chat_stream' ) ) {
|
|
$params = array(
|
|
'temperature' => $options['temperature'] ?? ( $this->temperature_settings[ $task_type ] ?? 0.7 ),
|
|
'max_tokens' => $options['max_tokens'] ?? 8192,
|
|
);
|
|
|
|
$result = $provider->chat_stream(
|
|
array(
|
|
array(
|
|
'role' => 'user',
|
|
'content' => $prompt,
|
|
),
|
|
),
|
|
$params,
|
|
$task_type,
|
|
$callback
|
|
);
|
|
|
|
return ! is_wp_error( $result );
|
|
}
|
|
|
|
// Fallback to non-streaming
|
|
$result = $this->generate_text_legacy( $prompt, $options );
|
|
if ( is_wp_error( $result ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Call back with full result
|
|
call_user_func( $callback, $result );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate image using WordPress AI Client or fallback
|
|
*
|
|
* @param string $prompt The image prompt.
|
|
* @param array $options Additional options (size, style).
|
|
* @return array|WP_Error Image data or error.
|
|
*/
|
|
public function generate_image( $prompt, $options = array() ) {
|
|
$size = $options['size'] ?? '1024x1024';
|
|
$style = $options['style'] ?? 'natural';
|
|
|
|
// Try WordPress AI Client first
|
|
if ( $this->core_available && wpaw_wp_ai_supports_images() ) {
|
|
$builder = wp_ai_client_prompt()
|
|
->with_text( $prompt )
|
|
->as_output_modality_image();
|
|
|
|
$result = $builder->generate_image();
|
|
|
|
if ( ! is_wp_error( $result ) ) {
|
|
return array(
|
|
'url' => $result->get_url(),
|
|
'data_uri' => $result->get_data_uri(),
|
|
'revised_prompt' => method_exists( $result, 'get_revised_prompt' ) ? $result->get_revised_prompt() : $prompt,
|
|
);
|
|
}
|
|
|
|
error_log( 'WP Agentic Writer: Core image generation failed: ' . $result->get_error_message() );
|
|
}
|
|
|
|
// Fallback to legacy image manager
|
|
if ( class_exists( 'WP_Agentic_Writer_Image_Manager' ) ) {
|
|
$manager = WP_Agentic_Writer_Image_Manager::get_instance();
|
|
return $manager->generate_image( $prompt, $options );
|
|
}
|
|
|
|
return new WP_Error(
|
|
'image_generation_unavailable',
|
|
__( 'Image generation is not available. Configure AI provider in WordPress Settings.', 'wp-agentic-writer' ),
|
|
array( 'status' => 400 )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate structured JSON response
|
|
*
|
|
* @param string $prompt The prompt.
|
|
* @param array $schema JSON schema for response.
|
|
* @param array $options Additional options.
|
|
* @return array|WP_Error Parsed JSON or error.
|
|
*/
|
|
public function generate_json( $prompt, $schema, $options = array() ) {
|
|
$task_type = $options['task_type'] ?? 'chat';
|
|
|
|
// Try WordPress AI Client first
|
|
if ( $this->core_available && wpaw_wp_ai_supports_text() ) {
|
|
$models = $this->model_preferences[ $task_type ] ?? $this->model_preferences['chat'];
|
|
|
|
$builder = wp_ai_client_prompt()
|
|
->with_text( $prompt )
|
|
->using_temperature( $options['temperature'] ?? 0.3 )
|
|
->as_json_response( $schema )
|
|
->using_model_preference( ...$models );
|
|
|
|
$result = $builder->generate_text();
|
|
|
|
if ( ! is_wp_error( $result ) ) {
|
|
$json = json_decode( $result->get_text(), true );
|
|
if ( json_last_error() === JSON_ERROR_NONE ) {
|
|
return $json;
|
|
}
|
|
error_log( 'WP Agentic Writer: JSON parse error: ' . json_last_error_msg() );
|
|
}
|
|
}
|
|
|
|
// Fallback to legacy with manual JSON extraction
|
|
$text = $this->generate_text_legacy( $prompt . "\n\nRespond with ONLY valid JSON, no additional text.", $options );
|
|
|
|
if ( is_wp_error( $text ) ) {
|
|
return $text;
|
|
}
|
|
|
|
// Try to extract JSON from response
|
|
$text = trim( $text );
|
|
|
|
// Remove code block markers if present
|
|
$text = preg_replace( '/^```(?:json)?\s*/i', '', $text );
|
|
$text = preg_replace( '/\s*```$/i', '', $text );
|
|
|
|
$result = json_decode( $text, true );
|
|
|
|
if ( json_last_error() === JSON_ERROR_NONE ) {
|
|
return $result;
|
|
}
|
|
|
|
return new WP_Error(
|
|
'json_parse_error',
|
|
__( 'Failed to parse JSON response', 'wp-agentic-writer' ),
|
|
array( 'status' => 500 )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Detect user intent from message
|
|
*
|
|
* @param string $message User message.
|
|
* @param bool $has_plan Whether user has an existing plan.
|
|
* @param string $mode Current agent mode.
|
|
* @return array Intent result with type and cost.
|
|
*/
|
|
public function detect_intent( $message, $has_plan = false, $mode = 'chat' ) {
|
|
$options = array(
|
|
'task_type' => 'clarity',
|
|
'max_tokens' => 50,
|
|
'temperature' => 0.1,
|
|
);
|
|
|
|
$prompt = "Based on the user's message, determine their intent. Choose ONE:
|
|
|
|
1. \"create_outline\" - User wants to create an article outline/structure
|
|
2. \"start_writing\" - User wants to write the full article
|
|
3. \"refine_content\" - User wants to improve existing content
|
|
4. \"add_section\" - User wants to add a new section
|
|
5. \"continue_chat\" - User wants to continue discussing/exploring
|
|
6. \"clarify\" - User is asking questions or needs clarification
|
|
|
|
Consider:
|
|
- The user's explicit request
|
|
- Whether they have an outline already (has_plan: " . ( $has_plan ? 'true' : 'false' ) . ")
|
|
- Current mode (current_mode: {$mode})
|
|
|
|
User's message: \"{$message}\"
|
|
|
|
Respond with ONLY the intent code (e.g., \"create_outline\"). No explanation.";
|
|
|
|
$result = $this->generate_text( $prompt, $options );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return array(
|
|
'intent' => 'continue_chat',
|
|
'cost' => 0,
|
|
'error' => $result->get_error_message(),
|
|
);
|
|
}
|
|
|
|
// Validate intent
|
|
$intent = trim( strtolower( $result ) );
|
|
$intent = preg_replace( '/["\'\\s]/', '', $intent );
|
|
|
|
$valid_intents = array( 'create_outline', 'start_writing', 'refine_content', 'add_section', 'continue_chat', 'clarify' );
|
|
|
|
if ( ! in_array( $intent, $valid_intents, true ) ) {
|
|
$intent = 'continue_chat';
|
|
}
|
|
|
|
return array(
|
|
'intent' => $intent,
|
|
'cost' => 0.001, // Estimated cost
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate title for content
|
|
*
|
|
* @param string $content Content to generate title for.
|
|
* @param array $options Additional options.
|
|
* @return string|WP_Error Generated title or error.
|
|
*/
|
|
public function generate_title( $content, $options = array() ) {
|
|
$options['task_type'] = 'title';
|
|
$options['max_tokens'] = 60;
|
|
|
|
$prompt = "Generate a catchy, SEO-friendly title (max 60 characters) for the following content. Only return the title, no additional text:\n\n" . substr( $content, 0, 1000 );
|
|
|
|
return $this->generate_text( $prompt, $options );
|
|
}
|
|
|
|
/**
|
|
* Generate excerpt for content
|
|
*
|
|
* @param string $content Content to generate excerpt for.
|
|
* @param array $options Additional options.
|
|
* @return string|WP_Error Generated excerpt or error.
|
|
*/
|
|
public function generate_excerpt( $content, $options = array() ) {
|
|
$options['task_type'] = 'title';
|
|
$options['max_tokens'] = 160;
|
|
|
|
$prompt = "Generate a compelling excerpt (max 160 characters) for the following content. Only return the excerpt, no additional text:\n\n" . substr( $content, 0, 2000 );
|
|
|
|
return $this->generate_text( $prompt, $options );
|
|
}
|
|
|
|
/**
|
|
* Summarize context for token optimization
|
|
*
|
|
* @param array $messages Chat messages to summarize.
|
|
* @param int $max_tokens Maximum tokens for summary.
|
|
* @return string|WP_Error Summary or error.
|
|
*/
|
|
public function summarize_context( $messages, $max_tokens = 1000 ) {
|
|
$options = array(
|
|
'task_type' => 'clarity',
|
|
'max_tokens' => $max_tokens,
|
|
'temperature' => 0.3,
|
|
);
|
|
|
|
// Build context string
|
|
$context = '';
|
|
foreach ( $messages as $msg ) {
|
|
$role = $msg['role'] ?? 'user';
|
|
$content = $msg['content'] ?? '';
|
|
if ( is_array( $content ) ) {
|
|
$content = $content[0]['text'] ?? '';
|
|
}
|
|
$context .= "[{$role}]: " . substr( $content, 0, 500 ) . "\n\n";
|
|
}
|
|
|
|
$prompt = "Summarize the following conversation, preserving key information and context. Focus on:\n- Topic and goal\n- Key decisions or plans made\n- Important details or constraints\n\nConversation:\n{$context}\n\nProvide a concise summary:";
|
|
|
|
return $this->generate_text( $prompt, $options );
|
|
}
|
|
|
|
/**
|
|
* Estimate cost based on usage
|
|
*
|
|
* @param array $usage Token usage data.
|
|
* @param string $model Model name.
|
|
* @return float Estimated cost in USD.
|
|
*/
|
|
private function estimate_cost( $usage, $model ) {
|
|
// Simple cost estimation
|
|
$input_tokens = $usage['input_tokens'] ?? 0;
|
|
$output_tokens = $usage['output_tokens'] ?? 0;
|
|
|
|
// Rough estimates per 1M tokens (in USD)
|
|
$rates = array(
|
|
'claude-sonnet' => 3.00,
|
|
'claude-haiku' => 0.25,
|
|
'gpt-4o' => 5.00,
|
|
'gpt-4o-mini' => 0.15,
|
|
'gemini' => 0.50,
|
|
);
|
|
|
|
$rate = 1.00; // Default rate
|
|
foreach ( $rates as $key => $value ) {
|
|
if ( stripos( $model, $key ) !== false ) {
|
|
$rate = $value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ( ( $input_tokens + $output_tokens ) / 1000000 ) * $rate;
|
|
}
|
|
|
|
/**
|
|
* Get capability status
|
|
*
|
|
* @return array Capabilities status.
|
|
*/
|
|
public function get_capabilities() {
|
|
return array(
|
|
'core_available' => $this->core_available,
|
|
'text_support' => wpaw_wp_ai_supports_text(),
|
|
'image_support' => wpaw_wp_ai_supports_images(),
|
|
'speech_support' => wpaw_wp_ai_supports_speech(),
|
|
'current_mode' => $this->get_ai_mode(),
|
|
'streaming_available' => false, // Core doesn't support streaming yet
|
|
);
|
|
}
|
|
} |