Skip to content
4 changes: 2 additions & 2 deletions includes/Abilities/Title_Generation/Title_Generation.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use WordPress\AI_Client\AI_Client;

use function WordPress\AI\get_post_context;
use function WordPress\AI\get_preferred_models;
use function WordPress\AI\get_preferred_models_for_text_generation;
use function WordPress\AI\normalize_content;

/**
Expand Down Expand Up @@ -267,7 +267,7 @@ static function ( $key, $value ) {
->using_system_instruction( $this->get_system_instruction() )
->using_temperature( 0.7 )
->using_candidate_count( (int) $candidates )
->using_model_preference( ...get_preferred_models() )
->using_model_preference( ...get_preferred_models_for_text_generation() )
->generate_texts();
}
}
201 changes: 201 additions & 0 deletions includes/Services/AI_Service.php
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially had something similar in place when building out the Title Generation Experiment before getting feedback that it made things over-abstracted (see #67 (review)). So may be worth revisiting that discussion to ensure we're all on the same page cc/ @JasonTheAdams

That said, overall I'm fine with this approach but seems we just add the AI_Service class in this PR but we're not actually using it anywhere. Do we want to update how the Title Generation Experiment is working to take advantage of this new approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wary of overabstraction here too, which is why the PR has been notably reduced in scope from what it was originally.

I think the way it currently is is fine. Although it mostly feels like syntactic sugar and a slightly alternative way to use the fluent API from the WP AI Client prompt builder.

Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php
/**
* AI Service implementation.
*
* Provides a centralized service layer for AI operations.
*
* @package WordPress\AI\Services
*/

declare( strict_types=1 );

namespace WordPress\AI\Services;

use WordPress\AI_Client\AI_Client;
use WordPress\AI_Client\Builders\Prompt_Builder_With_WP_Error;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;

use function WordPress\AI\get_preferred_models_for_text_generation;

/**
* AI Service class.
*
* Manages AI provider configuration and provides a consistent interface
* for experimental features to communicate with AI providers.
*
* @since x.x.x
*/
class AI_Service {

/**
* Singleton instance.
*
* @since x.x.x
*
* @var \WordPress\AI\Services\AI_Service|null
*/
private static ?self $instance = null;

/**
* Whether the service has been initialized.
*
* @since x.x.x
*
* @var bool
*/
private bool $initialized = false;

/**
* Option key mapping from WordPress snake_case to SDK camelCase.
*
* @since x.x.x
*
* @var array<string, string>
*/
private static array $option_key_map = array(
'system_instruction' => 'systemInstruction',
'candidate_count' => 'candidateCount',
'max_tokens' => 'maxTokens',
'temperature' => 'temperature',
'top_p' => 'topP',
'top_k' => 'topK',
'stop_sequences' => 'stopSequences',
'presence_penalty' => 'presencePenalty',
'frequency_penalty' => 'frequencyPenalty',
'logprobs' => 'logprobs',
'top_logprobs' => 'topLogprobs',
);

/**
* Gets the singleton instance.
*
* @since x.x.x
*
* @return \WordPress\AI\Services\AI_Service The singleton instance.
*/
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

/**
* Private constructor to enforce singleton pattern.
*
* @since x.x.x
*/
private function __construct() {}

/**
* Initializes the AI service.
*
* This method should be called after AI_Client::init() on the WordPress 'init' hook.
*
* @since x.x.x
*/
public function init(): void {
if ( $this->initialized ) {
return;
}

$this->initialized = true;

/**
* Fires when the AI service is initialized.
*
* @since x.x.x
*
* @param \WordPress\AI\Services\AI_Service $service The AI service instance.
*/
do_action( 'ai_experiments_service_initialized', $this );
}

/**
* Creates a text generation prompt builder with default configuration applied.
*
* This is the primary method for text generation with AI providers. It returns
* a configured prompt builder that consumers can use with the full SDK API.
*
* Example usage:
* ```php
* $service = AI_Service::get_instance();
*
* // Simple usage
* $text = $service->create_textgen_prompt( 'Summarize this text' )->generate_text();
*
* // With options
* $text = $service->create_textgen_prompt( 'Translate to French', array(
* 'system_instruction' => 'You are a translator.',
* 'temperature' => 0.3,
* 'max_tokens' => 500,
* ) )->generate_text();
*
* // Generate multiple candidates
* $titles = $service->create_textgen_prompt( 'Generate titles', array(
* 'candidate_count' => 5,
* 'temperature' => 0.8,
* ) )->generate_texts();
* ```
*
* @since x.x.x
*
* @param string|null $prompt Optional. Initial prompt content.
* @param array<string, mixed> $options Optional. Configuration options. {
* @type string $system_instruction System instruction for the AI.
* @type float $temperature Temperature for generation (0.0-2.0).
* @type int $max_tokens Maximum tokens to generate.
* @type float $top_p Top-p (nucleus) sampling value.
* @type int $top_k Top-k sampling value.
* @type int $candidate_count Number of candidates to generate.
* @type float $presence_penalty Presence penalty for generation.
* @type float $frequency_penalty Frequency penalty for generation.
* @type list<string> $stop_sequences Stop sequences for generation.
* @type bool $logprobs Whether to return log probabilities.
* @type int $top_logprobs Top log probabilities to return.
* }
* @return \WordPress\AI_Client\Builders\Prompt_Builder_With_WP_Error The prompt builder instance.
*/
public function create_textgen_prompt( ?string $prompt = null, array $options = array() ): Prompt_Builder_With_WP_Error {
$builder = AI_Client::prompt_with_wp_error( $prompt );

// Apply default model preferences.
$models = get_preferred_models_for_text_generation();
if ( ! empty( $models ) ) {
$builder = $builder->using_model_preference( ...$models );
}

// Apply options via ModelConfig if any are provided.
if ( ! empty( $options ) ) {
$config_array = $this->map_options_to_config( $options );
if ( ! empty( $config_array ) ) {
$config = ModelConfig::fromArray( $config_array );
$builder = $builder->using_model_config( $config );
}
}

return $builder;
}

/**
* Maps WordPress snake_case options to SDK camelCase config array.
*
* @since x.x.x
*
* @param array<string, mixed> $options The options array with snake_case keys.
* @return array<string, mixed> The mapped config array with camelCase keys.
*/
private function map_options_to_config( array $options ): array {
$config = array();

foreach ( self::$option_key_map as $wp_key => $sdk_key ) {
if ( ! array_key_exists( $wp_key, $options ) ) {
continue;
}

$config[ $sdk_key ] = $options[ $wp_key ];
}

return $config;
}
}
8 changes: 8 additions & 0 deletions includes/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace WordPress\AI;

