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 ); } }