Files
wp-agentic-writer/includes/class-wp-ai-client-wrapper.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
);
}
}