use WordPress\AI\Abilities\Utilities\Posts;
use WordPress\AI\Services\AI_Service;
use WordPress\AI\Settings\Settings_Page;
use WordPress\AI\Settings\Settings_Registration;
use WordPress\AI_Client\AI_Client;
Expand Down Expand Up @@ -221,6 +222,13 @@ function initialize_experiments(): void {
$settings_page->init();
}

// Initialize the WP AI Client.
AI_Client::init();

// Initialize the AI Service layer.
$ai_service = AI_Service::get_instance();
$ai_service->init();

// Register our post-related WordPress Abilities.
$post_abilities = new Posts();
$post_abilities->register();
Expand Down
50 changes: 44 additions & 6 deletions includes/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace WordPress\AI;

use Throwable;
use WordPress\AI\Services\AI_Service;
use WordPress\AI_Client\AI_Client;

/**
Expand Down Expand Up @@ -119,13 +120,13 @@ function get_post_context( int $post_id ): array {
}

/**
* Returns the preferred models.
* Returns the preferred models for text generation.
*
* @since 0.1.0
*
* @return array<int, array{string, string}> The preferred models.
* @return array<int, array{string, string}> The preferred models for text generation.
*/
function get_preferred_models(): array {
function get_preferred_models_for_text_generation(): array {
$preferred_models = array(
array(
'anthropic',
Expand All @@ -146,14 +147,51 @@ function get_preferred_models(): array {
);

/**
* Filters the preferred models.
* Filters the preferred models for text generation.
*
* @since 0.1.0
* @hook ai_experiments_preferred_models_for_text_generation
*
* @param array<int, array{string, string}> $preferred_models The preferred models.
* @param array<int, array{string, string}> $preferred_models The preferred models for text generation.
* @return array<int, array{string, string}> The filtered preferred models.
*/
return (array) apply_filters( 'ai_experiments_preferred_models', $preferred_models );
return (array) apply_filters( 'ai_experiments_preferred_models_for_text_generation', $preferred_models );
}

/**
* Gets the AI Service instance.
*
* Provides a convenient way to access the AI Service for performing AI operations.
*
* Example usage:
* ```php
* $service = WordPress\AI\get_ai_service();
*
* // Check if text generation is supported before generating
* $builder = $service->create_textgen_prompt( 'Summarize this article...' );
* if ( ! $builder->is_supported_for_text_generation() ) {
* return new WP_Error( 'ai_unsupported', 'No AI provider supports text generation.' );
* }
* $text = $builder->generate_text();
*
* // With options array
* $text = $service->create_textgen_prompt( 'Translate to French: Hello', array(
* 'system_instruction' => 'You are a translator.',
* 'temperature' => 0.3,
* ) )->generate_text();
*
* // Chain additional SDK methods
* $titles = $service->create_textgen_prompt( 'Generate titles for: My blog post' )
* ->using_candidate_count( 5 )
* ->generate_texts();
* ```
*
* @since x.x.x
*
* @return \WordPress\AI\Services\AI_Service The AI Service instance.
*/
function get_ai_service(): AI_Service {
return AI_Service::get_instance();
}

/**
Expand Down
Loading
Loading