Add PSR HTTP Message Interfaces and Dependencies
- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards. - Added getallheaders function to retrieve HTTP headers in a compatible manner. - Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts. - Introduced function for triggering deprecation notices in Symfony.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Deepl\Deepl_AI_API_Client
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Deepl;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Class to interact directly with the Deepl API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.7.0 Now extends `Generic_AI_API_Client`.
|
||||
*/
|
||||
class Deepl_AI_API_Client extends Generic_AI_API_Client {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $default_base_url The default base URL for the API.
|
||||
* @param string $default_api_version The default API version.
|
||||
* @param string $api_name The (human-readable) API name.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The authentication instance. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $default_base_url,
|
||||
string $default_api_version,
|
||||
string $api_name,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
// Set custom header name for Deepl API key authentication.
|
||||
if ( $authentication ) {
|
||||
$authentication->set_authencation_scheme( 'DeepL-Auth-Key' );
|
||||
}
|
||||
|
||||
$key_name=$authentication->get_option_definitions('deepl');
|
||||
|
||||
$key_value=get_option(array_keys($key_name)[0], '');
|
||||
|
||||
$key_free=str_ends_with($key_value, ':fx');
|
||||
|
||||
if($key_free){
|
||||
$default_base_url = 'https://api-free.deepl.com';
|
||||
}
|
||||
|
||||
parent::__construct( $default_base_url, $default_api_version, $api_name, $request_handler, $authentication );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URL for the specified model and task.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return string The request URL.
|
||||
*/
|
||||
protected function get_request_url( string $path, array $request_options = array() ): string {
|
||||
if ( isset( $request_options['stream'] ) && $request_options['stream'] && ! str_ends_with( $path, '?alt=sse' ) ) {
|
||||
$path .= '?alt=sse';
|
||||
}
|
||||
|
||||
return parent::get_request_url( $path, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional default request options to the given request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return array<string, mixed> The updated request options.
|
||||
*/
|
||||
protected function add_default_options( array $request_options ): array {
|
||||
$request_options = parent::add_default_options( $request_options );
|
||||
|
||||
return $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Deepl\Deepl_AI_Service
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Deepl;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Class for the Deepl AI service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.7.0 Now extends `Abstract_AI_Service`.
|
||||
*/
|
||||
class Deepl_AI_Service extends Abstract_AI_Service implements With_API_Client {
|
||||
use With_API_Client_Trait;
|
||||
|
||||
const DEFAULT_API_BASE_URL = 'https://api.deepl.com';
|
||||
const DEFAULT_API_VERSION = 'v2';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
* @param Authentication $authentication The authentication credentials.
|
||||
* @param Request_Handler $request_handler Optional. The request handler instance to use for requests. Default is a
|
||||
* new HTTP_With_Streams instance.
|
||||
*/
|
||||
public function __construct( Service_Metadata $metadata, Authentication $authentication, ?Request_Handler $request_handler = null ) {
|
||||
$this->set_service_metadata( $metadata );
|
||||
$this->set_api_client(
|
||||
new Deepl_AI_API_Client(
|
||||
self::DEFAULT_API_BASE_URL,
|
||||
self::DEFAULT_API_VERSION,
|
||||
'DeepL',
|
||||
$request_handler ?? new HTTP_With_Streams(),
|
||||
$authentication
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array {
|
||||
$api = $this->get_api_client();
|
||||
|
||||
$request = $api->create_get_request( 'usage', array(), $request_options );
|
||||
|
||||
$response_data = $api->make_request( $request )->get_data();
|
||||
|
||||
if ( ! isset( $response_data['character_count'] ) || ! $response_data['character_limit'] ) {
|
||||
throw $api->create_missing_response_key_exception( 'character_limit' );
|
||||
}
|
||||
|
||||
return array('languages'=>Model_Metadata::from_array(array('slug'=>'languages','name'=>'Languages','capabilities'=>array(AI_Capability::TEXT_GENERATION))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new model instance for the provided model metadata and parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $model_metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
|
||||
* a list of available parameters.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generative_AI_Model The new model instance.
|
||||
*/
|
||||
protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model {
|
||||
$model_class = AI_Capabilities::get_model_class_for_capabilities(
|
||||
array(
|
||||
Deepl_AI_Text_Generation_Model::class,
|
||||
),
|
||||
$model_metadata->get_capabilities()
|
||||
);
|
||||
|
||||
return new $model_class(
|
||||
$this->get_api_client(),
|
||||
$model_metadata,
|
||||
$model_params,
|
||||
$request_options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts model slugs by preference.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string[] $model_slugs The model slugs to sort.
|
||||
* @return string[] The model slugs, sorted by preference.
|
||||
*/
|
||||
protected function sort_models_by_preference( array $model_slugs ): array {
|
||||
return $model_slugs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Deepl\Deepl_AI_Text_Generation_Model
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Deepl;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidate;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\Transformer;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a Deepl text generation AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Renamed from `Deepl_AI_Model`.
|
||||
*/
|
||||
class Deepl_AI_Text_Generation_Model extends Abstract_AI_Model implements With_API_Client, With_Text_Generation, With_Function_Calling {
|
||||
use With_API_Client_Trait;
|
||||
use With_Text_Generation_Trait;
|
||||
use Model_Param_Text_Generation_Config_Trait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The AI API client instance.
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
|
||||
* {@see Deepl_AI_Service::get_model()} for the list of available
|
||||
* parameters. Default empty array.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
|
||||
$this->set_api_client( $api_client );
|
||||
$this->set_model_metadata( $metadata );
|
||||
|
||||
$this->set_text_generation_config_from_model_params( $model_params );
|
||||
|
||||
$this->set_request_options( $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
protected function send_generate_text_request( array $contents, array $request_options ): Candidates {
|
||||
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$request = $api->create_post_request(
|
||||
'translate',
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options
|
||||
)
|
||||
);
|
||||
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_data(
|
||||
$response,
|
||||
function ( $response_data ) {
|
||||
return $this->get_response_candidates( $response_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$model = $this->get_model_slug();
|
||||
if ( ! str_contains( $model, '/' ) ) {
|
||||
$model = 'models/' . $model;
|
||||
}
|
||||
|
||||
$request = $api->create_post_request(
|
||||
"{$model}:streamGenerateContent",
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options,
|
||||
array( 'stream' => true )
|
||||
)
|
||||
);
|
||||
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_stream(
|
||||
$response,
|
||||
function ( $response_data, $prev_chunk_candidates ) {
|
||||
return $this->get_response_candidates( $response_data, $prev_chunk_candidates );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*/
|
||||
private function prepare_generate_text_params( array $contents ): array {
|
||||
$transformers = $this->get_content_transformers();
|
||||
|
||||
$params = Transformer::transform_content( $contents[0], $transformers );
|
||||
|
||||
if(isset($params['parts'])){
|
||||
$params = json_decode($params['parts'][0]['text'], true);
|
||||
}
|
||||
|
||||
return array_filter( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the candidates with content from the response.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $response_data The response data.
|
||||
* @param ?Candidates $prev_chunk_candidates The candidates from the previous chunk in case of a streaming
|
||||
* response, or null.
|
||||
* @return Candidates The candidates with content parts.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response does not have any candidates with content.
|
||||
*/
|
||||
private function get_response_candidates( array $response_data, ?Candidates $prev_chunk_candidates = null ): Candidates {
|
||||
if ( ! isset( $response_data['translations'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'translations' );
|
||||
}
|
||||
|
||||
if ( null === $prev_chunk_candidates ) {
|
||||
$other_data = $response_data;
|
||||
unset( $other_data['translations'] );
|
||||
|
||||
$candidates = new Candidates();
|
||||
|
||||
$translate_strings=array();
|
||||
$translation_data=array();
|
||||
|
||||
foreach ( $response_data['translations'] as $index => $translation_data ) {
|
||||
$translate_strings[$index]=$translation_data['text'];
|
||||
|
||||
if(!isset($translation_data['confidence'])){
|
||||
$translation_data['confidence']=0;
|
||||
}
|
||||
|
||||
if(!isset($translation_data['detected_source_language'])){
|
||||
$translation_data['detected_source_language']='';
|
||||
}
|
||||
}
|
||||
|
||||
$translation_data['text']=json_encode($translate_strings, JSON_FORCE_OBJECT);
|
||||
|
||||
$candidates->add_candidate(
|
||||
new Candidate(
|
||||
$this->prepare_translation_content( $translation_data ),
|
||||
$other_data
|
||||
)
|
||||
);
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
// Subsequent chunk of a streaming response.
|
||||
$candidates_data = $this->merge_translation_chunk(
|
||||
$prev_chunk_candidates->to_array(),
|
||||
$response_data
|
||||
);
|
||||
|
||||
return Candidates::from_array( $candidates_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a streaming response chunk with the previous candidates data.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $candidates_data The candidates data from the previous chunk.
|
||||
* @param array<string, mixed> $chunk_data The response chunk data.
|
||||
* @return array<string, mixed> The merged candidates data.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function merge_translation_chunk( array $candidates_data, array $chunk_data ): array {
|
||||
if ( ! isset( $chunk_data['translations'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'translations' );
|
||||
}
|
||||
|
||||
$other_data = $chunk_data;
|
||||
unset( $other_data['translations'] );
|
||||
|
||||
foreach ( $chunk_data['translations'] as $index => $candidate_data ) {
|
||||
$candidates_data[ $index ] = array_merge( $candidates_data[ $index ], $candidate_data, $other_data );
|
||||
}
|
||||
|
||||
return $candidates_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given choice from the API response into a Content instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $choice_data The API response candidate data.
|
||||
* @param int $index The index of the choice in the response.
|
||||
* @return Content The Content instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function prepare_translation_content( array $translation_data ): Content {
|
||||
return new Content(
|
||||
Content_Role::MODEL,
|
||||
$this->prepare_translation_content_parts( $translation_data )
|
||||
);
|
||||
}
|
||||
|
||||
private function prepare_translation_content_parts( array $translation_data ): Parts {
|
||||
$parts[] = array(
|
||||
'text' => $translation_data['text'],
|
||||
'detected_source_language' => $translation_data['detected_source_language'] ?? '',
|
||||
'confidence' => $translation_data['confidence'] ?? 0,
|
||||
);
|
||||
|
||||
return Parts::from_array($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.7.0 Changed to non-static.
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
private function get_content_transformers(): array {
|
||||
return array(
|
||||
'role' => static function ( Content $content ) {
|
||||
return $content->get_role();
|
||||
},
|
||||
'parts' => static function ( Content $content ) {
|
||||
$parts = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Text_Part ) {
|
||||
$parts[] = array( 'text' => $part->get_text() );
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Google\Google_AI_API_Client
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Google;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Class to interact directly with the Google Generative Language API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.7.0 Now extends `Generic_AI_API_Client`.
|
||||
*/
|
||||
class Google_AI_API_Client extends Generic_AI_API_Client {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $default_base_url The default base URL for the API.
|
||||
* @param string $default_api_version The default API version.
|
||||
* @param string $api_name The (human-readable) API name.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The authentication instance. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $default_base_url,
|
||||
string $default_api_version,
|
||||
string $api_name,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
// Set custom header name for Google API key authentication.
|
||||
if ( $authentication ) {
|
||||
$authentication->set_header_name( 'X-Goog-Api-Key' );
|
||||
}
|
||||
|
||||
parent::__construct( $default_base_url, $default_api_version, $api_name, $request_handler, $authentication );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URL for the specified model and task.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return string The request URL.
|
||||
*/
|
||||
protected function get_request_url( string $path, array $request_options = array() ): string {
|
||||
if ( isset( $request_options['stream'] ) && $request_options['stream'] && ! str_ends_with( $path, '?alt=sse' ) ) {
|
||||
$path .= '?alt=sse';
|
||||
}
|
||||
|
||||
return parent::get_request_url( $path, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional default request options to the given request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return array<string, mixed> The updated request options.
|
||||
*/
|
||||
protected function add_default_options( array $request_options ): array {
|
||||
$request_options = parent::add_default_options( $request_options );
|
||||
|
||||
if ( ! isset( $request_options['headers'] ) ) {
|
||||
$request_options['headers'] = array();
|
||||
}
|
||||
$request_options['headers']['X-Goog-Api-Client'] = 'ai-services/' . ATFPP_V;
|
||||
|
||||
return $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Google\Google_AI_Service
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Google;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Class for the Google AI service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.7.0 Now extends `Abstract_AI_Service`.
|
||||
*/
|
||||
class Google_AI_Service extends Abstract_AI_Service implements With_API_Client {
|
||||
use With_API_Client_Trait;
|
||||
|
||||
const DEFAULT_API_BASE_URL = 'https://generativelanguage.googleapis.com';
|
||||
const DEFAULT_API_VERSION = 'v1beta';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
* @param Authentication $authentication The authentication credentials.
|
||||
* @param Request_Handler $request_handler Optional. The request handler instance to use for requests. Default is a
|
||||
* new HTTP_With_Streams instance.
|
||||
*/
|
||||
public function __construct( Service_Metadata $metadata, Authentication $authentication, ?Request_Handler $request_handler = null ) {
|
||||
$this->set_service_metadata( $metadata );
|
||||
$this->set_api_client(
|
||||
new Google_AI_API_Client(
|
||||
self::DEFAULT_API_BASE_URL,
|
||||
self::DEFAULT_API_VERSION,
|
||||
'Google Generative Language',
|
||||
$request_handler ?? new HTTP_With_Streams(),
|
||||
$authentication
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array {
|
||||
$api = $this->get_api_client();
|
||||
|
||||
$request = $api->create_get_request(
|
||||
'models',
|
||||
array(
|
||||
// 1000 is the maximum page size - we just want to retrieve all models in one go.
|
||||
'pageSize' => 1000,
|
||||
),
|
||||
$request_options
|
||||
);
|
||||
$response_data = $api->make_request( $request )->get_data();
|
||||
|
||||
if ( ! isset( $response_data['models'] ) || ! $response_data['models'] ) {
|
||||
throw $api->create_missing_response_key_exception( 'models' );
|
||||
}
|
||||
|
||||
$gemini_legacy_capabilities = array(
|
||||
AI_Capability::FUNCTION_CALLING,
|
||||
AI_Capability::TEXT_GENERATION,
|
||||
);
|
||||
$gemini_capabilities = array(
|
||||
AI_Capability::FUNCTION_CALLING,
|
||||
AI_Capability::MULTIMODAL_INPUT,
|
||||
AI_Capability::TEXT_GENERATION,
|
||||
);
|
||||
$imagen_capabilities = array(
|
||||
AI_Capability::IMAGE_GENERATION,
|
||||
);
|
||||
|
||||
return array_reduce(
|
||||
$response_data['models'],
|
||||
static function ( array $models_data, array $model_data ) use ( $gemini_legacy_capabilities, $gemini_capabilities, $imagen_capabilities ) {
|
||||
$model_slug = $model_data['baseModelId'] ?? $model_data['name'];
|
||||
if ( str_starts_with( $model_slug, 'models/' ) ) {
|
||||
$model_slug = substr( $model_slug, 7 );
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $model_data['supportedGenerationMethods'] ) &&
|
||||
in_array( 'generateContent', $model_data['supportedGenerationMethods'], true )
|
||||
) {
|
||||
if (
|
||||
str_starts_with( $model_slug, 'gemini-1.0' ) ||
|
||||
str_starts_with( $model_slug, 'gemini-pro' ) // 'gemini-pro' without version refers to 1.0.
|
||||
) {
|
||||
$model_caps = $gemini_legacy_capabilities;
|
||||
} else {
|
||||
$model_caps = $gemini_capabilities;
|
||||
|
||||
if ( // Web search is supported by Gemini 2.0 and newer.
|
||||
str_starts_with( $model_slug, 'gemini-' ) &&
|
||||
! str_starts_with( $model_slug, 'gemini-1.5-' )
|
||||
) {
|
||||
$model_caps[] = AI_Capability::WEB_SEARCH;
|
||||
}
|
||||
|
||||
if ( // New multimodal output model for image generation.
|
||||
str_contains( $model_slug, 'image-generation' ) ||
|
||||
str_starts_with( $model_slug, 'gemini-2.0-flash-exp' )
|
||||
) {
|
||||
$model_caps[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
} elseif ( // New multimodal output model for audio generation.
|
||||
str_contains( $model_slug, '-tts' )
|
||||
) {
|
||||
$model_caps[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
}
|
||||
} elseif (
|
||||
isset( $model_data['supportedGenerationMethods'] ) &&
|
||||
in_array( 'predict', $model_data['supportedGenerationMethods'], true )
|
||||
) {
|
||||
$model_caps = $imagen_capabilities;
|
||||
} else {
|
||||
$model_caps = array();
|
||||
}
|
||||
|
||||
$models_data[ $model_slug ] = Model_Metadata::from_array(
|
||||
array(
|
||||
'slug' => $model_slug,
|
||||
'name' => $model_data['displayName'] ?? $model_slug,
|
||||
'capabilities' => $model_caps,
|
||||
)
|
||||
);
|
||||
return $models_data;
|
||||
},
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new model instance for the provided model metadata and parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $model_metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
|
||||
* a list of available parameters.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generative_AI_Model The new model instance.
|
||||
*/
|
||||
protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model {
|
||||
$model_class = AI_Capabilities::get_model_class_for_capabilities(
|
||||
array(
|
||||
Google_AI_Text_Generation_Model::class,
|
||||
),
|
||||
$model_metadata->get_capabilities()
|
||||
);
|
||||
|
||||
return new $model_class(
|
||||
$this->get_api_client(),
|
||||
$model_metadata,
|
||||
$model_params,
|
||||
$request_options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts model slugs by preference.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string[] $model_slugs The model slugs to sort.
|
||||
* @return string[] The model slugs, sorted by preference.
|
||||
*/
|
||||
protected function sort_models_by_preference( array $model_slugs ): array {
|
||||
// Prioritize latest, non-experimental models, preferring cheaper ones.
|
||||
$get_preference_group = static function ( $model_slug ) {
|
||||
if ( str_starts_with( $model_slug, 'gemini-2.0' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-flash' ) ) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ( str_starts_with( $model_slug, 'gemini-' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-flash' ) ) {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
return 4;
|
||||
};
|
||||
|
||||
$preference_groups = array_fill( 0, 5, array() );
|
||||
foreach ( $model_slugs as $model_slug ) {
|
||||
$group = $get_preference_group( $model_slug );
|
||||
$preference_groups[ $group ][] = $model_slug;
|
||||
}
|
||||
|
||||
return array_merge( ...$preference_groups );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Google\Google_AI_Text_Generation_Model
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Google;
|
||||
|
||||
use ATFPP\AI_Translate\Google\Types\Safety_Setting;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Helpers;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidate;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Web_Search_Tool;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Web_Search;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_System_Instruction_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Tool_Config_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Tools_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\Transformer;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a Google text generation AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Renamed from `Google_AI_Model`.
|
||||
*/
|
||||
class Google_AI_Text_Generation_Model extends Abstract_AI_Model implements With_API_Client, With_Text_Generation, With_Function_Calling, With_Web_Search, With_Multimodal_Input, With_Multimodal_Output {
|
||||
use With_API_Client_Trait;
|
||||
use With_Text_Generation_Trait;
|
||||
use Model_Param_Text_Generation_Config_Trait;
|
||||
use Model_Param_Tool_Config_Trait;
|
||||
use Model_Param_Tools_Trait;
|
||||
use Model_Param_System_Instruction_Trait;
|
||||
|
||||
/**
|
||||
* The safety settings.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Safety_Setting[]
|
||||
*/
|
||||
protected $safety_settings;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The AI API client instance.
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
|
||||
* {@see Google_AI_Service::get_model()} for the list of available
|
||||
* parameters. Default empty array.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
|
||||
$this->set_api_client( $api_client );
|
||||
$this->set_model_metadata( $metadata );
|
||||
|
||||
$this->set_text_generation_config_from_model_params( $model_params );
|
||||
$this->set_tool_config_from_model_params( $model_params );
|
||||
$this->set_tools_from_model_params( $model_params );
|
||||
$this->set_system_instruction_from_model_params( $model_params );
|
||||
$this->set_safety_settings_from_model_params( $model_params );
|
||||
|
||||
$this->set_request_options( $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the safety settings if provided in the `safetySettings` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `safetySettings` model parameter is invalid.
|
||||
*/
|
||||
protected function set_safety_settings_from_model_params( array $model_params ): void {
|
||||
$this->safety_settings = array();
|
||||
if ( ! isset( $model_params['safetySettings'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $model_params['safetySettings'] as $safety_setting ) {
|
||||
if ( is_array( $safety_setting ) ) {
|
||||
$safety_setting = Safety_Setting::from_array( $safety_setting );
|
||||
}
|
||||
if ( ! $safety_setting instanceof Safety_Setting ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The safetySettings parameter must contain Safety_Setting instances.'
|
||||
);
|
||||
}
|
||||
$this->safety_settings[] = $safety_setting;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
protected function send_generate_text_request( array $contents, array $request_options ): Candidates {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$model = $this->get_model_slug();
|
||||
if ( ! str_contains( $model, '/' ) ) {
|
||||
$model = 'models/' . $model;
|
||||
}
|
||||
|
||||
$request = $api->create_post_request(
|
||||
"{$model}:generateContent",
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_data(
|
||||
$response,
|
||||
function ( $response_data ) {
|
||||
return $this->get_response_candidates( $response_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$model = $this->get_model_slug();
|
||||
if ( ! str_contains( $model, '/' ) ) {
|
||||
$model = 'models/' . $model;
|
||||
}
|
||||
|
||||
$request = $api->create_post_request(
|
||||
"{$model}:streamGenerateContent",
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options,
|
||||
array( 'stream' => true )
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_stream(
|
||||
$response,
|
||||
function ( $response_data, $prev_chunk_candidates ) {
|
||||
return $this->get_response_candidates( $response_data, $prev_chunk_candidates );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*/
|
||||
private function prepare_generate_text_params( array $contents ): array {
|
||||
$transformers = $this->get_content_transformers();
|
||||
|
||||
$params = array(
|
||||
'contents' => array_map(
|
||||
static function ( Content $content ) use ( $transformers ) {
|
||||
return Transformer::transform_content( $content, $transformers );
|
||||
},
|
||||
$contents
|
||||
),
|
||||
);
|
||||
|
||||
if ( $this->get_tools() ) {
|
||||
$params['tools'] = $this->prepare_tools_param( $this->get_tools() );
|
||||
}
|
||||
|
||||
if ( $this->get_tool_config() ) {
|
||||
$params['toolConfig'] = $this->prepare_tool_config_param( $this->get_tool_config() );
|
||||
}
|
||||
|
||||
$generation_config = $this->get_text_generation_config();
|
||||
if ( $generation_config ) {
|
||||
$params = array_merge( $generation_config->get_additional_args(), $params );
|
||||
$params['generationConfig'] = Transformer::transform_generation_config_params(
|
||||
isset( $params['generationConfig'] ) && is_array( $params['generationConfig'] ) ? $params['generationConfig'] : array(),
|
||||
$generation_config,
|
||||
$this->get_generation_config_transformers()
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->get_system_instruction() ) {
|
||||
$params['systemInstruction'] = $this->get_system_instruction()->to_array();
|
||||
}
|
||||
|
||||
if ( $this->safety_settings ) {
|
||||
$params['safetySettings'] = array_map(
|
||||
static function ( Safety_Setting $safety_setting ) {
|
||||
return $safety_setting->to_array();
|
||||
},
|
||||
$this->safety_settings
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the candidates with content from the response.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $response_data The response data.
|
||||
* @param ?Candidates $prev_chunk_candidates The candidates from the previous chunk in case of a streaming
|
||||
* response, or null.
|
||||
* @return Candidates The candidates with content parts.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response does not have any candidates with content.
|
||||
*/
|
||||
private function get_response_candidates( array $response_data, ?Candidates $prev_chunk_candidates = null ): Candidates {
|
||||
if ( ! isset( $response_data['candidates'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'candidates' );
|
||||
}
|
||||
|
||||
$this->check_non_empty_candidates( $response_data['candidates'] );
|
||||
|
||||
if ( null === $prev_chunk_candidates ) {
|
||||
$other_data = $response_data;
|
||||
unset( $other_data['candidates'] );
|
||||
|
||||
$candidates = new Candidates();
|
||||
foreach ( $response_data['candidates'] as $index => $candidate_data ) {
|
||||
$other_candidate_data = $candidate_data;
|
||||
unset( $other_candidate_data['content'] );
|
||||
|
||||
$candidates->add_candidate(
|
||||
new Candidate(
|
||||
$this->prepare_candidate_content( $candidate_data, $index ),
|
||||
array_merge( $other_candidate_data, $other_data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
// Subsequent chunk of a streaming response.
|
||||
$candidates_data = $this->merge_candidates_chunk(
|
||||
$prev_chunk_candidates->to_array(),
|
||||
$response_data
|
||||
);
|
||||
|
||||
return Candidates::from_array( $candidates_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a streaming response chunk with the previous candidates data.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $candidates_data The candidates data from the previous chunk.
|
||||
* @param array<string, mixed> $chunk_data The response chunk data.
|
||||
* @return array<string, mixed> The merged candidates data.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function merge_candidates_chunk( array $candidates_data, array $chunk_data ): array {
|
||||
if ( ! isset( $chunk_data['candidates'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'candidates' );
|
||||
}
|
||||
|
||||
$other_data = $chunk_data;
|
||||
unset( $other_data['candidates'] );
|
||||
|
||||
foreach ( $chunk_data['candidates'] as $index => $candidate_data ) {
|
||||
$candidates_data[ $index ] = array_merge( $candidates_data[ $index ], $candidate_data, $other_data );
|
||||
}
|
||||
|
||||
return $candidates_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Content instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @param int $index The index of the candidate in the response.
|
||||
* @return Content The Content instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function prepare_candidate_content( array $candidate_data, int $index ): Content {
|
||||
if ( ! isset( $candidate_data['content']['parts'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( "candidates.{$index}.content.parts" );
|
||||
}
|
||||
|
||||
// TODO: How to handle the 'audio/L16' MIME type output? Can it be converted to WAV in PHP?
|
||||
foreach ( $candidate_data['content']['parts'] as $index => $part ) {
|
||||
if ( isset( $part['inlineData']['mimeType'] ) && isset( $part['inlineData']['data'] ) ) {
|
||||
$candidate_data['content']['parts'][ $index ]['inlineData']['data'] = Helpers::base64_data_to_base64_data_url(
|
||||
$part['inlineData']['data'],
|
||||
$part['inlineData']['mimeType']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$role = isset( $candidate_data['content']['role'] ) && 'user' === $candidate_data['content']['role']
|
||||
? Content_Role::USER
|
||||
: Content_Role::MODEL;
|
||||
|
||||
return new Content(
|
||||
$role,
|
||||
Parts::from_array( $candidate_data['content']['parts'] )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the response includes candidates with content.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed>[] $candidates_data The candidates data from the response.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response does not include any candidates with content.
|
||||
*/
|
||||
private function check_non_empty_candidates( array $candidates_data ): void {
|
||||
$errors = array();
|
||||
foreach ( $candidates_data as $candidate_data ) {
|
||||
if ( ! isset( $candidate_data['content'] ) ) {
|
||||
if ( isset( $candidate_data['finishReason'] ) ) {
|
||||
$errors[] = $candidate_data['finishReason'];
|
||||
} else {
|
||||
$errors[] = 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $errors ) === count( $candidates_data ) ) {
|
||||
$message = 'The response does not include any candidates with content.';
|
||||
|
||||
$errors = array_unique(
|
||||
array_filter(
|
||||
$errors,
|
||||
static function ( $error ) {
|
||||
return 'unknown' !== $error;
|
||||
}
|
||||
)
|
||||
);
|
||||
if ( count( $errors ) > 0 ) {
|
||||
$message .= ' ' . sprintf(
|
||||
/* translators: %s: finish reason code */
|
||||
'Finish reason: %s',
|
||||
implode(
|
||||
', ',
|
||||
$errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->get_api_client()->create_response_exception( $message );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.7.0 Changed to non-static.
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
private function get_content_transformers(): array {
|
||||
return array(
|
||||
'role' => static function ( Content $content ) {
|
||||
return $content->get_role();
|
||||
},
|
||||
'parts' => static function ( Content $content ) {
|
||||
$parts = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Text_Part ) {
|
||||
$parts[] = array( 'text' => $part->get_text() );
|
||||
} elseif ( $part instanceof Function_Call_Part ) {
|
||||
$parts[] = array(
|
||||
'functionCall' => array(
|
||||
'name' => $part->get_name(),
|
||||
'args' => $part->get_args(),
|
||||
),
|
||||
);
|
||||
} elseif ( $part instanceof Function_Response_Part ) {
|
||||
$parts[] = array(
|
||||
'functionResponse' => array(
|
||||
'name' => $part->get_name(),
|
||||
|
||||
/*
|
||||
* The Google AI API requires function responses to be objects.
|
||||
* See also https://ai.google.dev/gemini-api/docs/function-calling#multi-turn-example-1
|
||||
*/
|
||||
'response' => array(
|
||||
'name' => $part->get_name(),
|
||||
'content' => $part->get_response(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw new Generative_AI_Exception(
|
||||
'The Google AI API only supports text, image, audio, function call, and function response parts.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generation configuration transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.7.0 Changed to non-static.
|
||||
*
|
||||
* @return array<string, callable> The generation configuration transformers.
|
||||
*/
|
||||
private function get_generation_config_transformers(): array {
|
||||
return array(
|
||||
'stopSequences' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_stop_sequences();
|
||||
},
|
||||
'responseMimeType' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_response_mime_type();
|
||||
},
|
||||
'responseSchema' => static function ( Text_Generation_Config $config ) {
|
||||
if ( $config->get_response_mime_type() === 'application/json' ) {
|
||||
return $config->get_response_schema();
|
||||
}
|
||||
return array();
|
||||
},
|
||||
'candidateCount' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_candidate_count();
|
||||
},
|
||||
'maxOutputTokens' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_max_output_tokens();
|
||||
},
|
||||
'temperature' => static function ( Text_Generation_Config $config ) {
|
||||
// In the Google AI API temperature ranges from 0.0 to 2.0.
|
||||
return $config->get_temperature() * 2.0;
|
||||
},
|
||||
'topP' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_top_p();
|
||||
},
|
||||
'topK' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_top_k();
|
||||
},
|
||||
'presencePenalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_presence_penalty();
|
||||
},
|
||||
'frequencyPenalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_frequency_penalty();
|
||||
},
|
||||
'responseLogprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_response_logprobs();
|
||||
},
|
||||
'logprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_logprobs();
|
||||
},
|
||||
'responseModalities' => static function ( Text_Generation_Config $config ) {
|
||||
$modalities = $config->get_output_modalities();
|
||||
if ( count( $modalities ) > 0 ) {
|
||||
return array_map(
|
||||
static function ( $modality ) {
|
||||
// Change "text" to "Text", "image" to "Image", etc.
|
||||
return ucfirst( $modality );
|
||||
},
|
||||
$modalities
|
||||
);
|
||||
}
|
||||
return $modalities;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request tools parameter for the model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Tools $tools The tools to prepare the parameter with.
|
||||
* @return array<string, mixed>[] The tools parameter value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid tool is provided.
|
||||
*/
|
||||
private function prepare_tools_param( Tools $tools ): array {
|
||||
$tools_param = array();
|
||||
|
||||
foreach ( $tools as $tool ) {
|
||||
if ( $tool instanceof Function_Declarations_Tool ) {
|
||||
$function_declarations = $tool->get_function_declarations();
|
||||
$declarations_data = array();
|
||||
foreach ( $function_declarations as $declaration ) {
|
||||
$declarations_data[] = array_filter(
|
||||
array(
|
||||
'name' => $declaration['name'],
|
||||
'description' => $declaration['description'] ?? null,
|
||||
'parameters' => isset( $declaration['parameters'] ) ? $this->remove_additional_properties_key( $declaration['parameters'] ) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
$tools_param[] = array(
|
||||
'functionDeclarations' => $declarations_data,
|
||||
);
|
||||
} elseif ( $tool instanceof Web_Search_Tool ) {
|
||||
// Filtering by allowed or disallowed domains is not supported by the Google AI API.
|
||||
$tools_param[] = array( 'googleSearch' => new \stdClass() );
|
||||
} else {
|
||||
throw $this->get_api_client()->create_bad_request_exception(
|
||||
'Only function declarations and web search tools are supported.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tools_param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `additionalProperties` key from the schema, including child schemas.
|
||||
*
|
||||
* This is necessary because the Google AI API will reject the schema if it contains this key.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $schema The schema to remove the `additionalProperties` key from.
|
||||
* @return array<string, mixed> The schema without the `additionalProperties` key.
|
||||
*/
|
||||
private function remove_additional_properties_key( array $schema ): array {
|
||||
if ( isset( $schema['additionalProperties'] ) ) {
|
||||
unset( $schema['additionalProperties'] );
|
||||
}
|
||||
if ( isset( $schema['properties'] ) ) {
|
||||
foreach ( $schema['properties'] as $key => $child_schema ) {
|
||||
$schema['properties'][ $key ] = $this->remove_additional_properties_key( $child_schema );
|
||||
}
|
||||
}
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request tool config parameter for the model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Tool_Config $tool_config The tool config to prepare the parameter with.
|
||||
* @return array<string, mixed> The tool config parameter value.
|
||||
*/
|
||||
private function prepare_tool_config_param( Tool_Config $tool_config ): array {
|
||||
$tool_config_param = array(
|
||||
'functionCallingConfig' => array(
|
||||
// Either 'auto' or 'any'.
|
||||
'mode' => strtoupper( $tool_config->get_function_call_mode() ),
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'ANY' === $tool_config_param['functionCallingConfig']['mode'] ) {
|
||||
$allowed_function_names = $tool_config->get_allowed_function_names();
|
||||
if ( count( $allowed_function_names ) > 0 ) {
|
||||
$tool_config_param['functionCallingConfig']['allowedFunctionNames'] = $allowed_function_names;
|
||||
}
|
||||
}
|
||||
|
||||
return $tool_config_param;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Google\Types\Safety_Setting
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Google\Types;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a safety setting that can be sent as part of request parameters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Safety_Setting implements Arrayable {
|
||||
|
||||
const HARM_CATEGORY_HATE_SPEECH = 'HARM_CATEGORY_HATE_SPEECH';
|
||||
const HARM_CATEGORY_SEXUALLY_EXPLICIT = 'HARM_CATEGORY_SEXUALLY_EXPLICIT';
|
||||
const HARM_CATEGORY_HARASSMENT = 'HARM_CATEGORY_HARASSMENT';
|
||||
const HARM_CATEGORY_DANGEROUS_CONTENT = 'HARM_CATEGORY_DANGEROUS_CONTENT';
|
||||
|
||||
const BLOCK_LOW_AND_ABOVE = 'BLOCK_LOW_AND_ABOVE';
|
||||
const BLOCK_MEDIUM_AND_ABOVE = 'BLOCK_MEDIUM_AND_ABOVE';
|
||||
const BLOCK_ONLY_HIGH = 'BLOCK_ONLY_HIGH';
|
||||
const BLOCK_NONE = 'BLOCK_NONE';
|
||||
|
||||
/**
|
||||
* The safety setting category.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* The safety setting threshold.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $threshold;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $category The safety setting category.
|
||||
* @param string $threshold The safety setting threshold.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given category or threshold is invalid.
|
||||
*/
|
||||
public function __construct( string $category, string $threshold ) {
|
||||
if ( ! $this->is_valid_category( $category ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The category %s is invalid.',
|
||||
htmlspecialchars( $category ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! $this->is_valid_threshold( $threshold ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The threshold %s is invalid.',
|
||||
htmlspecialchars( $threshold ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->category = $category;
|
||||
$this->threshold = $threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the safety setting category.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The safety setting category.
|
||||
*/
|
||||
public function get_category(): string {
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the safety setting threshold.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The safety setting threshold.
|
||||
*/
|
||||
public function get_threshold(): string {
|
||||
return $this->threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'category' => $this->category,
|
||||
'threshold' => $this->threshold,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Safety_Setting instance from an array of content data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Safety_Setting Safety_Setting instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Safety_Setting {
|
||||
if ( ! isset( $data['category'], $data['threshold'] ) ) {
|
||||
throw new InvalidArgumentException( 'Safety_Setting data must contain category and threshold.' );
|
||||
}
|
||||
|
||||
return new Safety_Setting( $data['category'], $data['threshold'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given category is valid.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $category The category to check.
|
||||
* @return bool True if the category is valid, false otherwise.
|
||||
*/
|
||||
private function is_valid_category( string $category ): bool {
|
||||
return in_array(
|
||||
$category,
|
||||
array(
|
||||
self::HARM_CATEGORY_HATE_SPEECH,
|
||||
self::HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
self::HARM_CATEGORY_HARASSMENT,
|
||||
self::HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given threshold is valid.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $threshold The threshold to check.
|
||||
* @return bool True if the threshold is valid, false otherwise.
|
||||
*/
|
||||
private function is_valid_threshold( string $threshold ): bool {
|
||||
return in_array(
|
||||
$threshold,
|
||||
array(
|
||||
self::BLOCK_LOW_AND_ABOVE,
|
||||
self::BLOCK_MEDIUM_AND_ABOVE,
|
||||
self::BLOCK_ONLY_HIGH,
|
||||
self::BLOCK_NONE,
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\OpenAI\OpenAI_AI_Service
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\OpenAI;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Class for the OpenAI AI service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.7.0 Now extends `Abstract_AI_Service`.
|
||||
*/
|
||||
class OpenAI_AI_Service extends Abstract_AI_Service implements With_API_Client {
|
||||
use With_API_Client_Trait;
|
||||
|
||||
const DEFAULT_API_BASE_URL = 'https://api.openai.com';
|
||||
const DEFAULT_API_VERSION = 'v1';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
* @param Authentication $authentication The authentication credentials.
|
||||
* @param Request_Handler $request_handler Optional. The request handler instance to use for requests. Default is a
|
||||
* new HTTP_With_Streams instance.
|
||||
*/
|
||||
public function __construct( Service_Metadata $metadata, Authentication $authentication, ?Request_Handler $request_handler = null ) {
|
||||
$this->set_service_metadata( $metadata );
|
||||
$this->set_api_client(
|
||||
new Generic_AI_API_Client(
|
||||
self::DEFAULT_API_BASE_URL,
|
||||
self::DEFAULT_API_VERSION,
|
||||
'OpenAI',
|
||||
$request_handler ?? new HTTP_With_Streams(),
|
||||
$authentication
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array {
|
||||
$api = $this->get_api_client();
|
||||
|
||||
$request = $api->create_get_request( 'models', array(), $request_options );
|
||||
$response_data = $api->make_request( $request )->get_data();
|
||||
|
||||
if ( ! isset( $response_data['data'] ) || ! $response_data['data'] ) {
|
||||
throw $api->create_missing_response_key_exception( 'data' );
|
||||
}
|
||||
|
||||
// Unfortunately, the OpenAI API does not return model capabilities, so we have to hardcode them here.
|
||||
$gpt_capabilities = array(
|
||||
AI_Capability::FUNCTION_CALLING,
|
||||
AI_Capability::TEXT_GENERATION,
|
||||
);
|
||||
$gpt_multimodal_capabilities = array(
|
||||
AI_Capability::FUNCTION_CALLING,
|
||||
AI_Capability::MULTIMODAL_INPUT,
|
||||
AI_Capability::TEXT_GENERATION,
|
||||
);
|
||||
$image_capabilities = array(
|
||||
AI_Capability::IMAGE_GENERATION,
|
||||
);
|
||||
$tts_capabilities = array(
|
||||
AI_Capability::TEXT_TO_SPEECH,
|
||||
);
|
||||
|
||||
return array_reduce(
|
||||
$response_data['data'],
|
||||
static function ( array $models_data, array $model_data ) use ( $gpt_capabilities, $gpt_multimodal_capabilities, $image_capabilities, $tts_capabilities ) {
|
||||
$model_slug = $model_data['id'];
|
||||
|
||||
if (
|
||||
str_starts_with( $model_slug, 'dall-e-' ) ||
|
||||
str_starts_with( $model_slug, 'gpt-image-' )
|
||||
) {
|
||||
$model_caps = $image_capabilities;
|
||||
} elseif (
|
||||
str_starts_with( $model_slug, 'tts-' ) ||
|
||||
str_contains( $model_slug, '-tts' )
|
||||
) {
|
||||
$model_caps = $tts_capabilities;
|
||||
} elseif (
|
||||
( str_starts_with( $model_slug, 'gpt-' ) || str_starts_with( $model_slug, 'o1-' ) )
|
||||
&& ! str_contains( $model_slug, '-instruct' )
|
||||
&& ! str_contains( $model_slug, '-realtime' )
|
||||
) {
|
||||
if ( str_starts_with( $model_slug, 'gpt-4o' ) ) {
|
||||
$model_caps = $gpt_multimodal_capabilities;
|
||||
// New multimodal output model for audio generation.
|
||||
if ( str_contains( $model_slug, '-audio' ) ) {
|
||||
$model_caps[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
} elseif ( ! str_contains( $model_slug, '-audio' ) ) {
|
||||
$model_caps = $gpt_capabilities;
|
||||
} else {
|
||||
$model_caps = array();
|
||||
}
|
||||
} else {
|
||||
$model_caps = array();
|
||||
}
|
||||
|
||||
$models_data[ $model_slug ] = Model_Metadata::from_array(
|
||||
// The OpenAI API does not return a display name, so 'name' is omitted to auto-generate.
|
||||
array(
|
||||
'slug' => $model_slug,
|
||||
'capabilities' => $model_caps,
|
||||
)
|
||||
);
|
||||
return $models_data;
|
||||
},
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new model instance for the provided model metadata and parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $model_metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
|
||||
* a list of available parameters.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generative_AI_Model The new model instance.
|
||||
*/
|
||||
protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model {
|
||||
$model_class = AI_Capabilities::get_model_class_for_capabilities(
|
||||
array(
|
||||
OpenAI_AI_Text_Generation_Model::class
|
||||
),
|
||||
$model_metadata->get_capabilities()
|
||||
);
|
||||
|
||||
return new $model_class(
|
||||
$this->get_api_client(),
|
||||
$model_metadata,
|
||||
$model_params,
|
||||
$request_options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts model slugs by preference.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string[] $model_slugs The model slugs to sort.
|
||||
* @return string[] The model slugs, sorted by preference.
|
||||
*/
|
||||
protected function sort_models_by_preference( array $model_slugs ): array {
|
||||
// Prioritize latest, non-experimental models, preferring cheaper ones.
|
||||
$get_preference_group = static function ( $model_slug ) {
|
||||
if ( str_starts_with( $model_slug, 'gpt-4.1' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-mini' ) ) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ( str_starts_with( $model_slug, 'gpt-4o' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-mini' ) ) {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
if ( str_starts_with( $model_slug, 'gpt-4' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-turbo' ) ) {
|
||||
return 4;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
if ( str_starts_with( $model_slug, 'gpt-' ) ) {
|
||||
if ( str_ends_with( $model_slug, '-turbo' ) ) {
|
||||
return 6;
|
||||
}
|
||||
return 7;
|
||||
}
|
||||
return 8;
|
||||
};
|
||||
|
||||
$preference_groups = array_fill( 0, 9, array() );
|
||||
foreach ( $model_slugs as $model_slug ) {
|
||||
$group = $get_preference_group( $model_slug );
|
||||
$preference_groups[ $group ][] = $model_slug;
|
||||
}
|
||||
|
||||
return array_merge( ...$preference_groups );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\OpenAI\OpenAI_AI_Text_Generation_Model
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\OpenAI;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Base\OpenAI_Compatible_AI_Text_Generation_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Traits\OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing an OpenAI text generation AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Renamed from `OpenAI_AI_Model`.
|
||||
* @since 0.7.0 Now extends `OpenAI_Compatible_AI_Text_Generation_Model` instead of `Abstract_AI_Model`.
|
||||
*/
|
||||
class OpenAI_AI_Text_Generation_Model extends OpenAI_Compatible_AI_Text_Generation_Model implements With_Function_Calling, With_Multimodal_Input, With_Multimodal_Output {
|
||||
use OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait {
|
||||
prepare_generate_text_params as prepare_generate_text_params_with_function_calling;
|
||||
prepare_tool as prepare_function_declarations_tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* The expected MIME type of any audio output generated by the model.
|
||||
*
|
||||
* Internal temporary storage to not have to pass it around, as it should not be part of the interface.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $expected_audio_mime_type = 'audio/mpeg';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The AI API client instance.
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
|
||||
* {@see OpenAI_AI_Service::get_model()} for the list of available
|
||||
* parameters. Default empty array.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
|
||||
parent::__construct( $api_client, $metadata, $model_params, $request_options );
|
||||
|
||||
$this->set_tool_config_from_model_params( $model_params );
|
||||
$this->set_tools_from_model_params( $model_params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid tool is provided.
|
||||
*/
|
||||
protected function prepare_generate_text_params( array $contents ): array {
|
||||
$params = $this->prepare_generate_text_params_with_function_calling( $contents );
|
||||
|
||||
// If 'audio' output is requested, the OpenAI API requires the 'audio' parameter to be set.
|
||||
if (
|
||||
isset( $params['modalities'] ) &&
|
||||
is_array( $params['modalities'] ) &&
|
||||
in_array( 'audio', $params['modalities'], true ) &&
|
||||
! isset( $params['audio'] )
|
||||
) {
|
||||
$params['audio'] = array(
|
||||
'voice' => 'alloy',
|
||||
'format' => 'mp3',
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $params['audio']['format'] ) ) {
|
||||
// Hack: Store the expected MIME type for audio output, as the OpenAI API does not return it.
|
||||
$this->expected_audio_mime_type = 'mp3' === $params['audio']['format'] ? 'audio/mpeg' : 'audio/' . $params['audio']['format'];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a single tool for the API request, amending the provided parameters as needed.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters to prepare the tools for. Passed by reference.
|
||||
* @param Tool $tool The tool to prepare.
|
||||
* @return bool True if the tool was successfully prepared, false otherwise.
|
||||
*/
|
||||
protected function prepare_tool( array &$params, Tool $tool ): bool {
|
||||
$result = $this->prepare_function_declarations_tool( $params, $tool );
|
||||
if ( ! $result ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The OpenAI API supports a 'strict' argument for function tools, which is not part of the standard OpenAI API
|
||||
* specification and therefore may not be supported by other providers.
|
||||
* Since it makes sense to always use it for OpenAI, we add it here if not set.
|
||||
*/
|
||||
if ( isset( $params['tools'] ) && is_array( $params['tools'] ) ) {
|
||||
$params['tools'] = array_map(
|
||||
function ( $openai_tool_data ) {
|
||||
if ( ! isset( $openai_tool_data['type'] ) || 'function' !== $openai_tool_data['type'] ) {
|
||||
return $openai_tool_data;
|
||||
}
|
||||
// Add the 'strict' argument to the function tool if not set.
|
||||
if ( ! isset( $openai_tool_data['function']['strict'] ) ) {
|
||||
$openai_tool_data['function']['strict'] = true;
|
||||
}
|
||||
return $openai_tool_data;
|
||||
},
|
||||
$params['tools']
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Parts instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
|
||||
$parts = parent::prepare_response_candidate_content_parts( $candidate_data );
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generation configuration transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The generation configuration transformers.
|
||||
*/
|
||||
protected function get_generation_config_transformers(): array {
|
||||
$transformers = parent::get_generation_config_transformers();
|
||||
|
||||
// Support multimodal output (e.g. for speech generation).
|
||||
$transformers['modalities'] = static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_output_modalities();
|
||||
};
|
||||
|
||||
return $transformers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Plugin_Main
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate;
|
||||
|
||||
use ATFPP\AI_Translate\Google\Google_AI_Service;
|
||||
use ATFPP\AI_Translate\Google\Google_AI_Text_Generation_Model;
|
||||
use ATFPP\AI_Translate\Deepl\Deepl_AI_Service;
|
||||
use ATFPP\AI_Translate\Deepl\Deepl_AI_Text_Generation_Model;
|
||||
use ATFPP\AI_Translate\OpenAI\OpenAI_AI_Service;
|
||||
use ATFPP\AI_Translate\OpenAI\OpenAI_AI_Text_Generation_Model;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\Service_Registration_Context;
|
||||
use ATFPP\AI_Translate\Services\Services_API;
|
||||
use ATFPP\AI_Translate\Services\Services_API_Instance;
|
||||
use ATFPP\AI_Translate\Services\Services_Loader;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Hooks;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Hook_Registrar;
|
||||
|
||||
/**
|
||||
* Plugin main class.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Plugin_Main implements With_Hooks {
|
||||
|
||||
/**
|
||||
* Plugin service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Services loader.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Services_Loader
|
||||
*/
|
||||
private $services_loader;
|
||||
|
||||
/**
|
||||
* Services API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Services_API
|
||||
*/
|
||||
private $services_api;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
*/
|
||||
public function __construct( string $main_file ) {
|
||||
// Instantiate the services loader, which separately initializes all functionality related to the AI services.
|
||||
$this->services_loader = new Services_Loader( $main_file );
|
||||
|
||||
// Then retrieve the canonical AI services instance, which is created by the services loader.
|
||||
$this->services_api = Services_API_Instance::get();
|
||||
|
||||
// Last but not least, set up the container for the main plugin functionality.
|
||||
$this->container = $this->set_up_container( $main_file );
|
||||
|
||||
$this->register_default_services();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relevant WordPress hooks.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function add_hooks(): void {
|
||||
$this->services_loader->add_hooks();
|
||||
$this->add_cleanup_hooks();
|
||||
$this->add_service_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cleanup hooks related to plugin deactivation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function add_cleanup_hooks(): void {
|
||||
// This function is only available in WordPress 6.4+.
|
||||
if ( ! function_exists( 'wp_set_options_autoload' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable autoloading of plugin options on deactivation.
|
||||
register_deactivation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// For network-wide deactivation, this cleanup cannot be reliably implemented.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Reinstate original autoload settings on (re-)activation.
|
||||
register_activation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// See deactivation hook for network-wide cleanup limitations.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds general service hooks on 'init' to initialize the plugin.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function add_service_hooks(): void {
|
||||
// Register options.
|
||||
$this->load_options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the plugin options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
private function load_options(): void {
|
||||
$option_registrar = new Option_Hook_Registrar( $this->container['option_registry'] );
|
||||
$option_registrar->add_register_callback(
|
||||
function ( $registry ) {
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
$option = $this->container['option_container']->get( $key );
|
||||
$registry->register(
|
||||
$option->get_key(),
|
||||
$option->get_registration_args()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin option names that are autoloaded.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string[] List of autoloaded plugin options.
|
||||
*/
|
||||
private function get_autoloaded_options(): array {
|
||||
$autoloaded_options = array();
|
||||
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
// Trigger option instantiation so that the autoload config is populated.
|
||||
$this->container['option_container']->get( $key );
|
||||
|
||||
$autoload = $this->container['option_repository']->get_autoload_config( $key );
|
||||
|
||||
if ( true === $autoload ) {
|
||||
$autoloaded_options[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $autoloaded_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the plugin container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return Service_Container Plugin container.
|
||||
*/
|
||||
private function set_up_container( string $main_file ): Service_Container {
|
||||
$builder = new Plugin_Service_Container_Builder();
|
||||
|
||||
return $builder->build_env( $main_file )
|
||||
->build_services()
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the default AI services.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
*/
|
||||
private function register_default_services(): void {
|
||||
$this->services_api->register_service(
|
||||
'google',
|
||||
static function ( Service_Registration_Context $context ) {
|
||||
return new Google_AI_Service(
|
||||
$context->get_metadata(),
|
||||
$context->get_authentication(),
|
||||
$context->get_request_handler()
|
||||
);
|
||||
},
|
||||
array(
|
||||
'name' => 'Google (Gemini, Imagen)',
|
||||
'credentials_url' => 'https://aistudio.google.com/app/apikey',
|
||||
'type' => Service_Type::CLOUD,
|
||||
'capabilities' => AI_Capabilities::get_model_classes_capabilities(
|
||||
array(
|
||||
Google_AI_Text_Generation_Model::class,
|
||||
)
|
||||
),
|
||||
'allow_override' => false,
|
||||
)
|
||||
);
|
||||
$this->services_api->register_service(
|
||||
'openai',
|
||||
static function ( Service_Registration_Context $context ) {
|
||||
return new OpenAI_AI_Service(
|
||||
$context->get_metadata(),
|
||||
$context->get_authentication(),
|
||||
$context->get_request_handler()
|
||||
);
|
||||
},
|
||||
array(
|
||||
'name' => 'OpenAI (GPT, Dall-E)',
|
||||
'credentials_url' => 'https://platform.openai.com/api-keys',
|
||||
'type' => Service_Type::CLOUD,
|
||||
'capabilities' => AI_Capabilities::get_model_classes_capabilities(
|
||||
array(
|
||||
OpenAI_AI_Text_Generation_Model::class
|
||||
)
|
||||
),
|
||||
'allow_override' => false,
|
||||
)
|
||||
);
|
||||
$this->services_api->register_service(
|
||||
'deepl',
|
||||
static function ( Service_Registration_Context $context ) {
|
||||
return new Deepl_AI_Service(
|
||||
$context->get_metadata(),
|
||||
$context->get_authentication(),
|
||||
$context->get_request_handler()
|
||||
);
|
||||
},
|
||||
array(
|
||||
'name' => 'DeepL',
|
||||
'credentials_url' => 'https://www.deepl.com/docs-api/translating-text/',
|
||||
'type' => Service_Type::CLOUD,
|
||||
'capabilities' => AI_Capabilities::get_model_classes_capabilities(
|
||||
array(
|
||||
Deepl_AI_Text_Generation_Model::class
|
||||
)
|
||||
),
|
||||
'allow_override' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Plugin_Service_Container_Builder
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate;
|
||||
|
||||
use ATFPP\AI_Translate\Chatbot\Chatbot;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Input;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Network_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Network_Runner;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Plugin_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Site_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Registry;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
|
||||
/**
|
||||
* Plugin service container builder.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Plugin_Service_Container_Builder {
|
||||
|
||||
/**
|
||||
* Service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->container = new Service_Container();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Service_Container Service container for the plugin.
|
||||
*/
|
||||
public function get(): Service_Container {
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin environment service for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_env( string $main_file ): self {
|
||||
$this->container['plugin_env'] = function () use ( $main_file ) {
|
||||
return new Plugin_Env( $main_file, ATFPP_V );
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_services(): self {
|
||||
$this->build_general_services();
|
||||
$this->build_option_services();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the general services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_general_services(): void {
|
||||
$this->container['input'] = static function () {
|
||||
return new Input();
|
||||
};
|
||||
$this->container['current_user'] = static function () {
|
||||
return new Current_User();
|
||||
};
|
||||
$this->container['site_env'] = static function () {
|
||||
return new Site_Env();
|
||||
};
|
||||
$this->container['network_env'] = static function () {
|
||||
return new Network_Env();
|
||||
};
|
||||
$this->container['network_runner'] = static function ( $cont ) {
|
||||
return new Network_Runner( $cont['network_env'] );
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Builds the option services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_option_services(): void {
|
||||
$this->container['option_repository'] = static function () {
|
||||
return new Option_Repository();
|
||||
};
|
||||
$this->container['option_container'] = function () {
|
||||
$options = new Option_Container();
|
||||
return $options;
|
||||
};
|
||||
$this->container['option_registry'] = static function () {
|
||||
return new Option_Registry( 'ai_services' );
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Candidates_Stream_Processor
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Class to process a candidates stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
final class Candidates_Stream_Processor {
|
||||
|
||||
/**
|
||||
* Generator that yields the chunks of response candidates.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Generator<Candidates>
|
||||
*/
|
||||
private $generator;
|
||||
|
||||
/**
|
||||
* The overall candidates instance.
|
||||
*
|
||||
* May be incomplete if the stream has not been fully processed yet.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Candidates|null
|
||||
*/
|
||||
private $candidates;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Generator<Candidates> $generator The generator that yields the chunks of response candidates.
|
||||
*/
|
||||
public function __construct( Generator $generator ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
|
||||
$this->generator = $generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all chunks from the generator and adds them to the overall candidates instance.
|
||||
*
|
||||
* A callback can be passed that is called for each chunk of candidates. You could use such a callback for example
|
||||
* to echo the text contents of each chunk as they are being processed.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param callable|null $chunk_callback Optional. Callback that is called for each chunk of candidates.
|
||||
* @return Candidates The complete candidates instance.
|
||||
*/
|
||||
public function read_all( ?callable $chunk_callback = null ): Candidates {
|
||||
foreach ( $this->generator as $candidates ) {
|
||||
$this->add_chunk( $candidates );
|
||||
if ( null !== $chunk_callback ) {
|
||||
$chunk_callback( $candidates );
|
||||
}
|
||||
}
|
||||
return $this->get_complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a chunk of candidates to the overall candidates instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Candidates $candidates The chunk of candidates to add.
|
||||
*/
|
||||
public function add_chunk( Candidates $candidates ): void {
|
||||
if ( null === $this->candidates ) {
|
||||
$this->candidates = $candidates;
|
||||
return;
|
||||
}
|
||||
|
||||
$existing_candidates = $this->candidates->to_array();
|
||||
$new_candidates = $candidates->to_array();
|
||||
|
||||
foreach ( $new_candidates as $index => $new_candidate ) {
|
||||
if ( ! isset( $existing_candidates[ $index ] ) ) {
|
||||
$existing_candidates[] = $new_candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $existing_candidates[ $index ]['content'] ) && isset( $new_candidate['content'] ) ) {
|
||||
$existing_candidates[ $index ]['content'] = $this->append_content(
|
||||
$existing_candidates[ $index ]['content'],
|
||||
$new_candidate['content']
|
||||
);
|
||||
unset( $new_candidate['content'] );
|
||||
}
|
||||
|
||||
$existing_candidates[ $index ] = array_merge( $existing_candidates[ $index ], $new_candidate );
|
||||
}
|
||||
|
||||
$this->candidates = Candidates::from_array( $existing_candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the complete candidates instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Candidates|null The complete candidates instance, or null if the generator is not done yet.
|
||||
*/
|
||||
public function get_complete(): ?Candidates {
|
||||
// Only return the candidates if the generator is done.
|
||||
if ( $this->generator->valid() ) {
|
||||
return null;
|
||||
}
|
||||
return $this->candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the content of a new candidate to the content of an existing candidate.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $existing_content The existing content data.
|
||||
* @param array<string, mixed> $new_content The new content data.
|
||||
* @return array<string, mixed> The combined content data.
|
||||
*/
|
||||
private function append_content( array $existing_content, array $new_content ) {
|
||||
if ( ! isset( $existing_content['parts'] ) || ! isset( $new_content['parts'] ) ) {
|
||||
return $existing_content;
|
||||
}
|
||||
|
||||
foreach ( $new_content['parts'] as $index => $new_part ) {
|
||||
if ( ! isset( $existing_content['parts'][ $index ] ) ) {
|
||||
$existing_content['parts'][] = $new_part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $existing_content['parts'][ $index ]['text'] ) || ! isset( $new_part['text'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing_content['parts'][ $index ]['text'] .= $new_part['text'];
|
||||
}
|
||||
|
||||
return $existing_content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\AI_Capability
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the AI capability enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class AI_Capability extends Abstract_Enum {
|
||||
|
||||
const CHAT_HISTORY = 'chat_history';
|
||||
const FUNCTION_CALLING = 'function_calling';
|
||||
const IMAGE_GENERATION = 'image_generation';
|
||||
const MULTIMODAL_INPUT = 'multimodal_input';
|
||||
const MULTIMODAL_OUTPUT = 'multimodal_output';
|
||||
const TEXT_GENERATION = 'text_generation';
|
||||
const TEXT_TO_SPEECH = 'text_to_speech';
|
||||
const WEB_SEARCH = 'web_search';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::CHAT_HISTORY,
|
||||
self::FUNCTION_CALLING,
|
||||
self::IMAGE_GENERATION,
|
||||
self::MULTIMODAL_INPUT,
|
||||
self::MULTIMODAL_OUTPUT,
|
||||
self::TEXT_GENERATION,
|
||||
self::TEXT_TO_SPEECH,
|
||||
self::WEB_SEARCH,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Abstract_Enum
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Contracts\Enum;
|
||||
|
||||
/**
|
||||
* Base class for an enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
abstract class Abstract_Enum implements Enum {
|
||||
|
||||
/**
|
||||
* The value map, to store in memory which values are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, array<string, bool>>
|
||||
*/
|
||||
private static $value_map = array();
|
||||
|
||||
/**
|
||||
* Checks if the given value is valid for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $value The value to check.
|
||||
* @return bool True if the value is valid, false otherwise.
|
||||
*/
|
||||
final public static function is_valid_value( string $value ): bool {
|
||||
$value_map = self::get_value_map_for_class( static::class );
|
||||
return isset( $value_map[ $value ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of valid values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of valid values.
|
||||
*/
|
||||
final public static function get_values(): array {
|
||||
$value_map = self::get_value_map_for_class( static::class );
|
||||
return array_keys( $value_map );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value map for the given child class name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $class_name The child class name.
|
||||
* @return array<string, bool> The value map.
|
||||
*/
|
||||
private static function get_value_map_for_class( string $class_name ): array {
|
||||
if ( ! isset( self::$value_map[ $class_name ] ) ) {
|
||||
self::$value_map[ $class_name ] = array_fill_keys( call_user_func( array( $class_name, 'get_all_values' ) ), true );
|
||||
}
|
||||
return self::$value_map[ $class_name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
abstract protected static function get_all_values(): array;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Content_Role
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the content role enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Content_Role extends Abstract_Enum {
|
||||
|
||||
const USER = 'user';
|
||||
const MODEL = 'model';
|
||||
const SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::USER,
|
||||
self::MODEL,
|
||||
self::SYSTEM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Enums\Contracts\Enum
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a class for an enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
interface Enum {
|
||||
|
||||
/**
|
||||
* Checks if the given value is valid for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $value The value to check.
|
||||
* @return bool True if the value is valid, false otherwise.
|
||||
*/
|
||||
public static function is_valid_value( string $value ): bool;
|
||||
|
||||
/**
|
||||
* Gets the list of valid values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of valid values.
|
||||
*/
|
||||
public static function get_values(): array;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Modality
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the modality enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Modality extends Abstract_Enum {
|
||||
|
||||
const TEXT = 'text';
|
||||
const IMAGE = 'image';
|
||||
const AUDIO = 'audio';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::TEXT,
|
||||
self::IMAGE,
|
||||
self::AUDIO,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Service_Type
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the service type enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Type extends Abstract_Enum {
|
||||
|
||||
const CLOUD = 'cloud';
|
||||
const SERVER = 'server';
|
||||
const CLIENT = 'client';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::CLOUD,
|
||||
self::SERVER,
|
||||
self::CLIENT,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Helpers
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Blob;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Repository;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static helper methods as part of the public API.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
|
||||
*/
|
||||
final class Helpers {
|
||||
|
||||
/**
|
||||
* Converts a text string to a Content instance.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $text The text.
|
||||
* @param string $role Optional. The role to use for the content. Default 'user'.
|
||||
* @return Content The content instance.
|
||||
*/
|
||||
public static function text_to_content( string $text, string $role = Content_Role::USER ): Content {
|
||||
return Formatter::format_content( $text, $role );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Content instance to a text string.
|
||||
*
|
||||
* This method will return the combined text from all consecutive text parts in the content.
|
||||
* Realistically, this should almost always return the text from just one part, as API responses typically do not
|
||||
* contain multiple text parts in a row - but it might be possible.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content $content The content instance.
|
||||
* @return string The text, or an empty string if there are no text parts.
|
||||
*/
|
||||
public static function content_to_text( Content $content ): string {
|
||||
$parts = $content->get_parts();
|
||||
|
||||
$text_parts = array();
|
||||
foreach ( $parts as $part ) {
|
||||
/*
|
||||
* If there is any non-text part present, we want to ensure that no interrupted text content is returned.
|
||||
* Therefore, we break the loop as soon as we encounter a non-text part, unless no text parts have been
|
||||
* found yet, in which case the text may only start with a later part.
|
||||
*/
|
||||
if ( ! $part instanceof Text_Part ) {
|
||||
if ( count( $text_parts ) > 0 ) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$text_parts[] = trim( $part->get_text() );
|
||||
}
|
||||
|
||||
if ( count( $text_parts ) === 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode( "\n\n", $text_parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text from the first Content instance in the given list which contains text.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content[] $contents The list of Content instances.
|
||||
* @return string The text, or an empty string if no Content instance has text parts.
|
||||
*/
|
||||
public static function get_text_from_contents( array $contents ): string {
|
||||
foreach ( $contents as $content ) {
|
||||
$text = self::content_to_text( $content );
|
||||
if ( '' !== $text ) {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first Content instance in the given list which contains text.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Content[] $contents The list of Content instances.
|
||||
* @return Content|null The Content instance, or null if no Content instance has text parts.
|
||||
*/
|
||||
public static function get_text_content_from_contents( array $contents ): ?Content {
|
||||
foreach ( $contents as $content ) {
|
||||
$text = self::content_to_text( $content );
|
||||
if ( '' !== $text ) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Content instances for each candidate in the given list.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Candidates $candidates The list of candidates.
|
||||
* @return Content[] The list of Content instances.
|
||||
*/
|
||||
public static function get_candidate_contents( Candidates $candidates ): array {
|
||||
$contents = array();
|
||||
|
||||
foreach ( $candidates as $candidate ) {
|
||||
$content = $candidate->get_content();
|
||||
if ( ! $content ) {
|
||||
continue;
|
||||
}
|
||||
$contents[] = $content;
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a stream of candidates, aggregating the candidates chunks into a single candidates instance.
|
||||
*
|
||||
* This method returns a stream processor instance that can be used to read all chunks from the given candidates
|
||||
* generator and process them with a callback. Alternatively, you can read from the generator yourself and provide
|
||||
* all chunks to the processor manually.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Generator<Candidates> $generator The generator that yields the chunks of response candidates.
|
||||
* @return Candidates_Stream_Processor The stream processor instance.
|
||||
*/
|
||||
public static function process_candidates_stream( Generator $generator ): Candidates_Stream_Processor { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
|
||||
return new Candidates_Stream_Processor( $generator );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base64-encoded data URL representation of the given file URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file Absolute path to the file, or its URL.
|
||||
* @param string $mime_type Optional. The MIME type of the file. If provided, the base64-encoded data URL will
|
||||
* be prefixed with `data:{mime_type};base64,`. Default empty string.
|
||||
* @return string The base64-encoded file data URL, or empty string on failure.
|
||||
*/
|
||||
public static function file_to_base64_data_url( string $file, string $mime_type = '' ): string {
|
||||
$blob = self::file_to_blob( $file, $mime_type );
|
||||
if ( ! $blob ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::blob_to_base64_data_url( $blob );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary data blob representation of the given file URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file Absolute path to the file, or its URL.
|
||||
* @param string $mime_type Optional. The MIME type of the file. If provided, the automatically detected MIME type
|
||||
* will be overwritten. Default empty string.
|
||||
* @return Blob|null The binary data blob, or null on failure.
|
||||
*/
|
||||
public static function file_to_blob( string $file, string $mime_type = '' ): ?Blob {
|
||||
try {
|
||||
return Blob::from_file( $file, $mime_type );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base64-encoded data URL representation of the given binary data blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Blob $blob The binary data blob.
|
||||
* @return string The base64-encoded file data URL, or empty string on failure.
|
||||
*/
|
||||
public static function blob_to_base64_data_url( Blob $blob ): string {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
$base64 = base64_encode( $blob->get_binary_data() );
|
||||
$mime_type = $blob->get_mime_type();
|
||||
return "data:$mime_type;base64,$base64";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary data blob representation of the given base64-encoded data URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $base64_data_url The base64-encoded data URL.
|
||||
* @return Blob|null The binary data blob, or null on failure.
|
||||
*/
|
||||
public static function base64_data_url_to_blob( string $base64_data_url ): ?Blob {
|
||||
if ( ! preg_match( '/^data:([a-z0-9-]+\/[a-z0-9-]+);base64,/', $base64_data_url, $matches ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$base64 = substr( $base64_data_url, strlen( $matches[0] ) );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$binary_data = base64_decode( $base64 );
|
||||
if ( false === $binary_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Blob( $binary_data, $matches[1] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the given base64 data is prefixed correctly to be a data URL.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param string $base64_data Base64-encoded data. If it is already a data URL, it will be returned as is.
|
||||
* @param string $mime_type MIME type for the data.
|
||||
* @return string The base64 data URL.
|
||||
*/
|
||||
public static function base64_data_to_base64_data_url( string $base64_data, string $mime_type ): string {
|
||||
if ( str_starts_with( $base64_data, 'data:' ) ) {
|
||||
return $base64_data;
|
||||
}
|
||||
|
||||
return 'data:' . $mime_type . ';base64,' . $base64_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the given base64 data URL has its prefix removed to be just the base64 data.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param string $base64_data_url Base64 data URL. If it is already without prefix, it will be returned as is.
|
||||
* @return string The base64-encoded data.
|
||||
*/
|
||||
public static function base64_data_url_to_base64_data( string $base64_data_url ): string {
|
||||
if ( ! str_starts_with( $base64_data_url, 'data:' ) ) {
|
||||
return $base64_data_url;
|
||||
}
|
||||
|
||||
return preg_replace(
|
||||
'/^data:[a-z0-9-]+\/[a-z0-9-]+;base64,/',
|
||||
'',
|
||||
$base64_data_url
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Blob
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Simple value class representing a binary data blob, e.g. from a file.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Blob {
|
||||
|
||||
/**
|
||||
* The binary data of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $binary_data;
|
||||
|
||||
/**
|
||||
* The MIME type of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $mime_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $binary_data The binary data of the blob.
|
||||
* @param string $mime_type The MIME type of the blob.
|
||||
*/
|
||||
public function __construct( string $binary_data, string $mime_type ) {
|
||||
$this->binary_data = $binary_data;
|
||||
$this->mime_type = $mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the binary data of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The binary data.
|
||||
*/
|
||||
public function get_binary_data(): string {
|
||||
return $this->binary_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the MIME type of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The MIME type.
|
||||
*/
|
||||
public function get_mime_type(): string {
|
||||
return $this->mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blob instance from a file.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file The file path or URL.
|
||||
* @param string $mime_type Optional. MIME type, to override the automatic detection. Default empty string.
|
||||
* @return Blob The blob instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the file could not be read or if the MIME type cannot be determined.
|
||||
*/
|
||||
public static function from_file( string $file, string $mime_type = '' ): self {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$blob = file_get_contents( $file );
|
||||
if ( ! $blob ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Could not read file %s.',
|
||||
htmlspecialchars( $file ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $mime_type ) {
|
||||
$file_type = wp_check_filetype( $file );
|
||||
if ( ! $file_type['type'] ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Could not determine MIME type of file %s.',
|
||||
htmlspecialchars( $file ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$mime_type = $file_type['type'];
|
||||
}
|
||||
|
||||
return new self( $blob, $mime_type );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Candidate
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a candidate for a content response from a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Candidate implements Arrayable {
|
||||
|
||||
/**
|
||||
* The content, unless no content is available as part of the candidate.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var ?Content
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* Additional data for the candidate, if any.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param ?Content $content The content, or null to indicate no content is available.
|
||||
* @param array<string, mixed> $additional_data Additional data for the candidate, if any.
|
||||
*/
|
||||
public function __construct( ?Content $content, array $additional_data = array() ) {
|
||||
$this->content = $content;
|
||||
|
||||
// Remove the content from the additional data, if present, to prevent conflicts.
|
||||
unset( $additional_data['content'] );
|
||||
$this->additional_data = $additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ?Content The content.
|
||||
*/
|
||||
public function get_content(): ?Content {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a field value from the additional data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $field The field name.
|
||||
* @return mixed|null The field value, or null if not found.
|
||||
*/
|
||||
public function get_field_value( string $field ) {
|
||||
if ( isset( $this->additional_data[ $field ] ) ) {
|
||||
return $this->additional_data[ $field ];
|
||||
}
|
||||
|
||||
if ( str_contains( $field, '_' ) ) {
|
||||
$camel_case_field = $this->underscore_to_camel_case( $field );
|
||||
if ( isset( $this->additional_data[ $camel_case_field ] ) ) {
|
||||
return $this->additional_data[ $camel_case_field ];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A few common special cases.
|
||||
* For instance, "finish_reason" is sometimes called "stop_reason".
|
||||
*/
|
||||
switch ( $field ) {
|
||||
case 'finish_reason':
|
||||
return $this->get_field_value( 'stop_reason' );
|
||||
case 'finishReason':
|
||||
return $this->get_field_value( 'stopReason' );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> The additional data.
|
||||
*/
|
||||
public function get_additional_data(): array {
|
||||
return $this->additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_merge(
|
||||
array(
|
||||
'content' => $this->content ? $this->content->to_array() : null,
|
||||
),
|
||||
$this->additional_data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Candidate instance from an array of content data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Candidate Candidate instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Candidate {
|
||||
if ( ! isset( $data['content'] ) ) {
|
||||
return new Candidate( null, $data );
|
||||
}
|
||||
|
||||
/*
|
||||
* Apparently, the API sometimes omits this.
|
||||
* Given candidates are always part of a model response, we can safely assume the role is 'model'.
|
||||
*/
|
||||
if ( ! isset( $data['content']['role'] ) ) {
|
||||
$data['content']['role'] = Content_Role::MODEL;
|
||||
}
|
||||
|
||||
$content = Content::from_array( $data['content'] );
|
||||
unset( $data['content'] );
|
||||
|
||||
return new Candidate( $content, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'content' => array_merge(
|
||||
array(
|
||||
'description' => __( 'Candidate content.', 'ai-services' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
Content::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a snake_case string to camelCase.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $input The snake_case string.
|
||||
* @return string The camelCase string.
|
||||
*/
|
||||
private function underscore_to_camel_case( string $input ): string {
|
||||
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Candidates
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of response candidates for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Candidates implements Collection, Arrayable {
|
||||
|
||||
/**
|
||||
* The candidates.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Candidate[]
|
||||
*/
|
||||
private $candidates = array();
|
||||
|
||||
/**
|
||||
* Adds a candidate to the collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Candidate $candidate The candidate.
|
||||
*/
|
||||
public function add_candidate( Candidate $candidate ): void {
|
||||
$this->candidates[] = $candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the candidates collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ArrayIterator<int, Candidate> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the candidates collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the parts collection by the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The filter arguments.
|
||||
*
|
||||
* @type string $part_class_name The class name to only allow candidates with content parts of that class.
|
||||
* }
|
||||
* @return Candidates The filtered parts collection.
|
||||
*/
|
||||
public function filter( array $args ): self {
|
||||
if ( isset( $args['part_class_name'] ) ) {
|
||||
$part_class_name = $args['part_class_name'];
|
||||
$map = static function ( Candidate $candidate ) use ( $part_class_name ) {
|
||||
$candidate_content = $candidate->get_content();
|
||||
if ( ! $candidate_content ) {
|
||||
return null;
|
||||
}
|
||||
$filtered_parts = $candidate_content->get_parts()->filter( array( 'class_name' => $part_class_name ) );
|
||||
if ( count( $filtered_parts ) > 0 ) {
|
||||
$candidate_data = $candidate->to_array();
|
||||
$candidate_data['content']['parts'] = $filtered_parts->to_array();
|
||||
return Candidate::from_array( $candidate_data );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
$map = static function ( Candidate $candidate ) {
|
||||
return Candidate::from_array( $candidate->to_array() );
|
||||
};
|
||||
}
|
||||
|
||||
$candidates = new Candidates();
|
||||
foreach ( $this->candidates as $candidate ) {
|
||||
$mapped_candidate = $map( $candidate );
|
||||
if ( $mapped_candidate ) {
|
||||
$candidates->add_candidate( $mapped_candidate );
|
||||
}
|
||||
}
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the candidate at the given index.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Candidate The candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Candidate {
|
||||
if ( ! isset( $this->candidates[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->candidates[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed>[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Candidate $candidate ) {
|
||||
return $candidate->to_array();
|
||||
},
|
||||
$this->candidates
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Candidates instance from an array of candidates data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed>[] $data The candidates data.
|
||||
* @return Candidates The Candidates instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the candidates data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Candidates {
|
||||
$candidates = new Candidates();
|
||||
|
||||
foreach ( $data as $candidate ) {
|
||||
if ( ! is_array( $candidate ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid candidate data.' );
|
||||
}
|
||||
|
||||
$candidates->add_candidate( Candidate::from_array( $candidate ) );
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Content
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing an entry of content for a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Content implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The role of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $role;
|
||||
|
||||
/**
|
||||
* The parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Parts
|
||||
*/
|
||||
private $parts;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $role The role of the content.
|
||||
* @param Parts $parts The parts of the content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given role is invalid.
|
||||
*/
|
||||
public function __construct( string $role, Parts $parts ) {
|
||||
if ( ! Content_Role::is_valid_value( $role ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The role %s is invalid.',
|
||||
htmlspecialchars( $role ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->role = $role;
|
||||
$this->parts = $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The role of the content.
|
||||
*/
|
||||
public function get_role(): string {
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Parts The parts of the content.
|
||||
*/
|
||||
public function get_parts(): Parts {
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'role' => $this->role,
|
||||
'parts' => $this->parts->to_array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Content instance from an array of content data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Content Content instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Content {
|
||||
if ( ! isset( $data['role'], $data['parts'] ) ) {
|
||||
throw new InvalidArgumentException( 'Content data must contain role and parts.' );
|
||||
}
|
||||
|
||||
return new Content( $data['role'], Parts::from_array( $data['parts'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'role' => array(
|
||||
'description' => __( 'The role of the content, i.e. which source it comes from.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
Content_Role::USER,
|
||||
Content_Role::MODEL,
|
||||
Content_Role::SYSTEM,
|
||||
),
|
||||
),
|
||||
'parts' => array_merge(
|
||||
array( 'description' => __( 'Content parts, including optional multimodal input.', 'ai-services' ) ),
|
||||
Parts::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Types\Contracts\Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Part extends Arrayable {
|
||||
|
||||
/**
|
||||
* Sets data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
*/
|
||||
public function set_data( array $data ): void;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Types\Contracts\Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
interface Tool extends Arrayable {
|
||||
|
||||
/**
|
||||
* Sets data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
*/
|
||||
public function set_data( array $data ): void;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\History
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a chat history.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class History {
|
||||
|
||||
/**
|
||||
* The feature the history is associated with.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $feature;
|
||||
|
||||
/**
|
||||
* The history slug, unique within the feature.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* When the history was last updated, as MySQL datetime string in GMT.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $last_updated;
|
||||
|
||||
/**
|
||||
* The history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var History_Entry[]
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $feature The feature the history is associated with.
|
||||
* @param string $slug The history slug.
|
||||
* @param string $last_updated When the history was last updated, as MySQL datetime string in GMT.
|
||||
* @param History_Entry[] $entries The history entries.
|
||||
*/
|
||||
public function __construct( string $feature, string $slug, string $last_updated, array $entries ) {
|
||||
$this->feature = $feature;
|
||||
$this->slug = $slug;
|
||||
$this->last_updated = $last_updated;
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the feature the history is associated with.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The feature.
|
||||
*/
|
||||
public function get_feature(): string {
|
||||
return $this->feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history slug.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The history slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets when the history was last updated.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The last updated MySQL datetime string in GMT.
|
||||
*/
|
||||
public function get_last_updated(): string {
|
||||
return $this->last_updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return History_Entry[] The history entries.
|
||||
*/
|
||||
public function get_entries(): array {
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param History_Entry[]|array<string, mixed>[] $entries The history entries.
|
||||
*/
|
||||
public function set_entries( array $entries ): void {
|
||||
$this->entries = array_map(
|
||||
function ( $entry_data ) {
|
||||
if ( ! $entry_data instanceof History_Entry ) {
|
||||
return History_Entry::from_array( $entry_data );
|
||||
}
|
||||
return $entry_data;
|
||||
},
|
||||
$entries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'feature' => $this->feature,
|
||||
'slug' => $this->slug,
|
||||
'lastUpdated' => $this->last_updated,
|
||||
'entries' => array_map(
|
||||
function ( History_Entry $entry ) {
|
||||
return $entry->to_array();
|
||||
},
|
||||
$this->entries
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a History instance from an array of history data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The history data.
|
||||
* @return History History instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): History {
|
||||
if ( ! isset( $data['feature'], $data['slug'], $data['lastUpdated'], $data['entries'] ) ) {
|
||||
throw new InvalidArgumentException( 'History data must contain feature, slug, lastUpdated, and entries.' );
|
||||
}
|
||||
|
||||
return new History(
|
||||
$data['feature'],
|
||||
$data['slug'],
|
||||
$data['lastUpdated'],
|
||||
array_map(
|
||||
function ( array $entry_data ) {
|
||||
return History_Entry::from_array( $entry_data );
|
||||
},
|
||||
$data['entries']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'feature' => array(
|
||||
'description' => __( 'Unique identifier of the feature. Must only contain lowercase letters, numbers, hyphens.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique identifier of the history within the feature. Must only contain lowercase letters, numbers, hyphens.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'lastUpdated' => array(
|
||||
'description' => __( 'When the history was last updated, as MySQL datetime string in GMT.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'entries' => array(
|
||||
'description' => __( 'The history entries, in ascending order.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => History_Entry::get_json_schema(),
|
||||
'context' => array( 'view', 'edit' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\History_Entry
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a single entry in a chat history.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class History_Entry {
|
||||
|
||||
/**
|
||||
* The history entry's content.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var Content
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* Additional data for the history entry, if any.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Content $content The history entry content.
|
||||
* @param array<string, mixed> $additional_data Additional data for the history entry, if any.
|
||||
*/
|
||||
public function __construct( Content $content, array $additional_data = array() ) {
|
||||
$this->content = $content;
|
||||
|
||||
// Remove the content from the additional data, if present, to prevent conflicts.
|
||||
unset( $additional_data['content'] );
|
||||
$this->additional_data = $additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history entry content.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return Content The content.
|
||||
*/
|
||||
public function get_content(): Content {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The additional data.
|
||||
*/
|
||||
public function get_additional_data(): array {
|
||||
return $this->additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the candidate to an array.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation of the candidate.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_merge(
|
||||
array(
|
||||
'content' => $this->content->to_array(),
|
||||
),
|
||||
$this->additional_data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a History_Entry instance from an array of content data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return History_Entry History_Entry instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): History_Entry {
|
||||
if ( ! isset( $data['content'] ) ) {
|
||||
throw new InvalidArgumentException( 'History entry data must contain content.' );
|
||||
}
|
||||
|
||||
$content = Content::from_array( $data['content'] );
|
||||
unset( $data['content'] );
|
||||
|
||||
return new History_Entry( $content, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'content' => array_merge(
|
||||
array(
|
||||
'description' => __( 'History entry content.', 'ai-services' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
Content::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Model_Metadata
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value class representing metadata about a generative AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Model_Metadata implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The model slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The model name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* List of AI capabilities supported by the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $capabilities;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The arguments for the model metadata.
|
||||
*
|
||||
* @type string $slug The model slug.
|
||||
* @type string $name Optional. The model name. Default will be generated from the slug.
|
||||
* @type string[] $capabilities Optional. The list of AI capabilities supported by the model.
|
||||
* Default empty array.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given slug is invalid.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$args = $this->parse_args( $args );
|
||||
|
||||
$this->slug = $args['slug'];
|
||||
$this->name = $args['name'];
|
||||
$this->capabilities = $args['capabilities'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The model name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of AI capabilities supported by the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] List of AI capabilities supported by the model.
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'capabilities' => $this->capabilities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Model_Metadata instance from an array of model metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The model metadata arguments.
|
||||
* @return Model_Metadata The Model_Metadata instance.
|
||||
*/
|
||||
public static function from_array( array $args ): Model_Metadata {
|
||||
return new Model_Metadata( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the model metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The model metadata arguments.
|
||||
* @return array<string, mixed> The parsed model metadata arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid argument is provided.
|
||||
*/
|
||||
private function parse_args( array $args ): array {
|
||||
if ( ! isset( $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException( 'The slug is required.' );
|
||||
}
|
||||
|
||||
if ( isset( $args['name'] ) ) {
|
||||
$args['name'] = (string) $args['name'];
|
||||
} else {
|
||||
$args['name'] = ucwords( str_replace( array( '-', '_' ), ' ', $args['slug'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
if ( ! is_array( $args['capabilities'] ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities must be an array.' );
|
||||
}
|
||||
foreach ( $args['capabilities'] as $capability ) {
|
||||
if ( ! AI_Capability::is_valid_value( $capability ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities contain an invalid value.' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$args['capabilities'] = array();
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique model slug.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'User-facing model name.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'description' => __( 'List of AI capabilities supported by the model.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => AI_Capability::get_values(),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of content parts for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Parts implements Collection, Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Part[]
|
||||
*/
|
||||
private $parts = array();
|
||||
|
||||
/**
|
||||
* Adds a text part to the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $text The text.
|
||||
*/
|
||||
public function add_text_part( string $text ): void {
|
||||
$this->add_part(
|
||||
Text_Part::from_array( array( 'text' => $text ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function call part to the content.
|
||||
*
|
||||
* Every function call must have at least one of $id and $name provided.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $id The ID of the function call, or an empty string.
|
||||
* @param string $name The name of the function, or an empty string.
|
||||
* @param array<string, mixed> $args The arguments of the function call.
|
||||
*/
|
||||
public function add_function_call_part( string $id, string $name, array $args ): void {
|
||||
$data = array();
|
||||
if ( $id ) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
if ( $name ) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
$data['args'] = $args;
|
||||
|
||||
$this->add_part(
|
||||
Function_Call_Part::from_array(
|
||||
array( 'functionCall' => $data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function response part to the content.
|
||||
*
|
||||
* Every function response must have at least one of $id and $name provided.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $id The ID of the function response, or an empty string. If present, this must match the
|
||||
* function call ID.
|
||||
* @param string $name The name of the function, or an empty string. If present, this must match the name of
|
||||
* the function called.
|
||||
* @param mixed $response The function output response.
|
||||
*/
|
||||
public function add_function_response_part( string $id, string $name, $response ): void {
|
||||
$data = array();
|
||||
if ( $id ) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
if ( $name ) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
$data['response'] = $response;
|
||||
|
||||
$this->add_part(
|
||||
Function_Response_Part::from_array(
|
||||
array( 'functionResponse' => $data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a part to the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Part $part The part.
|
||||
*/
|
||||
public function add_part( Part $part ): void {
|
||||
$this->parts[] = $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the parts collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ArrayIterator<int, Part> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the parts collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the parts collection by the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The filter arguments.
|
||||
*
|
||||
* @type string $class_name The class name to only allow parts of that class.
|
||||
* }
|
||||
* @return Parts The filtered parts collection.
|
||||
*/
|
||||
public function filter( array $args ): self {
|
||||
if ( isset( $args['class_name'] ) ) {
|
||||
$class_name = $args['class_name'];
|
||||
$map = static function ( Part $part ) use ( $class_name ) {
|
||||
if ( $part instanceof $class_name ) {
|
||||
return call_user_func( array( $class_name, 'from_array' ), $part->to_array() );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
$map = static function ( Part $part ) {
|
||||
return call_user_func( array( get_class( $part ), 'from_array' ), $part->to_array() );
|
||||
};
|
||||
}
|
||||
|
||||
$parts = new Parts();
|
||||
foreach ( $this->parts as $part ) {
|
||||
$mapped_part = $map( $part );
|
||||
if ( $mapped_part ) {
|
||||
$parts->add_part( $mapped_part );
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the part at the given index.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Part The part.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Part {
|
||||
if ( ! isset( $this->parts[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->parts[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Part $part ) {
|
||||
return $part->to_array();
|
||||
},
|
||||
$this->parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Parts instance from an array of parts data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed[] $data The parts data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the parts data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Parts {
|
||||
$parts = new Parts();
|
||||
|
||||
foreach ( $data as $part ) {
|
||||
if ( ! is_array( $part ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid part data.' );
|
||||
}
|
||||
|
||||
if ( isset( $part['text'] ) ) {
|
||||
$parts->add_part( Text_Part::from_array( $part ) );
|
||||
} elseif ( isset( $part['functionCall'] ) ) {
|
||||
$parts->add_part( Function_Call_Part::from_array( $part ) );
|
||||
} elseif ( isset( $part['functionResponse'] ) ) {
|
||||
$parts->add_part( Function_Response_Part::from_array( $part ) );
|
||||
} else {
|
||||
throw new InvalidArgumentException( 'Invalid part data.' );
|
||||
}
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
$text_part_schema = Text_Part::get_json_schema();
|
||||
$function_call_part_schema = Function_Call_Part::get_json_schema();
|
||||
$function_response_part_schema = Function_Response_Part::get_json_schema();
|
||||
unset(
|
||||
$text_part_schema['type'],
|
||||
$function_call_part_schema['type'],
|
||||
$function_response_part_schema['type']
|
||||
);
|
||||
|
||||
return array(
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'oneOf' => array(
|
||||
$text_part_schema,
|
||||
$function_call_part_schema,
|
||||
$function_response_part_schema,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Abstract_Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Part;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class for a part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
abstract class Abstract_Part implements Part, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The part data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final public function __construct() {
|
||||
// Empty constructor, only to prevent override.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
*/
|
||||
final public function set_data( array $data ): void {
|
||||
$this->data = $this->format_data( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
abstract protected function format_data( array $data ): array;
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
abstract protected function get_default_data(): array;
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
if ( ! $this->data ) {
|
||||
$this->data = $this->get_default_data();
|
||||
}
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a specific Part instance from an array of part data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return Part The Part instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the parts data is invalid.
|
||||
*/
|
||||
final public static function from_array( array $data ): Part {
|
||||
$part = new static();
|
||||
$part->set_data( $data );
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function call part of content for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Call_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the ID of the function call from the part.
|
||||
*
|
||||
* Every function call must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function call ID, or empty string if none set.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionCall']['id'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionCall']['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function name from the part.
|
||||
*
|
||||
* Every function call must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function name, or empty string if none set.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionCall']['name'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionCall']['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function input arguments from the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The function input arguments.
|
||||
*/
|
||||
public function get_args(): array {
|
||||
return $this->to_array()['functionCall']['args'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionCall'] ) || ! is_array( $data['functionCall'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain an associative array functionCall value.' );
|
||||
}
|
||||
|
||||
$function_call = $data['functionCall'];
|
||||
|
||||
if (
|
||||
( ! isset( $function_call['id'] ) || ! is_string( $function_call['id'] ) ) &&
|
||||
( ! isset( $function_call['name'] ) || ! is_string( $function_call['name'] ) )
|
||||
) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain either a string id value or a string name value.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $function_call['args'] ) || ! is_array( $function_call['args'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain an object / associative array args value.' );
|
||||
}
|
||||
|
||||
$function_call_formatted = array();
|
||||
if ( isset( $function_call['id'] ) ) {
|
||||
$function_call_formatted['id'] = $function_call['id'];
|
||||
}
|
||||
if ( isset( $function_call['name'] ) ) {
|
||||
$function_call_formatted['name'] = $function_call['name'];
|
||||
}
|
||||
$function_call_formatted['args'] = $function_call['args'];
|
||||
|
||||
return array(
|
||||
'functionCall' => $function_call_formatted,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionCall' => array(
|
||||
'name' => '',
|
||||
'args' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionCall' => array(
|
||||
'description' => __( 'Function call as part of the prompt.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'ID of the function call. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function to call. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'args' => array(
|
||||
'description' => __( 'Arguments input for the function to call.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function response part of content for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Response_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the ID of the function response from the part.
|
||||
*
|
||||
* If present, this must match the function call ID.
|
||||
* Every function response must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function response ID, or empty string if none set.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionResponse']['id'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionResponse']['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function name from the part.
|
||||
*
|
||||
* If present, this must match the name of the function called.
|
||||
* Every function response must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function name, or empty string if none set.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionResponse']['name'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionResponse']['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function output response from the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed The function output response.
|
||||
*/
|
||||
public function get_response() {
|
||||
return $this->to_array()['functionResponse']['response'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionResponse'] ) || ! is_array( $data['functionResponse'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain an associative array functionResponse value.' );
|
||||
}
|
||||
|
||||
$function_response = $data['functionResponse'];
|
||||
|
||||
if (
|
||||
( ! isset( $function_response['id'] ) || ! is_string( $function_response['id'] ) ) &&
|
||||
( ! isset( $function_response['name'] ) || ! is_string( $function_response['name'] ) )
|
||||
) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain either a string id value or a string name value.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $function_response['response'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain a response value.' );
|
||||
}
|
||||
|
||||
$function_response_formatted = array();
|
||||
if ( isset( $function_response['id'] ) ) {
|
||||
$function_response_formatted['id'] = $function_response['id'];
|
||||
}
|
||||
if ( isset( $function_response['name'] ) ) {
|
||||
$function_response_formatted['name'] = $function_response['name'];
|
||||
}
|
||||
$function_response_formatted['response'] = $function_response['response'];
|
||||
|
||||
return array(
|
||||
'functionResponse' => $function_response_formatted,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionResponse' => array(
|
||||
'name' => '',
|
||||
'response' => null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionResponse' => array(
|
||||
'description' => __( 'Function response as part of the prompt.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'ID of the function response. If present, it must match the function call ID. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function called. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'response' => array(
|
||||
'description' => __( 'Response from the function called.', 'ai-services' ),
|
||||
'type' => array( 'string', 'number', 'boolean', 'array', 'object' ),
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a text part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Text_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the text from the part.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string The text.
|
||||
*/
|
||||
public function get_text(): string {
|
||||
return $this->to_array()['text'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['text'] ) || ! is_string( $data['text'] ) ) {
|
||||
throw new InvalidArgumentException( 'The text part data must contain a string text value.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'text' => $data['text'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'text' => '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'text' => array(
|
||||
'description' => __( 'Prompt text content.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Service_Metadata
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value class representing metadata about a generative AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Metadata implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The service name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* The service credentials URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $credentials_url;
|
||||
|
||||
/**
|
||||
* The service type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* List of AI capabilities supported by the service and its models.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $capabilities;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The arguments for the service metadata.
|
||||
*
|
||||
* @type string $slug The service slug.
|
||||
* @type string $name Optional. The service name. Default will be generated from the slug.
|
||||
* @type string $credentials_url Optional. The service credentials URL. Default empty string.
|
||||
* @type string $type Optional. The service type. Default `Service_Type::CLOUD`.
|
||||
* @type string[] $capabilities Optional. The list of AI capabilities supported by the service and its
|
||||
* models. Default empty array.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given slug is invalid.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$args = $this->parse_args( $args );
|
||||
|
||||
$this->slug = $args['slug'];
|
||||
$this->name = $args['name'];
|
||||
$this->credentials_url = $args['credentials_url'];
|
||||
$this->type = $args['type'];
|
||||
$this->capabilities = $args['capabilities'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service credentials URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service credentials URL.
|
||||
*/
|
||||
public function get_credentials_url(): string {
|
||||
return $this->credentials_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service type.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of AI capabilities supported by the service and its models.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] List of AI capabilities supported by the service and its models.
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'credentials_url' => $this->credentials_url,
|
||||
'type' => $this->type,
|
||||
'capabilities' => $this->capabilities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Service_Metadata instance from an array of service metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service metadata arguments.
|
||||
* @return Service_Metadata The Service_Metadata instance.
|
||||
*/
|
||||
public static function from_array( array $args ): Service_Metadata {
|
||||
return new Service_Metadata( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the service metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service metadata arguments.
|
||||
* @return array<string, mixed> The parsed service metadata arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid argument is provided.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
private function parse_args( array $args ): array {
|
||||
if ( ! isset( $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException( 'The slug is required.' );
|
||||
}
|
||||
|
||||
if ( ! preg_match( '/^[a-z0-9-]+$/', $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The service slug must only contain lowercase letters, numbers, and hyphens.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['name'] ) ) {
|
||||
$args['name'] = (string) $args['name'];
|
||||
} else {
|
||||
$args['name'] = ucwords( str_replace( array( '-', '_' ), ' ', $args['slug'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $args['credentials_url'] ) ) {
|
||||
$args['credentials_url'] = (string) $args['credentials_url'];
|
||||
|
||||
// Basic sanity check to ensure a protocol is present.
|
||||
if ( ! str_contains( $args['credentials_url'], ':' ) && ! in_array( $args['credentials_url'][0], array( '/', '#', '?' ), true ) ) {
|
||||
$args['credentials_url'] = 'https://' . $args['credentials_url'];
|
||||
}
|
||||
} else {
|
||||
$args['credentials_url'] = '';
|
||||
}
|
||||
|
||||
if ( isset( $args['type'] ) ) {
|
||||
if ( ! Service_Type::is_valid_value( $args['type'] ) ) {
|
||||
throw new InvalidArgumentException( 'The service type is invalid.' );
|
||||
}
|
||||
} else {
|
||||
$args['type'] = Service_Type::CLOUD;
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
if ( ! is_array( $args['capabilities'] ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities must be an array.' );
|
||||
}
|
||||
foreach ( $args['capabilities'] as $capability ) {
|
||||
if ( ! AI_Capability::is_valid_value( $capability ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities contain an invalid value.' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$args['capabilities'] = array();
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique service slug.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'User-facing service name.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'credentials_url' => array(
|
||||
'description' => __( 'Service credentials URL, or empty string if not specified.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => __( 'Service type.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => Service_Type::get_values(),
|
||||
'readonly' => true,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'description' => __( 'List of AI capabilities supported by the service and its models.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => AI_Capability::get_values(),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Modality;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing text configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.5.0 Renamed from `Generation_Config`.
|
||||
* @since 0.7.0 Now extends `Abstract_Generation_Config`.
|
||||
*/
|
||||
class Text_Generation_Config extends Abstract_Generation_Config {
|
||||
|
||||
/**
|
||||
* Returns the stop sequences.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The stop sequences, or empty array if not set.
|
||||
*/
|
||||
public function get_stop_sequences(): array {
|
||||
return $this->get_arg( 'stopSequences' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response MIME type.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string The response MIME type, or empty string if not set.
|
||||
*/
|
||||
public function get_response_mime_type(): string {
|
||||
return $this->get_arg( 'responseMimeType' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response schema.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The response schema, or empty array if not set.
|
||||
*/
|
||||
public function get_response_schema(): array {
|
||||
return $this->get_arg( 'responseSchema' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the candidate count.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The candidate count (default 1).
|
||||
*/
|
||||
public function get_candidate_count(): int {
|
||||
return $this->get_arg( 'candidateCount' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum output tokens.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The maximum output tokens, or 0 if not set.
|
||||
*/
|
||||
public function get_max_output_tokens(): int {
|
||||
return $this->get_arg( 'maxOutputTokens' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the temperature.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The temperature (between 0.0 and 1.0), or 0.0 if not set.
|
||||
*/
|
||||
public function get_temperature(): float {
|
||||
return $this->get_arg( 'temperature' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top P.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The top P, or 0.0 if not set.
|
||||
*/
|
||||
public function get_top_p(): float {
|
||||
return $this->get_arg( 'topP' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top K.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The top K, or 0 if not set.
|
||||
*/
|
||||
public function get_top_k(): int {
|
||||
return $this->get_arg( 'topK' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presence penalty.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The presence penalty, or 0.0 if not set.
|
||||
*/
|
||||
public function get_presence_penalty(): float {
|
||||
return $this->get_arg( 'presencePenalty' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frequency penalty.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The frequency penalty, or 0.0 if not set.
|
||||
*/
|
||||
public function get_frequency_penalty(): float {
|
||||
return $this->get_arg( 'frequencyPenalty' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether to include the response logprobs.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool Whether to include the response logprobs.
|
||||
*/
|
||||
public function get_response_logprobs(): bool {
|
||||
return $this->get_arg( 'responseLogprobs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top logprobs.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The top logprobs, or 0 if not set.
|
||||
*/
|
||||
public function get_logprobs(): int {
|
||||
return $this->get_arg( 'logprobs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output modalities.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @return string[] The output modalities, or empty array if not set.
|
||||
*/
|
||||
public function get_output_modalities(): array {
|
||||
return $this->get_arg( 'outputModalities' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the definition for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The supported arguments definition.
|
||||
*/
|
||||
protected function get_supported_args_definition(): array {
|
||||
$schema = self::get_json_schema();
|
||||
return $schema['properties'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value based on the given type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported or the value is invalid.
|
||||
*/
|
||||
protected function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
if ( 'temperature' === $arg_name && ( (float) $value < 0.0 || (float) $value > 1.0 ) ) {
|
||||
throw new InvalidArgumentException( 'Temperature must be between 0.0 and 1.0.' );
|
||||
}
|
||||
|
||||
return parent::sanitize_arg( $value, $type, $arg_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'stopSequences' => array(
|
||||
'description' => __( 'Set of character sequences that will stop output generation.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
'responseMimeType' => array(
|
||||
'description' => __( 'MIME type of the generated candidate text.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'text/plain', 'application/json' ),
|
||||
),
|
||||
'responseSchema' => array(
|
||||
'description' => __( 'Output schema of the generated candidate text (only relevant if responseMimeType is application/json).', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
'candidateCount' => array(
|
||||
'description' => __( 'Number of response candidates to generate.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
),
|
||||
'maxOutputTokens' => array(
|
||||
'description' => __( 'The maximum number of tokens to include in a response candidate.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
),
|
||||
'temperature' => array(
|
||||
'description' => sprintf(
|
||||
/* translators: 1: Minimum value, 2: Maximum value */
|
||||
__( 'Floating point value to control the randomness of the output, between %1$s and %2$s.', 'ai-services' ),
|
||||
'0.0',
|
||||
'1.0'
|
||||
),
|
||||
'type' => 'number',
|
||||
'minimum' => 0.0,
|
||||
'maximum' => 1.0,
|
||||
),
|
||||
'topP' => array(
|
||||
'description' => __( 'The maximum cumulative probability of tokens to consider when sampling.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'topK' => array(
|
||||
'description' => __( 'The maximum number of tokens to consider when sampling.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'presencePenalty' => array(
|
||||
'description' => __( 'Presence penalty applied to the next token’s logprobs if the token has already been seen in the response.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'frequencyPenalty' => array(
|
||||
'description' => __( 'Frequency penalty applied to the next token’s logprobs, multiplied by the number of times each token has been seen in the response so far.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'responseLogprobs' => array(
|
||||
'description' => __( 'Whether to return log probabilities of the output tokens in the response or not.', 'ai-services' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'logprobs' => array(
|
||||
'description' => __( 'The number of top logprobs to return at each decoding step.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'outputModalities' => array(
|
||||
'description' => __( 'The modalities that the response can contain.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
Modality::TEXT,
|
||||
Modality::IMAGE,
|
||||
Modality::AUDIO,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tool_Config
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use ATFPP\AI_Translate\Services\Util\Strings;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing tool configuration for a generative AI model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Tool_Config implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $sanitized_args;
|
||||
|
||||
/**
|
||||
* Type definitions for the supported arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $supported_args = array(
|
||||
'functionCallMode' => 'string',
|
||||
'allowedFunctionNames' => 'array',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $args The configuration arguments.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$this->sanitized_args = $this->sanitize_args( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the function call mode.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function call mode, or empty string if not set.
|
||||
*/
|
||||
public function get_function_call_mode(): string {
|
||||
return $this->sanitized_args['functionCallMode'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed function names.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string[] The allowed function names, or empty array if not set.
|
||||
*/
|
||||
public function get_allowed_function_names(): array {
|
||||
return $this->sanitized_args['allowedFunctionNames'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return $this->sanitized_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tool_Config instance from an array of content data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Tool_Config Tool_Config instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Tool_Config {
|
||||
return new Tool_Config( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionCallMode' => array(
|
||||
'description' => __( 'Mode for how to consider function calling.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'auto', 'any' ),
|
||||
),
|
||||
'allowedFunctionNames' => array(
|
||||
'description' => __( 'List of function names allowed to call.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $args The arguments to sanitize.
|
||||
* @return array<string, mixed> Sanitized arguments.
|
||||
*/
|
||||
private function sanitize_args( array $args ): array {
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $args as $key => $value ) {
|
||||
if ( isset( $this->supported_args[ $key ] ) ) {
|
||||
$sanitized[ $key ] = $this->sanitize_arg( $value, $this->supported_args[ $key ], $key );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( str_contains( $key, '_' ) ) {
|
||||
$camelcase_key = Strings::snake_case_to_camel_case( $key );
|
||||
if ( isset( $this->supported_args[ $camelcase_key ] ) ) {
|
||||
$sanitized[ $camelcase_key ] = $this->sanitize_arg( $value, $this->supported_args[ $camelcase_key ], $camelcase_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizies the given value based on the given type.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported.
|
||||
*/
|
||||
private function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
if ( 'functionCallMode' === $arg_name && ! in_array( $value, array( 'auto', 'any' ), true ) ) {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'array':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( ! $value ) {
|
||||
return array();
|
||||
}
|
||||
return array( $value );
|
||||
}
|
||||
return array_values( $value );
|
||||
case 'string':
|
||||
return (string) $value;
|
||||
case 'object':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( is_object( $value ) ) {
|
||||
if ( $value instanceof Arrayable ) {
|
||||
return $value->to_array();
|
||||
}
|
||||
return (array) $value;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
return $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
default:
|
||||
throw new InvalidArgumentException( 'Unsupported type.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Web_Search_Tool;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of content tools for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Tools implements Collection, Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The tools.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var Tool[]
|
||||
*/
|
||||
private $tools = array();
|
||||
|
||||
/**
|
||||
* Adds a function declarations tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed>[] $function_declarations The function declarations.
|
||||
*/
|
||||
public function add_function_declarations_tool( array $function_declarations ): void {
|
||||
$this->add_tool(
|
||||
Function_Declarations_Tool::from_array(
|
||||
array( 'functionDeclarations' => $function_declarations )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a web search tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $allowed_domains Optional. The allowed domains. Default empty array.
|
||||
* @param string[] $disallowed_domains Optional. The disallowed domains. Default empty array.
|
||||
*/
|
||||
public function add_web_search_tool( array $allowed_domains = array(), array $disallowed_domains = array() ): void {
|
||||
$this->add_tool(
|
||||
Web_Search_Tool::from_array(
|
||||
array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => $allowed_domains,
|
||||
'disallowedDomains' => $disallowed_domains,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Tool $tool The tool.
|
||||
*/
|
||||
public function add_tool( Tool $tool ): void {
|
||||
$this->tools[] = $tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the tools collection.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return ArrayIterator<int, Tool> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->tools );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the tools collection.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->tools );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tool at the given index.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Tool The tool.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Tool {
|
||||
if ( ! isset( $this->tools[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->tools[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Tool $tool ) {
|
||||
return $tool->to_array();
|
||||
},
|
||||
$this->tools
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tools instance from an array of tools data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param mixed[] $data The tools data.
|
||||
* @return Tools The Tools instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tools data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Tools {
|
||||
$tools = new Tools();
|
||||
|
||||
foreach ( $data as $tool ) {
|
||||
if ( ! is_array( $tool ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid tool data.' );
|
||||
}
|
||||
|
||||
if ( isset( $tool['functionDeclarations'] ) ) {
|
||||
$tools->add_tool( Function_Declarations_Tool::from_array( $tool ) );
|
||||
} elseif ( isset( $tool['webSearch'] ) ) {
|
||||
$tools->add_tool( Web_Search_Tool::from_array( $tool ) );
|
||||
} else {
|
||||
throw new InvalidArgumentException( 'Invalid tool data.' );
|
||||
}
|
||||
}
|
||||
|
||||
return $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
$function_declarations_tool_schema = Function_Declarations_Tool::get_json_schema();
|
||||
unset( $function_declarations_tool_schema['type'] );
|
||||
|
||||
$web_search_tool_schema = Web_Search_Tool::get_json_schema();
|
||||
unset( $web_search_tool_schema['type'] );
|
||||
|
||||
return array(
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'oneOf' => array(
|
||||
$function_declarations_tool_schema,
|
||||
$web_search_tool_schema,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Abstract_Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class for a tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
abstract class Abstract_Tool implements Tool, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The tool data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final public function __construct() {
|
||||
// Empty constructor, only to prevent override.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
*/
|
||||
final public function set_data( array $data ): void {
|
||||
$this->data = $this->format_data( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
abstract protected function format_data( array $data ): array;
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
abstract protected function get_default_data(): array;
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
if ( ! $this->data ) {
|
||||
$this->data = $this->get_default_data();
|
||||
}
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a specific Tool instance from an array of tool data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return Tool The Tool instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tools data is invalid.
|
||||
*/
|
||||
final public static function from_array( array $data ): Tool {
|
||||
$tool = new static();
|
||||
$tool->set_data( $data );
|
||||
return $tool;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function declarations tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Declarations_Tool extends Abstract_Tool {
|
||||
|
||||
/**
|
||||
* Gets the function declarations from the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed>[] The function declarations.
|
||||
*/
|
||||
public function get_function_declarations(): array {
|
||||
return $this->to_array()['functionDeclarations'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionDeclarations'] ) || ! is_array( $data['functionDeclarations'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function declarations tool data must contain an array functionDeclarations value.' );
|
||||
}
|
||||
|
||||
foreach ( $data['functionDeclarations'] as &$function_declaration ) {
|
||||
if ( ! isset( $function_declaration['name'] ) || ! is_string( $function_declaration['name'] ) ) {
|
||||
throw new InvalidArgumentException( 'Each function declaration data must contain a string name value.' );
|
||||
}
|
||||
if ( isset( $function_declaration['description'] ) && ! is_string( $function_declaration['description'] ) ) {
|
||||
throw new InvalidArgumentException( 'The description value of a function declaration must be a string.' );
|
||||
}
|
||||
if ( isset( $function_declaration['parameters'] ) && ! is_array( $function_declaration['parameters'] ) ) {
|
||||
throw new InvalidArgumentException( 'The parameters value of a function declaration must be an object / associative array.' );
|
||||
}
|
||||
|
||||
$function_declaration['parameters'] = $this->sanitize_parameters( $function_declaration['parameters'] );
|
||||
}
|
||||
|
||||
return array(
|
||||
'functionDeclarations' => $data['functionDeclarations'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionDeclarations' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the parameters schema, ensuring every object property is required and additional properties are disallowed.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $schema The schema to sanitize.
|
||||
* @return array<string, mixed> Sanitized schema.
|
||||
*/
|
||||
protected function sanitize_parameters( array $schema ): array {
|
||||
// Every schema must have a type, but that will be checked elsewhere so we can ignore it here.
|
||||
if ( ! isset( $schema['type'] ) ) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$type = (array) $schema['type'];
|
||||
if ( in_array( 'object', $type, true ) ) {
|
||||
if ( isset( $schema['properties'] ) ) {
|
||||
$schema['required'] = array_keys( $schema['properties'] );
|
||||
foreach ( $schema['properties'] as $key => $child_schema ) {
|
||||
$schema['properties'][ $key ] = $this->sanitize_parameters( $child_schema );
|
||||
}
|
||||
}
|
||||
$schema['additionalProperties'] = false;
|
||||
}
|
||||
|
||||
if ( in_array( 'array', $type, true ) && isset( $schema['items'] ) ) {
|
||||
$schema['items'] = $this->sanitize_parameters( $schema['items'] );
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionDeclarations' => array(
|
||||
'description' => __( 'Function declarations for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'description' => array(
|
||||
'description' => __( 'Description of the function.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'parameters' => array(
|
||||
'description' => __( 'Supported parameters of the function, as an object in JSON schema.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Web_Search_Tool
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a web search tool for a generative model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Web_Search_Tool extends Abstract_Tool {
|
||||
|
||||
/**
|
||||
* Gets the allowed domains for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The allowed domains.
|
||||
*/
|
||||
public function get_allowed_domains(): array {
|
||||
return $this->to_array()['webSearch']['allowedDomains'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the disallowed domains for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The disallowed domains.
|
||||
*/
|
||||
public function get_disallowed_domains(): array {
|
||||
return $this->to_array()['webSearch']['disallowedDomains'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( isset( $data['webSearch']['allowedDomains'] ) && ! is_array( $data['webSearch']['allowedDomains'] ) ) {
|
||||
throw new InvalidArgumentException( 'The allowedDomains value for the web search tool data must be an array of strings.' );
|
||||
}
|
||||
if ( isset( $data['webSearch']['disallowedDomains'] ) && ! is_array( $data['webSearch']['disallowedDomains'] ) ) {
|
||||
throw new InvalidArgumentException( 'The disallowedDomains value for the web search tool data must be an array of strings.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => isset( $data['webSearch']['allowedDomains'] ) ? array_values( array_filter( $data['webSearch']['allowedDomains'], 'is_string' ) ) : array(),
|
||||
'disallowedDomains' => isset( $data['webSearch']['disallowedDomains'] ) ? array_values( array_filter( $data['webSearch']['disallowedDomains'], 'is_string' ) ) : array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => array(),
|
||||
'disallowedDomains' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'webSearch' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'allowedDomains' => array(
|
||||
'description' => __( 'Web search allowed domains for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'disallowedDomains' => array(
|
||||
'description' => __( 'Web search disallowed domains for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Authentication\API_Key_Authentication
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Authentication;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
|
||||
/**
|
||||
* Class that represents an API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class API_Key_Authentication implements Authentication {
|
||||
|
||||
/**
|
||||
* The API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $api_key;
|
||||
|
||||
/**
|
||||
* The HTTP header to use for the API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $header_name = 'Authorization';
|
||||
|
||||
/**
|
||||
* The authentication scheme to use for the API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $authencation_scheme = 'Bearer';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $api_key The API key.
|
||||
*/
|
||||
public function __construct( string $api_key ) {
|
||||
$this->api_key = $api_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the given request with the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance. Updated in place.
|
||||
*/
|
||||
public function authenticate( Request $request ): void {
|
||||
if ( 'authorization' === strtolower( $this->header_name ) ) {
|
||||
$request->add_header( $this->header_name, $this->authencation_scheme . ' ' . $this->api_key );
|
||||
} else {
|
||||
$request->add_header( $this->header_name, $this->api_key );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header name to use to add the credentials to a request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $header_name The header name.
|
||||
*/
|
||||
public function set_header_name( string $header_name ): void {
|
||||
$this->header_name = $header_name;
|
||||
}
|
||||
|
||||
public function set_authencation_scheme( string $authencation_scheme ): void {
|
||||
$this->authencation_scheme = $authencation_scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the option definitions needed to store the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug The service slug.
|
||||
* @return array<string, array<string, mixed>> The option definitions.
|
||||
*/
|
||||
public static function get_option_definitions( string $service_slug ): array {
|
||||
$option_slug = sprintf( 'Atfpp_Ai_Translate_%s_api_key', $service_slug );
|
||||
|
||||
return array(
|
||||
$option_slug => array(
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'show_in_rest' => true,
|
||||
'autoload' => true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_AI_Model
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Base class for an AI model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
abstract class Abstract_AI_Model implements Generative_AI_Model {
|
||||
|
||||
/**
|
||||
* The model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Model_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* The request options.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $request_options;
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
final public function get_model_slug(): string {
|
||||
return $this->get_model_metadata()->get_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Model_Metadata The model metadata.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the model metadata is not set.
|
||||
*/
|
||||
final public function get_model_metadata(): Model_Metadata {
|
||||
if ( ! $this->metadata instanceof Model_Metadata ) {
|
||||
throw new RuntimeException( 'Model metadata must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The request options.
|
||||
*/
|
||||
final protected function get_request_options(): array {
|
||||
return $this->request_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
*/
|
||||
final protected function set_model_metadata( Model_Metadata $metadata ): void {
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
*/
|
||||
final protected function set_request_options( array $request_options ): void {
|
||||
$this->request_options = $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_AI_Service
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Base class for an AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
abstract class Abstract_AI_Service implements Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
final public function get_service_slug(): string {
|
||||
return $this->get_service_metadata()->get_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the service metadata is not set.
|
||||
*/
|
||||
final public function get_service_metadata(): Service_Metadata {
|
||||
if ( ! $this->metadata instanceof Service_Metadata ) {
|
||||
throw new RuntimeException( 'Service metadata must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* In case of a cloud based service, this is typically used to check whether the current service credentials are
|
||||
* valid. For other service types, this may check other requirements, or simply return true.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the connection check cannot be performed.
|
||||
*/
|
||||
public function is_connected(): bool {
|
||||
if ( Service_Type::CLOUD !== $this->get_service_metadata()->get_type() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->list_models();
|
||||
return true;
|
||||
} catch ( Generative_AI_Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
* @throws Generative_AI_Exception Thrown if getting the model fails.
|
||||
*/
|
||||
final public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model {
|
||||
$models_metadata = $this->cached_list_models( $request_options );
|
||||
|
||||
if ( isset( $model_params['model'] ) ) {
|
||||
$model = $model_params['model'];
|
||||
unset( $model_params['model'] );
|
||||
} else {
|
||||
if ( isset( $model_params['capabilities'] ) ) {
|
||||
$model_slugs = AI_Capabilities::get_model_slugs_for_capabilities(
|
||||
$models_metadata,
|
||||
$model_params['capabilities']
|
||||
);
|
||||
} else {
|
||||
$model_slugs = array_keys( $models_metadata );
|
||||
}
|
||||
$model = $this->sort_models_by_preference( $model_slugs )[0];
|
||||
}
|
||||
|
||||
if ( ! isset( $models_metadata[ $model ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid model slug "%1$s" for the service "%2$s".',
|
||||
htmlspecialchars( $model ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $this->get_service_slug() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$model_metadata = $models_metadata[ $model ];
|
||||
|
||||
return $this->create_model_instance( $model_metadata, $model_params, $request_options );
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Creates a new model instance for the provided model metadata and parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $model_metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
|
||||
* a list of available parameters.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generative_AI_Model The new model instance.
|
||||
*/
|
||||
abstract protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model;
|
||||
|
||||
/**
|
||||
* Sorts model slugs by preference.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_slugs The model slugs to sort.
|
||||
* @return string[] The model slugs, sorted by preference.
|
||||
*/
|
||||
protected function sort_models_by_preference( array $model_slugs ): array {
|
||||
// By default, no sorting is applied.
|
||||
return $model_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
*/
|
||||
final protected function set_service_metadata( Service_Metadata $metadata ): void {
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata, wrapped in a transient cache.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
private function cached_list_models( array $request_options = array() ): array {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->list_models( $request_options );
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this, 'list_models' ),
|
||||
array( $request_options )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_Generation_Config
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Util\Strings;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class representing configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.2.0 Originally implemented as non-abstract class `Types\Generation_Config`.
|
||||
* @since 0.7.0
|
||||
*/
|
||||
abstract class Abstract_Generation_Config implements Generation_Config {
|
||||
|
||||
/**
|
||||
* The sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $sanitized_args;
|
||||
|
||||
/**
|
||||
* Any additional arguments, unsanitized.
|
||||
*
|
||||
* These are not used directly by the class, but are passed through to the API.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_args;
|
||||
|
||||
/**
|
||||
* Default values for the sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $defaults;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $args The configuration arguments.
|
||||
*/
|
||||
final public function __construct( array $args ) {
|
||||
$args_definition = $this->get_supported_args_definition();
|
||||
|
||||
$args = $this->sanitize_args( $args, $args_definition );
|
||||
|
||||
$this->sanitized_args = $args['sanitized'];
|
||||
$this->additional_args = $args['additional'];
|
||||
$this->defaults = $this->get_defaults( $args_definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given supported argument.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $name The argument name.
|
||||
* @return mixed The argument value, or its default value if not set.
|
||||
*/
|
||||
final public function get_arg( string $name ) {
|
||||
if ( ! isset( $this->sanitized_args[ $name ] ) ) {
|
||||
return $this->defaults[ $name ] ?? null;
|
||||
}
|
||||
|
||||
return $this->sanitized_args[ $name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all formally supported arguments.
|
||||
*
|
||||
* Only includes arguments that have an explicit value set, i.e. not defaults.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The arguments.
|
||||
*/
|
||||
final public function get_args(): array {
|
||||
return $this->sanitized_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The additional arguments.
|
||||
*/
|
||||
final public function get_additional_args(): array {
|
||||
return $this->additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
return $this->sanitized_args + $this->additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Generation_Config instance from an array of content data.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return static Generation_Config instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): static {
|
||||
return new static( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the definition for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The supported arguments definition.
|
||||
*/
|
||||
abstract protected function get_supported_args_definition(): array;
|
||||
|
||||
/**
|
||||
* Gets the default values for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args_definition The arguments definition.
|
||||
* @return array<string, mixed> The default values.
|
||||
*/
|
||||
private function get_defaults( array $args_definition ): array {
|
||||
$defaults = array();
|
||||
|
||||
foreach ( $args_definition as $key => $definition ) {
|
||||
if ( isset( $definition['default'] ) ) {
|
||||
$defaults[ $key ] = $definition['default'];
|
||||
} elseif ( isset( $definition['type'] ) ) {
|
||||
// Set default to type-safe value that is considered false-y.
|
||||
switch ( $definition['type'] ) {
|
||||
case 'array':
|
||||
$defaults[ $key ] = array();
|
||||
break;
|
||||
case 'string':
|
||||
$defaults[ $key ] = '';
|
||||
break;
|
||||
case 'object':
|
||||
$defaults[ $key ] = array();
|
||||
break;
|
||||
case 'integer':
|
||||
$defaults[ $key ] = 0;
|
||||
break;
|
||||
case 'number':
|
||||
case 'float':
|
||||
$defaults[ $key ] = 0.0;
|
||||
break;
|
||||
case 'boolean':
|
||||
$defaults[ $key ] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.7.0 The $args_definition parameter was added.
|
||||
*
|
||||
* @param array<string, mixed> $args The arguments to sanitize.
|
||||
* @param array<string, mixed> $args_definition The arguments definition.
|
||||
* @return array<string, array<string, mixed>> Associative array with keys 'sanitized' and 'additional', each
|
||||
* containing an array of arguments. The 'sanitized' array contains
|
||||
* the supported sanitized arguments, while the 'additional' array
|
||||
* contains any additional arguments that are not supported, but can
|
||||
* be passed through to the API.
|
||||
*/
|
||||
private function sanitize_args( array $args, array $args_definition ): array {
|
||||
$sanitized = array();
|
||||
$additional = array();
|
||||
|
||||
foreach ( $args as $key => $value ) {
|
||||
if ( isset( $args_definition[ $key ] ) ) {
|
||||
$sanitized[ $key ] = $this->sanitize_arg( $value, $args_definition[ $key ]['type'] ?? 'string', $key );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( str_contains( $key, '_' ) ) {
|
||||
$camelcase_key = Strings::snake_case_to_camel_case( $key );
|
||||
if ( isset( $args_definition[ $camelcase_key ] ) ) {
|
||||
$sanitized[ $camelcase_key ] = $this->sanitize_arg( $value, $args_definition[ $camelcase_key ]['type'] ?? 'string', $camelcase_key );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$additional[ $key ] = $value;
|
||||
}
|
||||
|
||||
return array(
|
||||
'sanitized' => $sanitized,
|
||||
'additional' => $additional,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value based on the given type.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported or the value is invalid.
|
||||
*/
|
||||
protected function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
switch ( $type ) {
|
||||
case 'array':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( ! $value ) {
|
||||
return array();
|
||||
}
|
||||
return array( $value );
|
||||
}
|
||||
return array_values( $value );
|
||||
case 'string':
|
||||
return (string) $value;
|
||||
case 'object':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( is_object( $value ) ) {
|
||||
if ( $value instanceof Arrayable ) {
|
||||
return $value->to_array();
|
||||
}
|
||||
return (array) $value;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
return $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
default:
|
||||
throw new InvalidArgumentException( 'Unsupported type.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Traits\Generative_AI_API_Client_Trait;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Get_Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Post_Request;
|
||||
|
||||
/**
|
||||
* Generic implementation of an AI API client, configured via constructor parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
class Generic_AI_API_Client implements Generative_AI_API_Client {
|
||||
use Generative_AI_API_Client_Trait;
|
||||
|
||||
/**
|
||||
* The base URL for the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $default_base_url;
|
||||
|
||||
/**
|
||||
* The API version.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $default_api_version;
|
||||
|
||||
/**
|
||||
* The (human-readable) API name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $api_name;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The authentication instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Authentication|null
|
||||
*/
|
||||
private $authentication;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $default_base_url The default base URL for the API.
|
||||
* @param string $default_api_version The default API version.
|
||||
* @param string $api_name The (human-readable) API name.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The authentication instance. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $default_base_url,
|
||||
string $default_api_version,
|
||||
string $api_name,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
$this->default_base_url = $default_base_url;
|
||||
$this->default_api_version = $default_api_version;
|
||||
$this->api_name = $api_name;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->authentication = $authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GET request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_get_request( string $path, array $params, array $request_options = array() ): Request {
|
||||
$request_url = $this->get_request_url( $path, $request_options );
|
||||
$request_options = $this->filter_request_options(
|
||||
$this->add_default_options( $request_options ),
|
||||
$request_url
|
||||
);
|
||||
|
||||
$request = new Get_Request(
|
||||
$request_url,
|
||||
$params,
|
||||
$request_options
|
||||
);
|
||||
$this->authenticate_request( $request );
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a POST request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_post_request( string $path, array $params, array $request_options = array() ): Request {
|
||||
$request_url = $this->get_request_url( $path, $request_options );
|
||||
$request_options = $this->filter_request_options(
|
||||
$this->add_default_options( $request_options ),
|
||||
$request_url
|
||||
);
|
||||
|
||||
$request = new JSON_Post_Request(
|
||||
$request_url,
|
||||
$params,
|
||||
$request_options
|
||||
);
|
||||
$this->authenticate_request( $request );
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URL for the specified model and task.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return string The request URL.
|
||||
*/
|
||||
protected function get_request_url( string $path, array $request_options = array() ): string {
|
||||
$base_url = $request_options['base_url'] ?? $this->default_base_url;
|
||||
$api_version = $request_options['api_version'] ?? $this->default_api_version;
|
||||
$path = ltrim( $path, '/' );
|
||||
|
||||
if ( isset( $request_options['stream'] ) && $request_options['stream'] && ! str_ends_with( $path, '?alt=sse' ) ) {
|
||||
$path .= '?alt=sse';
|
||||
}
|
||||
|
||||
if ( '' === $api_version ) {
|
||||
return "{$base_url}/{$path}";
|
||||
}
|
||||
|
||||
return "{$base_url}/{$api_version}/{$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional default request options to the given request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return array<string, mixed> The updated request options.
|
||||
*/
|
||||
protected function add_default_options( array $request_options ): array {
|
||||
if ( ! isset( $request_options['timeout'] ) ) {
|
||||
$request_options['timeout'] = 120;
|
||||
}
|
||||
return $request_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the request, if an authentication instance is set.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Request $request The request to authenticate.
|
||||
*/
|
||||
final protected function authenticate_request( Request $request ): void {
|
||||
if ( $this->authentication ) {
|
||||
$this->authentication->authenticate( $request );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human readable API name (without the "API" suffix).
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The API name.
|
||||
*/
|
||||
final protected function get_api_name(): string {
|
||||
return $this->api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request handler instance to use for requests.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Request_Handler The request handler instance.
|
||||
*/
|
||||
final protected function get_request_handler(): Request_Handler {
|
||||
return $this->request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the request options, with awareness of the request URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @param string $request_url The request URL.
|
||||
* @return array<string, mixed> The filtered request options.
|
||||
*/
|
||||
private function filter_request_options( array $request_options, string $request_url ): array {
|
||||
if ( isset( $request_options['timeout'] ) ) {
|
||||
$timeout = $request_options['timeout'];
|
||||
|
||||
/**
|
||||
* Filters the request timeout to use for an API request to an AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param int $timeout The request timeout in seconds.
|
||||
* @param string $request_url The request URL.
|
||||
*/
|
||||
$request_options['timeout'] = (int) apply_filters( 'ai_services_request_timeout', $timeout, $request_url );
|
||||
|
||||
// If the filtered timeout is invalid, use the original value.
|
||||
if ( $request_options['timeout'] <= 0 ) {
|
||||
$request_options['timeout'] = $timeout;
|
||||
}
|
||||
}
|
||||
|
||||
return $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\OpenAI_Compatible_AI_Text_Generation_Model
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidate;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_System_Instruction_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\Transformer;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Generic implementation of an OpenAI API compatible text generation AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
class OpenAI_Compatible_AI_Text_Generation_Model extends Abstract_AI_Model implements With_API_Client, With_Text_Generation {
|
||||
use With_API_Client_Trait;
|
||||
use With_Text_Generation_Trait;
|
||||
use Model_Param_Text_Generation_Config_Trait;
|
||||
use Model_Param_System_Instruction_Trait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The AI API client instance.
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
|
||||
* {@see OpenAI_AI_Service::get_model()} for the list of available
|
||||
* parameters. Default empty array.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
|
||||
$this->set_api_client( $api_client );
|
||||
$this->set_model_metadata( $metadata );
|
||||
|
||||
$this->set_text_generation_config_from_model_params( $model_params );
|
||||
$this->set_system_instruction_from_model_params( $model_params );
|
||||
|
||||
$this->set_request_options( $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final protected function send_generate_text_request( array $contents, array $request_options ): Candidates {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$params['model'] = $this->get_model_slug();
|
||||
|
||||
$request = $api->create_post_request(
|
||||
'chat/completions',
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_data(
|
||||
$response,
|
||||
function ( $response_data ) {
|
||||
return $this->get_response_candidates( $response_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$params['model'] = $this->get_model_slug();
|
||||
$params['stream'] = true;
|
||||
|
||||
$request = $api->create_post_request(
|
||||
'chat/completions',
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options,
|
||||
array( 'stream' => true )
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_stream(
|
||||
$response,
|
||||
function ( $response_data, $prev_chunk_candidates ) {
|
||||
return $this->get_response_candidates( $response_data, $prev_chunk_candidates );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*/
|
||||
protected function prepare_generate_text_params( array $contents ): array {
|
||||
if ( $this->get_system_instruction() ) {
|
||||
$contents = array_merge( array( $this->get_system_instruction() ), $contents );
|
||||
}
|
||||
|
||||
$transformers = $this->get_content_transformers();
|
||||
|
||||
$params = array(
|
||||
'messages' => array_map(
|
||||
static function ( Content $content ) use ( $transformers ) {
|
||||
return Transformer::transform_content( $content, $transformers );
|
||||
},
|
||||
$contents
|
||||
),
|
||||
);
|
||||
|
||||
$generation_config = $this->get_text_generation_config();
|
||||
if ( $generation_config ) {
|
||||
$params = Transformer::transform_generation_config_params(
|
||||
array_merge( $generation_config->get_additional_args(), $params ),
|
||||
$generation_config,
|
||||
$this->get_generation_config_transformers()
|
||||
);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the candidates with content from the response.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $response_data The response data.
|
||||
* @param ?Candidates $prev_chunk_candidates The candidates from the previous chunk in case of a streaming
|
||||
* response, or null.
|
||||
* @return Candidates The candidates with content parts.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response does not have any candidates with content.
|
||||
*/
|
||||
private function get_response_candidates( array $response_data, ?Candidates $prev_chunk_candidates = null ): Candidates {
|
||||
if ( null === $prev_chunk_candidates ) {
|
||||
// Regular (non-streaming) response, or first chunk of a streaming response.
|
||||
if ( ! isset( $response_data['choices'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'choices' );
|
||||
}
|
||||
|
||||
$other_data = $response_data;
|
||||
unset( $other_data['choices'] );
|
||||
|
||||
$candidates = new Candidates();
|
||||
foreach ( $response_data['choices'] as $index => $candidate_data ) {
|
||||
if ( isset( $candidate_data['delta'] ) && ! isset( $candidate_data['message'] ) ) {
|
||||
$candidate_data['message'] = $candidate_data['delta'];
|
||||
unset( $candidate_data['delta'] );
|
||||
}
|
||||
|
||||
$other_candidate_data = $candidate_data;
|
||||
unset( $other_candidate_data['message'] );
|
||||
|
||||
$candidates->add_candidate(
|
||||
new Candidate(
|
||||
$this->prepare_response_candidate_content( $candidate_data, $index ),
|
||||
array_merge( $other_candidate_data, $other_data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
// Subsequent chunk of a streaming response.
|
||||
$candidates_data = $this->merge_candidates_chunk(
|
||||
$prev_chunk_candidates->to_array(),
|
||||
$response_data
|
||||
);
|
||||
|
||||
return Candidates::from_array( $candidates_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a streaming response chunk with the previous candidates data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidates_data The candidates data from the previous chunk.
|
||||
* @param array<string, mixed> $chunk_data The response chunk data.
|
||||
* @return array<string, mixed> The merged candidates data.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function merge_candidates_chunk( array $candidates_data, array $chunk_data ): array {
|
||||
if ( ! isset( $chunk_data['choices'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'choices' );
|
||||
}
|
||||
|
||||
$other_data = $chunk_data;
|
||||
unset( $other_data['choices'] );
|
||||
|
||||
foreach ( $chunk_data['choices'] as $index => $candidate_data ) {
|
||||
if ( isset( $candidate_data['delta']['reasoning_content'] ) ) {
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $candidate_data['delta']['reasoning_content'];
|
||||
} elseif ( isset( $candidate_data['delta']['content'] ) ) {
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $candidate_data['delta']['content'];
|
||||
} else {
|
||||
// If there was a previous content block, ensure it is ends in a double newline.
|
||||
if (
|
||||
isset( $candidate_data['finish_reason'] ) &&
|
||||
'stop' === $candidate_data['finish_reason'] &&
|
||||
'' !== $candidates_data[ $index ]['content']['parts'][0]['text'] &&
|
||||
! str_ends_with( $candidates_data[ $index ]['content']['parts'][0]['text'], "\n\n" )
|
||||
) {
|
||||
$text_suffix = str_ends_with( $candidates_data[ $index ]['content']['parts'][0]['text'], "\n" ) ? "\n" : "\n\n";
|
||||
} else {
|
||||
$text_suffix = '';
|
||||
}
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $text_suffix;
|
||||
}
|
||||
unset( $candidate_data['delta'] );
|
||||
|
||||
$candidates_data[ $index ] = array_merge( $candidates_data[ $index ], $candidate_data, $other_data );
|
||||
}
|
||||
|
||||
return $candidates_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Content instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @param int $index The index of the candidate in the response.
|
||||
* @return Content The Content instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function prepare_response_candidate_content( array $candidate_data, int $index ): Content {
|
||||
if ( ! isset( $candidate_data['message'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( "choices.{$index}.message" );
|
||||
}
|
||||
|
||||
$role = isset( $candidate_data['message']['role'] ) && 'user' === $candidate_data['message']['role']
|
||||
? Content_Role::USER
|
||||
: Content_Role::MODEL;
|
||||
|
||||
$parts = $this->prepare_response_candidate_content_parts( $candidate_data );
|
||||
if ( count( $parts ) === 0 ) {
|
||||
throw $this->get_api_client()->create_response_exception(
|
||||
'Could not resolve content parts: The response includes unexpected content.'
|
||||
);
|
||||
}
|
||||
|
||||
return new Content(
|
||||
$role,
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Parts instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
|
||||
$parts = array();
|
||||
if ( isset( $candidate_data['message']['reasoning_content'] ) && is_string( $candidate_data['message']['reasoning_content'] ) ) {
|
||||
$parts[] = array( 'text' => $candidate_data['message']['reasoning_content'] );
|
||||
}
|
||||
if ( isset( $candidate_data['message']['content'] ) && is_string( $candidate_data['message']['content'] ) ) {
|
||||
$parts[] = array( 'text' => $candidate_data['message']['content'] );
|
||||
}
|
||||
return Parts::from_array( $parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
protected function get_content_transformers(): array {
|
||||
$api_client = $this->get_api_client();
|
||||
|
||||
return array(
|
||||
'role' => static function ( Content $content ) {
|
||||
if ( $content->get_role() === Content_Role::MODEL ) {
|
||||
return 'assistant';
|
||||
}
|
||||
if ( $content->get_role() === Content_Role::SYSTEM ) {
|
||||
return 'system';
|
||||
}
|
||||
return 'user';
|
||||
},
|
||||
'content' => static function ( Content $content ) use ( $api_client ) {
|
||||
$parts = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Text_Part ) {
|
||||
$parts[] = array(
|
||||
'type' => 'text',
|
||||
'text' => $part->get_text(),
|
||||
);
|
||||
} else {
|
||||
throw $api_client->create_bad_request_exception(
|
||||
'The API only supports text, image, and audio parts.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generation configuration transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The generation configuration transformers.
|
||||
*/
|
||||
protected function get_generation_config_transformers(): array {
|
||||
return array(
|
||||
'stop' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_stop_sequences();
|
||||
},
|
||||
'response_format' => static function ( Text_Generation_Config $config ) {
|
||||
if ( $config->get_response_mime_type() === 'application/json' ) {
|
||||
$schema = $config->get_response_schema();
|
||||
if ( $schema ) {
|
||||
return array(
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => $schema,
|
||||
);
|
||||
}
|
||||
return array( 'type' => 'json_object' );
|
||||
}
|
||||
return array();
|
||||
},
|
||||
'n' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_candidate_count();
|
||||
},
|
||||
'max_completion_tokens' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_max_output_tokens();
|
||||
},
|
||||
'temperature' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_temperature();
|
||||
},
|
||||
'top_p' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_top_p();
|
||||
},
|
||||
'presence_penalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_presence_penalty();
|
||||
},
|
||||
'frequency_penalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_frequency_penalty();
|
||||
},
|
||||
'logprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_response_logprobs();
|
||||
},
|
||||
'top_logprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_logprobs();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Cache\Service_Request_Cache
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Cache;
|
||||
|
||||
use Exception;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class that allows to wrap service method calls so that their return values are cached.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Service_Request_Cache {
|
||||
|
||||
/**
|
||||
* Wraps the given method call in a WordPress transient so that its return value is cached.
|
||||
*
|
||||
* The transient name is generated based on the method name and arguments. It is unique per service and method
|
||||
* name, so that different services can have methods with the same name that are cached separately. It also
|
||||
* includes a timestamp of when the service configuration was last changed, so that the cache is invalidated as
|
||||
* needed.
|
||||
*
|
||||
* If the method throws an exception, the exception is cached as well, so that it can be rethrown on subsequent
|
||||
* calls.
|
||||
*
|
||||
* The transient is stored for 24 hours.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return mixed Method return value, potentially served from cache.
|
||||
*
|
||||
* @throws Exception Rethrown original exception from the method call, if there was one.
|
||||
*/
|
||||
public static function wrap_transient( string $service_slug, callable $method, array $args = array() ) {
|
||||
$key = self::get_cache_key( $method, $args );
|
||||
$last_changed = self::get_last_changed( $service_slug );
|
||||
|
||||
$transient_name = "ATFPP:{$service_slug}:{$key}:{$last_changed}";
|
||||
|
||||
$value = get_transient( $transient_name );
|
||||
if ( false === $value ) {
|
||||
$value = self::call_method( $method, $args );
|
||||
set_transient( $transient_name, self::sanitize_value_for_cache( $value ), DAY_IN_SECONDS );
|
||||
} else {
|
||||
$value = self::parse_value_from_cache( $value );
|
||||
}
|
||||
if ( $value instanceof Exception ) {
|
||||
throw $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given method call in the WordPress object cache so that its return value is cached.
|
||||
*
|
||||
* The cache key is generated based on the method name and arguments. It is unique per service and method name,
|
||||
* so that different services can have methods with the same name that are cached separately. It also includes
|
||||
* a timestamp of when the service configuration was last changed, so that the cache is invalidated as needed.
|
||||
*
|
||||
* The service slug is used as the cache group.
|
||||
*
|
||||
* If the method throws an exception, the exception is cached as well, so that it can be rethrown on subsequent
|
||||
* calls.
|
||||
*
|
||||
* The cached value is stored for 24 hours.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return mixed Method return value, potentially served from cache.
|
||||
*
|
||||
* @throws Exception Rethrown original exception from the method call, if there was one.
|
||||
*/
|
||||
public static function wrap_cache( string $service_slug, callable $method, array $args = array() ) {
|
||||
$key = self::get_cache_key( $method, $args );
|
||||
$last_changed = self::get_last_changed( $service_slug );
|
||||
|
||||
$cache_name = "{$key}:{$last_changed}";
|
||||
|
||||
$value = wp_cache_get( $cache_name, $service_slug );
|
||||
if ( false === $value ) {
|
||||
$value = self::call_method( $method, $args );
|
||||
wp_cache_set( $cache_name, self::sanitize_value_for_cache( $value ), $service_slug, DAY_IN_SECONDS );
|
||||
} else {
|
||||
$value = self::parse_value_from_cache( $value );
|
||||
}
|
||||
if ( $value instanceof Exception ) {
|
||||
throw $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the caches for a service.
|
||||
*
|
||||
* This method should be called whenever the configuration of a service changes, so that the caches are invalidated
|
||||
* and the next request will fetch fresh data. This encompasses both transients and the object cache.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
*/
|
||||
public static function invalidate_caches( string $service_slug ): void {
|
||||
self::set_last_changed( $service_slug );
|
||||
|
||||
// Not strictly necessary, but if we can clean up, let's do so.
|
||||
if (
|
||||
function_exists( 'wp_cache_flush_group' ) &&
|
||||
function_exists( 'wp_cache_supports' ) &&
|
||||
wp_cache_supports( 'flush_group' )
|
||||
) {
|
||||
wp_cache_flush_group( $service_slug );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given method with the given arguments, catching any exceptions that are thrown.
|
||||
*
|
||||
* If an exception is thrown, it will be returned instead of the method's return value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param callable $method Method to call.
|
||||
* @param mixed[] $args Method arguments.
|
||||
* @return mixed Method return value or exception.
|
||||
*/
|
||||
private static function call_method( callable $method, array $args ) {
|
||||
try {
|
||||
return call_user_func_array( $method, $args );
|
||||
} catch ( Exception $e ) {
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value to be stored in the cache.
|
||||
*
|
||||
* If the value is an exception, it is converted to an array with the exception class name and message.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @return mixed Sanitized value.
|
||||
*/
|
||||
private static function sanitize_value_for_cache( $value ) {
|
||||
// Exception thrown.
|
||||
if ( is_object( $value ) && $value instanceof Exception ) {
|
||||
return array(
|
||||
'classname' => get_class( $value ),
|
||||
'message' => $value->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
// Arrayable class object.
|
||||
if ( is_object( $value ) && $value instanceof Arrayable && method_exists( get_class( $value ), 'from_array' ) ) {
|
||||
return array(
|
||||
'classname' => get_class( $value ),
|
||||
'data' => $value->to_array(),
|
||||
);
|
||||
}
|
||||
|
||||
// Array (recursion necessary).
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $key => $item ) {
|
||||
$value[ $key ] = self::sanitize_value_for_cache( $item );
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given value from the cache.
|
||||
*
|
||||
* This converts any sanitized exceptions back to their original exception form.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value Value from the cache.
|
||||
* @return mixed Parsed value.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the cached value uses an invalid class.
|
||||
*/
|
||||
private static function parse_value_from_cache( $value ) {
|
||||
// Exception thrown.
|
||||
if ( is_array( $value ) && isset( $value['classname'], $value['message'] ) ) {
|
||||
$class = $value['classname'];
|
||||
if ( ! class_exists( $class ) ) { // This should never be true, but a reasonable safeguard.
|
||||
$class = Exception::class;
|
||||
}
|
||||
$message = $value['message'];
|
||||
return new $class( $message );
|
||||
}
|
||||
|
||||
// Arrayable class object.
|
||||
if ( is_array( $value ) && isset( $value['classname'], $value['data'] ) ) {
|
||||
$class = $value['classname'];
|
||||
if ( ! class_exists( $class ) || ! method_exists( $class, 'from_array' ) ) { // This should never be true, but a reasonable safeguard.
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %s: class name */
|
||||
esc_html__( 'The class %s from the cached value does not exist or does not have a from_array method.', 'ai-services' ),
|
||||
esc_html( $class )
|
||||
)
|
||||
);
|
||||
}
|
||||
$data = $value['data'];
|
||||
return call_user_func( array( $class, 'from_array' ), $data );
|
||||
}
|
||||
|
||||
// Array (recursion necessary).
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $key => $item ) {
|
||||
$value[ $key ] = self::parse_value_from_cache( $item );
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache key for a method call.
|
||||
*
|
||||
* The returned key does not include the service slug, so the service slug has to be separately included as part of
|
||||
* the identifier for where to cache the value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return string Cache key.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the method is not a method on a service or model instance.
|
||||
*/
|
||||
private static function get_cache_key( callable $method, array $args = array() ): string {
|
||||
if ( ! is_array( $method ) || ! is_object( $method[0] ) || ! is_string( $method[1] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
esc_html__( 'Only methods on service and model instances can be cached.', 'ai-services' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $method[0] instanceof Generative_AI_Service ) {
|
||||
$type = 'service';
|
||||
} elseif ( $method[0] instanceof Generative_AI_Model ) {
|
||||
$type = 'model';
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
esc_html__( 'Only methods on service and model instances can be cached.', 'ai-services' )
|
||||
);
|
||||
}
|
||||
|
||||
return $type . ':' . self::get_cache_hash( $method[1], $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache hash for a method call.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $method_name Method name.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return string Cache hash.
|
||||
*/
|
||||
private static function get_cache_hash( string $method_name, array $args = array() ): string {
|
||||
$hash = $method_name;
|
||||
if ( ! empty( $args ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
$hash .= '_' . md5( serialize( $args ) );
|
||||
}
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last changed value for a service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @return string UNIX timestamp for when the configuration of the service was last changed.
|
||||
*/
|
||||
private static function get_last_changed( string $service_slug ): string {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
return wp_cache_get_last_changed( $service_slug );
|
||||
}
|
||||
|
||||
$last_changed_option = (array) get_option( 'ais_services_last_changed', array() );
|
||||
if ( ! isset( $last_changed_option[ $service_slug ] ) ) {
|
||||
$last_changed_option[ $service_slug ] = microtime();
|
||||
update_option( 'ais_services_last_changed', $last_changed_option );
|
||||
}
|
||||
return $last_changed_option[ $service_slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last changed value for a service to the current UNIX timestamp.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
*/
|
||||
private static function set_last_changed( string $service_slug ): void {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_set_last_changed( $service_slug );
|
||||
return;
|
||||
}
|
||||
|
||||
$last_changed_option = (array) get_option( 'ais_services_last_changed', array() );
|
||||
$last_changed_option[ $service_slug ] = microtime();
|
||||
update_option( 'ais_services_last_changed', $last_changed_option );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Authentication
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
|
||||
/**
|
||||
* Interface for a class representing authentication credentials of a certain kind for an API client.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Authentication {
|
||||
|
||||
/**
|
||||
* Authenticates the given request with the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance. Updated in place.
|
||||
*/
|
||||
public function authenticate( Request $request ): void;
|
||||
|
||||
/**
|
||||
* Sets the header name to use to add the credentials to a request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $header_name The header name.
|
||||
*/
|
||||
public function set_header_name( string $header_name ): void;
|
||||
|
||||
/**
|
||||
* Returns the option definitions needed to store the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug The service slug.
|
||||
* @return array<string, array<string, mixed>> The option definitions.
|
||||
*/
|
||||
public static function get_option_definitions( string $service_slug ): array;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generation_Config
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface Generation_Config extends Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* Returns the value for the given supported argument.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $name The argument name.
|
||||
* @return mixed The argument value, or its default value if not set.
|
||||
*/
|
||||
public function get_arg( string $name );
|
||||
|
||||
/**
|
||||
* Returns all formally supported arguments.
|
||||
*
|
||||
* Only includes arguments that have an explicit value set, i.e. not defaults.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The arguments.
|
||||
*/
|
||||
public function get_args(): array;
|
||||
|
||||
/**
|
||||
* Returns the additional arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The additional arguments.
|
||||
*/
|
||||
public function get_additional_args(): array;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a client for a generative AI web API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_API_Client {
|
||||
|
||||
/**
|
||||
* Creates a GET request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_get_request( string $path, array $params, array $request_options = array() ): Request;
|
||||
|
||||
/**
|
||||
* Creates a POST request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_post_request( string $path, array $params, array $request_options = array() ): Request;
|
||||
|
||||
/**
|
||||
* Sends the given request to the API and returns the response data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance.
|
||||
* @return Response The response instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while making the request.
|
||||
*/
|
||||
public function make_request( Request $request ): Response;
|
||||
|
||||
/**
|
||||
* Processes the response data from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data as associative array and should return the processed data in the desired
|
||||
* format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
public function process_response_data( Response $response, $process_callback );
|
||||
|
||||
/**
|
||||
* Processes the response body from the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response body. Receives the response body as
|
||||
* string and should return the processed data in the desired format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response body.
|
||||
*/
|
||||
public function process_response_body( Response $response, $process_callback );
|
||||
|
||||
/**
|
||||
* Processes the response data stream from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must implement With_Stream. The response data will
|
||||
* be processed in chunks, with each chunk of data being passed to the process
|
||||
* callback.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data (associative array) as first parameter, and the previous processed data
|
||||
* as second parameter (or null in case this is the first chunk). It should
|
||||
* return the processed data for the chunk in the desired format.
|
||||
* @return Generator Generator that yields the individual processed response data chunks.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
public function process_response_stream( Response $response, $process_callback ): Generator;
|
||||
|
||||
/**
|
||||
* Creates a new exception for a bad request, i.e. invalid or unsupported request data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return InvalidArgumentException The exception instance.
|
||||
*/
|
||||
public function create_bad_request_exception( string $message ): InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API request error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_request_exception( string $message ): Generative_AI_Exception;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_response_exception( string $message ): Generative_AI_Exception;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error for a missing key.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $key The missing key in the response data.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_missing_response_key_exception( string $key ): Generative_AI_Exception;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_Model {
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
public function get_model_slug(): string;
|
||||
|
||||
/**
|
||||
* Gets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Model_Metadata The model metadata.
|
||||
*/
|
||||
public function get_model_metadata(): Model_Metadata;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a generative AI service which provides access to models.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_service_slug(): string;
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_service_metadata(): Service_Metadata;
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* This is typically used to check whether the current service credentials are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*/
|
||||
public function is_connected(): bool;
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array;
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Support for the $tools and $toolConfig arguments was added.
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
* @throws Generative_AI_Exception Thrown if getting the model fails.
|
||||
*/
|
||||
public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_API_Client
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a service or model that uses an AI API client.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface With_API_Client {
|
||||
|
||||
/**
|
||||
* Gets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Generative_AI_API_Client The API client instance.
|
||||
*/
|
||||
public function get_api_client(): Generative_AI_API_Client;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Function_Calling
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which supports function calling.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
interface With_Function_Calling {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a class that provides a JSON schema for its input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
interface With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows multimodal input.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface With_Multimodal_Input {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output
|
||||
*
|
||||
* @since 0.6.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows multimodal output.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*/
|
||||
interface With_Multimodal_Output {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Text_Generation
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows generating text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface With_Text_Generation {
|
||||
|
||||
/**
|
||||
* Generates text content using the model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function generate_text( $content, array $request_options = array() ): Candidates;
|
||||
|
||||
/**
|
||||
* Generates text content using the model, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function stream_generate_text( $content, array $request_options = array() ): Generator;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Web_Search
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which supports web search.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface With_Web_Search {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Decorators\AI_Service_Decorator
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Decorators;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for an AI service that wraps another AI service through a decorator pattern.
|
||||
*
|
||||
* This class effectively acts as middleware for the underlying AI service, allowing for additional functionality to be
|
||||
* centrally provided.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class AI_Service_Decorator implements Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* The underlying AI service to wrap.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Generative_AI_Service
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Generative_AI_Service $service The underlying AI service to wrap.
|
||||
*/
|
||||
public function __construct( Generative_AI_Service $service ) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_service_slug(): string {
|
||||
return $this->service->get_service_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_service_metadata(): Service_Metadata {
|
||||
return $this->service->get_service_metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* This is typically used to check whether the current service credentials are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*/
|
||||
public function is_connected(): bool {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->service->is_connected();
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this->service, 'is_connected' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->service->list_models( $request_options );
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this->service, 'list_models' ),
|
||||
array( $request_options )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Support for the $tools and $toolConfig arguments was added.
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
*/
|
||||
public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model {
|
||||
if ( ! isset( $model_params['feature'] ) || ! preg_match( '/^[a-z0-9-]+$/', $model_params['feature'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'You must provide a "feature" identifier as part of the model parameters, which only contains lowercase letters, numbers, and hyphens.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the AI service model parameters before retrieving the model with them.
|
||||
*
|
||||
* This can be used, for example, to inject additional parameters via server-side logic based on the given
|
||||
* feature identifier.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters. Commonly supports at least the parameters
|
||||
* 'feature', 'capabilities', 'generationConfig' and
|
||||
* 'systemInstruction'.
|
||||
* @param string $service_slug The service slug.
|
||||
*
|
||||
* @return array<string, mixed> The processed model parameters.
|
||||
*/
|
||||
$filtered_model_params = (array) apply_filters( 'ai_services_model_params', $model_params, $this->service->get_service_slug() );
|
||||
|
||||
// Ensure that the feature identifier cannot be changed.
|
||||
$filtered_model_params['feature'] = $model_params['feature'];
|
||||
$model_params = $filtered_model_params;
|
||||
|
||||
// Perform basic validation so that the model classes don't have to.
|
||||
$this->validate_model_params( $model_params );
|
||||
|
||||
return $this->service->get_model( $model_params, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates various model parameters centrally.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
private function validate_model_params( array $model_params ): void {
|
||||
if (
|
||||
isset( $model_params['tools'] )
|
||||
&& ! $model_params['tools'] instanceof Tools
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The tools argument must be an instance of %s.',
|
||||
Tools::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
isset( $model_params['toolConfig'] )
|
||||
&& ! $model_params['toolConfig'] instanceof Tool_Config
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The tool config argument must be an instance of %s.',
|
||||
Tool_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
isset( $model_params['generationConfig'] )
|
||||
&& ! $model_params['generationConfig'] instanceof Generation_Config
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The generation config argument must be an instance of %s.',
|
||||
Generation_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $model_params['systemInstruction'] )
|
||||
&& ! is_string( $model_params['systemInstruction'] )
|
||||
&& ! $model_params['systemInstruction'] instanceof Parts
|
||||
&& ! $model_params['systemInstruction'] instanceof Content
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The system instruction argument must be either a string, or an instance of %1$s, or an instance of %2$s.',
|
||||
'Parts',
|
||||
'Content'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class for an exception thrown when a runtime error occurs in a generative AI service, e.g. a failing API request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Generative_AI_Exception extends RuntimeException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler
|
||||
*
|
||||
* @since 0.6.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Stream_Response;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
|
||||
/**
|
||||
* Interface for a request handler that can stream responses.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*/
|
||||
interface Stream_Request_Handler {
|
||||
|
||||
/**
|
||||
* Sends an HTTP request and streams the response.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param Request $request The request to send.
|
||||
* @return Stream_Response The stream response.
|
||||
*
|
||||
* @throws Request_Exception Thrown if the request fails.
|
||||
*/
|
||||
public function request_stream( Request $request ): Stream_Response;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP\Contracts;
|
||||
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Interface for a class that contains a readable stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
interface With_Stream {
|
||||
|
||||
/**
|
||||
* Returns a generator that reads individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The generator for the response stream.
|
||||
*/
|
||||
public function read_stream(): Generator;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\HTTP;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
/**
|
||||
* Extended HTTP class with support for streaming responses.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
final class HTTP_With_Streams extends HTTP implements Stream_Request_Handler {
|
||||
|
||||
/**
|
||||
* Guzzle client instance.
|
||||
*
|
||||
* Used for streaming requests, as WordPress Core's Requests API does not support this.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Client
|
||||
*/
|
||||
private $guzzle;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $default_options Optional. Default options to use for all requests. Default empty
|
||||
* array.
|
||||
*/
|
||||
public function __construct( array $default_options = array() ) {
|
||||
parent::__construct( $default_options );
|
||||
|
||||
$this->guzzle = new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request and streams the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Request $request The request to send.
|
||||
* @return Stream_Response The stream response.
|
||||
*
|
||||
* @throws Request_Exception Thrown if the request fails.
|
||||
*/
|
||||
public function request_stream( Request $request ): Stream_Response {
|
||||
$request_args = $this->build_request_args( $request );
|
||||
|
||||
$request_options = array(
|
||||
'allow_redirects' => $request_args['options']['redirection'] > 0 ? array( 'max' => $request_args['options']['redirection'] ) : false,
|
||||
'timeout' => (float) $request_args['options']['timeout'],
|
||||
'stream' => true,
|
||||
);
|
||||
if ( isset( $request_args['data'] ) ) {
|
||||
if ( in_array( $request_args['type'], array( Request::HEAD, Request::GET, Request::DELETE ), true ) ) {
|
||||
$request_options['query'] = $request_args['data'];
|
||||
} else {
|
||||
if ( ! is_string( $request_args['data'] ) ) {
|
||||
$request_args['data'] = http_build_query( $request_args['data'], '', '&' );
|
||||
}
|
||||
$request_options['body'] = $request_args['data'];
|
||||
}
|
||||
}
|
||||
if ( isset( $request_args['headers'] ) ) {
|
||||
if ( ! isset( $request_args['headers']['User-Agent'] ) ) {
|
||||
$request_args['headers']['User-Agent'] = $request_args['options']['user-agent'];
|
||||
}
|
||||
} else {
|
||||
$request_args['headers'] = array(
|
||||
'User-Agent' => $request_args['options']['user-agent'],
|
||||
);
|
||||
}
|
||||
$request_options['headers'] = $request_args['headers'];
|
||||
|
||||
try {
|
||||
$response = $this->guzzle->request(
|
||||
$request_args['type'],
|
||||
$request_args['url'],
|
||||
$request_options
|
||||
);
|
||||
} catch ( ClientException $e ) {
|
||||
throw new Request_Exception(
|
||||
htmlspecialchars( $e->getMessage() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
);
|
||||
}
|
||||
|
||||
$headers = $this->sanitize_headers( $response->getHeaders() );
|
||||
|
||||
return new Stream_Response( $response->getStatusCode(), $response->getBody(), $headers );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\HTTP\Stream_Response
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Generic_Response;
|
||||
use ATFPP\AI_Translate_Dependencies\Psr\Http\Message\StreamInterface;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* Class for a HTTP response that uses streaming.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @implements IteratorAggregate<Generator>
|
||||
*/
|
||||
class Stream_Response extends Generic_Response implements With_Stream, IteratorAggregate {
|
||||
|
||||
/**
|
||||
* The stream to read from.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var StreamInterface
|
||||
*/
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param int $status The HTTP status code received with the response.
|
||||
* @param StreamInterface $stream The response body stream to read from.
|
||||
* @param array<string, string> $headers The headers received with the response.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the $stream parameter has an invalid type.
|
||||
*/
|
||||
public function __construct( int $status, StreamInterface $stream, array $headers ) {
|
||||
parent::__construct( $status, '', $headers );
|
||||
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a generator that reads individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The generator for the response stream.
|
||||
*/
|
||||
public function read_stream(): Generator {
|
||||
while ( ! $this->stream->eof() ) {
|
||||
$line = $this->read_line( $this->stream );
|
||||
$data = json_decode( $line, true );
|
||||
if ( ! $data ) {
|
||||
continue;
|
||||
}
|
||||
yield $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an iterator reading individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The iterator for the response stream.
|
||||
*/
|
||||
public function getIterator(): Generator {
|
||||
return $this->read_stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a line from the stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param StreamInterface $stream The stream to read from.
|
||||
* @return string The line read from the stream.
|
||||
*/
|
||||
private function read_line( $stream ): string {
|
||||
$buffer = '';
|
||||
|
||||
while ( ! $stream->eof() ) {
|
||||
$buffer .= $stream->read( 1 );
|
||||
|
||||
if ( strlen( $buffer ) === 1 && '{' !== $buffer ) {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
if ( json_decode( $buffer ) !== null ) {
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
|
||||
return rtrim( $buffer, ']' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Options\Option_Encrypter
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Options;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Util\Data_Encryption;
|
||||
|
||||
/**
|
||||
* Class that allows for options to be encrypted when stored in the database as well as decrypted when retrieved.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Option_Encrypter {
|
||||
|
||||
const ENCRYPTION_PREFIX = 'enc::';
|
||||
|
||||
/**
|
||||
* The data encryption instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Data_Encryption
|
||||
*/
|
||||
private $data_encryption;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Data_Encryption $data_encryption The data encryption instance.
|
||||
*/
|
||||
public function __construct( Data_Encryption $data_encryption ) {
|
||||
$this->data_encryption = $data_encryption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relevant hooks to handle encryption and decryption of the given option.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $option_slug The option to use encryption with.
|
||||
*/
|
||||
public function add_encryption_hooks( string $option_slug ): void {
|
||||
add_filter( "sanitize_option_{$option_slug}", array( $this, 'encrypt_option' ), 9999, 2 ); // Encrypt late.
|
||||
add_filter( "option_{$option_slug}", array( $this, 'decrypt_option' ), -9999, 2 ); // Decrypt early.
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given option has encryption enabled.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return bool True if the option has encryption enabled, false otherwise.
|
||||
*/
|
||||
public function has_encryption( string $option_slug ): bool {
|
||||
return (
|
||||
has_filter( "sanitize_option_{$option_slug}", array( $this, 'encrypt_option' ) ) &&
|
||||
has_filter( "option_{$option_slug}", array( $this, 'decrypt_option' ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given option value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value The option value to encrypt.
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return string Encrypted option value.
|
||||
*/
|
||||
public function encrypt_option( $value, string $option_slug ): string {
|
||||
// Do not encrypt if the value is empty.
|
||||
if ( '' === $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Bail if the value is already encrypted.
|
||||
if ( is_string( $value ) && str_starts_with( $value, self::ENCRYPTION_PREFIX ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encrypted = $this->data_encryption->encrypt( maybe_serialize( $value ) );
|
||||
|
||||
// If encryption fails, trigger a warning but continue with the unencrypted value. Better not to lose data.
|
||||
if ( ! $encrypted ) {
|
||||
$this->trigger_error(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Option slug */
|
||||
__( 'Failed to encrypt the value for the option "%s".', 'ai-services' ),
|
||||
$option_slug
|
||||
)
|
||||
);
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::ENCRYPTION_PREFIX . $encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given option value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value The option value to decrypt.
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return mixed Decrypted option value.
|
||||
*/
|
||||
public function decrypt_option( $value, string $option_slug ) {
|
||||
// Bail if the value is already decrypted.
|
||||
if ( ! is_string( $value ) || ! str_starts_with( $value, self::ENCRYPTION_PREFIX ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$decrypted = $this->data_encryption->decrypt( substr( $value, strlen( self::ENCRYPTION_PREFIX ) ) );
|
||||
|
||||
// If decryption fails, trigger a warning and return an empty string.
|
||||
if ( ! $decrypted ) {
|
||||
$this->trigger_error(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Option slug */
|
||||
__( 'Failed to decrypt the value for the option "%s".', 'ai-services' ),
|
||||
$option_slug
|
||||
)
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
return maybe_unserialize( $decrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an error, if WP_DEBUG is enabled.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $function_name The name of the function that triggered the error.
|
||||
* @param string $message The message explaining the error.
|
||||
* @param int $error_level Optional. The designated error type for this error.
|
||||
* Only works with E_USER family of constants. Default E_USER_NOTICE.
|
||||
*/
|
||||
private function trigger_error( string $function_name, string $message, int $error_level = E_USER_NOTICE ): void {
|
||||
// The wp_trigger_error() function was only added in WordPress 6.4, so this is a minimal shim.
|
||||
if ( ! function_exists( 'wp_trigger_error' ) ) {
|
||||
if ( ! WP_DEBUG ) {
|
||||
return;
|
||||
}
|
||||
if ( ! empty( $function_name ) ) {
|
||||
$message = sprintf( '%s(): %s', $function_name, $message );
|
||||
}
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error( esc_html( $message ), $error_level );
|
||||
return;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
wp_trigger_error( $function_name, $message, $error_level );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Service_Registration
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Authentication\API_Key_Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Decorators\AI_Service_Decorator;
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class representing a service registration.
|
||||
*
|
||||
* This is an internal class and NOT the actual service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Service_Registration {
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Whether the service can be overridden through another registration with the same slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var bool
|
||||
*/
|
||||
private $allow_override;
|
||||
|
||||
/**
|
||||
* The service creator.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var callable
|
||||
*/
|
||||
private $creator;
|
||||
|
||||
/**
|
||||
* The service instance arguments.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $instance_args;
|
||||
|
||||
/**
|
||||
* The authentication option slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $authentication_option_slugs;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 The service argument keys were updated.
|
||||
*
|
||||
* @param string $slug The service slug. Must only contain lowercase letters, numbers, hyphens.
|
||||
* @param callable $creator The service creator. Receives the Service_Registration_Context as sole
|
||||
* parameter and must return a Generative_AI_Service instance. The parameter
|
||||
* provides access to the service metadata and other relevant dependencies.
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. The service arguments. Default empty array.
|
||||
*
|
||||
* @type string $name The service name. Default is the slug with spaces and uppercase
|
||||
* first letters.
|
||||
* @type string $credentials_url The URL to manage credentials for the service. Default empty
|
||||
* string.
|
||||
* @type string $type The service type. Default is Service_Type::CLOUD.
|
||||
* @type string[] $capabilities The list of AI capabilities supported by the service and its
|
||||
* models. Default empty array.
|
||||
* @type bool $allow_override Whether the service can be overridden by another service with
|
||||
* the same slug. Default true.
|
||||
* @type Request_Handler $request_handler The request handler instance. Default is a new HTTP_With_Streams
|
||||
* instance.
|
||||
* @type Container $container The container instance with data for the API key options.
|
||||
* Default is a new Option_Container instance.
|
||||
* @type Key_Value_Repository $repository The repository instance to read API keys Default is a new
|
||||
* Option_Repository instance.
|
||||
* }
|
||||
*/
|
||||
public function __construct( string $slug, callable $creator, array $args = array() ) {
|
||||
$this->metadata = Service_Metadata::from_array( array_merge( array( 'slug' => $slug ), $args ) );
|
||||
|
||||
$this->creator = $creator;
|
||||
$this->allow_override = isset( $args['allow_override'] ) ? (bool) $args['allow_override'] : true;
|
||||
$this->instance_args = $this->parse_instance_args( $args );
|
||||
|
||||
$option_definitions = array();
|
||||
if ( $this->metadata->get_type() === Service_Type::CLOUD ) {
|
||||
$option_definitions = API_Key_Authentication::get_option_definitions( $slug );
|
||||
}
|
||||
|
||||
$this->authentication_option_slugs = array();
|
||||
foreach ( $option_definitions as $option_slug => $option_args ) {
|
||||
$this->authentication_option_slugs[] = $option_slug;
|
||||
$this->instance_args['container'][ $option_slug ] = function () use ( $option_slug, $option_args ) {
|
||||
return new Option(
|
||||
$this->instance_args['repository'],
|
||||
$option_slug,
|
||||
$option_args
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_metadata(): Service_Metadata {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication option instances.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Option[] The authentication option instances.
|
||||
*/
|
||||
public function get_authentication_options(): array {
|
||||
return array_map(
|
||||
function ( string $option_slug ) {
|
||||
return $this->instance_args['container'][ $option_slug ];
|
||||
},
|
||||
$this->authentication_option_slugs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication option slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string[] The authentication option slugs.
|
||||
*/
|
||||
public function get_authentication_option_slugs(): array {
|
||||
return $this->authentication_option_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Generative_AI_Service The service instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if no API key is set for the service or if the service creator's return value is
|
||||
* not a valid Generative_AI_Service instance.
|
||||
*/
|
||||
public function create_instance(): Generative_AI_Service {
|
||||
$authentication_options = $this->get_authentication_options();
|
||||
|
||||
$slug = $this->metadata->get_slug();
|
||||
|
||||
$authentication = null;
|
||||
if ( count( $authentication_options ) > 0 ) {
|
||||
// For now an API key is the only authentication method supported.
|
||||
$api_key = $authentication_options[0]->get_value();
|
||||
if ( ! $api_key ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Cannot instantiate service %s without an API key.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$authentication = new API_Key_Authentication( $api_key );
|
||||
}
|
||||
|
||||
$context = new Service_Registration_Context(
|
||||
$slug,
|
||||
$this->metadata,
|
||||
$this->instance_args['request_handler'],
|
||||
$authentication
|
||||
);
|
||||
|
||||
$instance = ( $this->creator )( $context );
|
||||
if ( ! $instance instanceof Generative_AI_Service ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %s must return an instance of Generative_AI_Service.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $instance->get_service_slug() !== $slug ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %1$s must return an instance of Generative_AI_Service with the same slug, but instead it returned another slug %2$s.',
|
||||
htmlspecialchars( $slug ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $instance->get_service_slug() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $instance->get_service_metadata() !== $this->metadata ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %s must return an instance of Generative_AI_Service with the same metadata, but instead it returned different metadata.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the instance in a decorator for centralized functionality.
|
||||
return new AI_Service_Decorator( $instance );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service can be overridden.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return bool True if the service can be overridden, false otherwise.
|
||||
*/
|
||||
public function allows_override(): bool {
|
||||
return $this->allow_override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the service registration instance arguments.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service registration instance arguments.
|
||||
* @return array<string, mixed> The parsed service registration instance arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid instance argument is provided.
|
||||
*/
|
||||
private function parse_instance_args( array $args ): array {
|
||||
$requirements_map = array(
|
||||
'request_handler' => array( Request_Handler::class, HTTP_With_Streams::class ),
|
||||
'container' => array( Container::class, Option_Container::class ),
|
||||
'repository' => array( Key_Value_Repository::class, Option_Repository::class ),
|
||||
);
|
||||
|
||||
$instance_args = array();
|
||||
foreach ( $requirements_map as $key => $requirements ) {
|
||||
list( $interface_name, $class_name ) = $requirements;
|
||||
|
||||
if ( isset( $args[ $key ] ) ) {
|
||||
if ( ! $args[ $key ] instanceof $interface_name ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The %1$s argument must be an instance of %2$s.',
|
||||
htmlspecialchars( $key ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $interface_name ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$instance_args[ $key ] = $args[ $key ];
|
||||
} else {
|
||||
$instance_args[ $key ] = new $class_name();
|
||||
}
|
||||
}
|
||||
|
||||
return $instance_args;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Service_Registration_Context
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Value class with service context dependencies and data that can be used to create a service instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Registration_Context {
|
||||
|
||||
/**
|
||||
* The service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* The service request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The service authentication instance, if any.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Authentication|null
|
||||
*/
|
||||
private $authentication;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
* @param Request_Handler $request_handler The service request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The service authentication instance, if any. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $slug,
|
||||
Service_Metadata $metadata,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
$this->slug = $slug;
|
||||
$this->metadata = $metadata;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->authentication = $authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_metadata(): Service_Metadata {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Request_Handler The service request handler instance.
|
||||
*/
|
||||
public function get_request_handler(): Request_Handler {
|
||||
return $this->request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service authentication instance, if any.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Authentication|null The service authentication instance, if any.
|
||||
*/
|
||||
public function get_authentication(): ?Authentication {
|
||||
return $this->authentication;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_API
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Options\Option_Encrypter;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Main API class providing the entry point to the generative AI services.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_API {
|
||||
|
||||
/**
|
||||
* The service registration definitions, keyed by service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, Service_Registration>
|
||||
*/
|
||||
private $service_registrations = array();
|
||||
|
||||
/**
|
||||
* The service instances, keyed by service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, Generative_AI_Service>
|
||||
*/
|
||||
private $service_instances = array();
|
||||
|
||||
/**
|
||||
* The current user instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Current_User
|
||||
*/
|
||||
private $current_user;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The container instance with data for the API key options.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* The repository instance to read API keys.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Key_Value_Repository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* The option encrypter instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Option_Encrypter
|
||||
*/
|
||||
private $option_encrypter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 The constructor parameters were updated.
|
||||
*
|
||||
* @param Current_User $current_user The current user instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Container $container The container instance with data for the API key options.
|
||||
* @param Key_Value_Repository $repository The repository instance to read API keys.
|
||||
* @param Option_Encrypter|null $option_encrypter Optional. The option encrypter instance. If not provided, the
|
||||
* API key options are assumed to not be encrypted. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
Current_User $current_user,
|
||||
Request_Handler $request_handler,
|
||||
Container $container,
|
||||
Key_Value_Repository $repository,
|
||||
?Option_Encrypter $option_encrypter = null
|
||||
) {
|
||||
$this->current_user = $current_user;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->container = $container;
|
||||
$this->repository = $repository;
|
||||
$this->option_encrypter = $option_encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a generative AI service.
|
||||
*
|
||||
* An AI service consists at least of a service class that implements the Generative_AI_Service interface and a
|
||||
* model class that implements the Generative_AI_Model interface.
|
||||
*
|
||||
* Consumers of the service will access the service class through a proxy wrapper class which automatically handles
|
||||
* caching and other infrastructure concerns. It is therefore advised to not implement any caching concerns in the
|
||||
* service class itself as well as to not implement any public methods other than those required by the relevant
|
||||
* interfaces.
|
||||
*
|
||||
* The $creator parameter of this method needs to return the instance of the service class.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @see Generative_AI_Service
|
||||
*
|
||||
* @param string $slug The service slug. Must only contain lowercase letters, numbers, hyphens. It
|
||||
* must be unique and must match the service slug returned by the service
|
||||
* class.
|
||||
* @param callable $creator The service creator. Receives the Service_Registration_Context as sole
|
||||
* parameter and must return a Generative_AI_Service instance. The parameter
|
||||
* provides access to the service metadata and other relevant dependencies.
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. The service arguments. Default empty array.
|
||||
*
|
||||
* @type string $name The user-facing service name. Default is the slug with spaces and uppercase
|
||||
* first letters.
|
||||
* @type string $credentials_url The URL to manage credentials for the service. Default empty string.
|
||||
* @type bool $allow_override Whether the service can be overridden by another service with the same slug.
|
||||
* Default true.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an already registered slug or invalid arguments are provided.
|
||||
*/
|
||||
public function register_service( string $slug, callable $creator, array $args = array() ): void {
|
||||
if ( 'browser' === $slug ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is reserved for in-browser AI and cannot be registered.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $this->service_registrations[ $slug ] ) && ! $this->service_registrations[ $slug ]->allows_override() ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is already registered and cannot be overridden.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$args['request_handler'] = $this->request_handler;
|
||||
$args['container'] = $this->container;
|
||||
$args['repository'] = $this->repository;
|
||||
|
||||
$this->service_registrations[ $slug ] = new Service_Registration( $slug, $creator, $args );
|
||||
|
||||
$option_slugs = $this->service_registrations[ $slug ]->get_authentication_option_slugs();
|
||||
foreach ( $option_slugs as $option_slug ) {
|
||||
// Ensure the authentication options are encrypted.
|
||||
if ( null !== $this->option_encrypter && ! $this->option_encrypter->has_encryption( $option_slug ) ) {
|
||||
$this->option_encrypter->add_encryption_hooks( $option_slug );
|
||||
}
|
||||
|
||||
/*
|
||||
* If the repository uses WordPress options, ensure the authentication options are invalidated when the
|
||||
* credentials change.
|
||||
*/
|
||||
if ( $this->repository instanceof Option_Repository ) {
|
||||
$invalid_service_caches = static function () use ( $slug ) {
|
||||
Service_Request_Cache::invalidate_caches( $slug );
|
||||
};
|
||||
add_action( "add_option_{$option_slug}", $invalid_service_caches );
|
||||
add_action( "update_option_{$option_slug}", $invalid_service_caches );
|
||||
add_action( "delete_option_{$option_slug}", $invalid_service_caches );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a service is registered.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return bool True if the service is registered, false otherwise.
|
||||
*/
|
||||
public function is_service_registered( string $slug ): bool {
|
||||
return isset( $this->service_registrations[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a service is available.
|
||||
*
|
||||
* For a service to be considered available, all of the following conditions must be met:
|
||||
* - The service is registered.
|
||||
* - The service has an API key set.
|
||||
* - The API key is valid.
|
||||
* - The current user has the necessary capabilities.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return bool True if the service is available, false otherwise.
|
||||
*/
|
||||
public function is_service_available( string $slug ): bool {
|
||||
/*
|
||||
* If the service was already instantiated in the class, it is available.
|
||||
* In that case, the only thing left to check is whether the current user has the necessary capabilities.
|
||||
*/
|
||||
if ( isset( $this->service_instances[ $slug ] ) ) {
|
||||
if ( ! $this->current_user->has_cap( 'ais_access_service', $slug ) ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the service is not registered, it is not available.
|
||||
if ( ! isset( $this->service_registrations[ $slug ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any authentication credentials are missing for the service, it is not available.
|
||||
$authentication_options = $this->service_registrations[ $slug ]->get_authentication_options();
|
||||
foreach ( $authentication_options as $option ) {
|
||||
if ( ! $option->get_value() ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test whether the API key is valid by listing the models.
|
||||
$instance = $this->service_registrations[ $slug ]->create_instance();
|
||||
if ( ! $instance->is_connected() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If so, the service is available so we can store the instance.
|
||||
$this->service_instances[ $slug ] = $instance;
|
||||
|
||||
// Finally, check whether the current user has the necessary capabilities.
|
||||
return $this->current_user->has_cap( 'ais_access_service', $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any services are available.
|
||||
*
|
||||
* For some use-cases it may be acceptable to use any AI service. In those cases, this method can be used to check
|
||||
* whether any services are available. If so, an arbitrary available service can be retrieved using the
|
||||
* {@see Services_API::get_available_service()} method.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. Arguments to filter the services to consider. By default, any available service is considered.
|
||||
*
|
||||
* @type string[] $slugs List of service slugs, to only consider any of these services.
|
||||
* @type string[] $capabilities List of AI capabilities, to only consider services that support all of these
|
||||
* capabilities.
|
||||
* }
|
||||
* @return bool True if any of the services are available, false otherwise.
|
||||
*/
|
||||
public function has_available_services( array $args = array() ): bool {
|
||||
$slug = $this->get_available_service_slug( $args );
|
||||
return '' !== $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generative AI service instance that is available for use.
|
||||
*
|
||||
* If you intend to call this method with a specific service slug, you should first check whether the service is
|
||||
* available using {@see Services_API::is_service_available()}.
|
||||
*
|
||||
* If you intend to call this method to get any service (optionally with additional criteria to satisfy), you
|
||||
* should first check if any of the services are available using {@see Services_API::has_available_services()}.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|array<string, mixed> $args Optional. Either a single service slug to get that service, or
|
||||
* arguments to get any service that satisfies the criteria from these
|
||||
* arguments. See {@see Services_API::has_available_services()} for the
|
||||
* possible arguments. Default is an empty array so that any available
|
||||
* service is considered.
|
||||
* @return Generative_AI_Service The available service instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no service corresponding to the given arguments is available.
|
||||
*/
|
||||
public function get_available_service( $args = array() ): Generative_AI_Service {
|
||||
if ( is_string( $args ) ) {
|
||||
$slug = $args;
|
||||
if ( ! $this->is_service_available( $slug ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is either not registered or not available.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service_instances[ $slug ];
|
||||
}
|
||||
|
||||
$slug = $this->get_available_service_slug( $args );
|
||||
if ( '' === $slug ) {
|
||||
if ( count( $args ) > 0 ) {
|
||||
$message = 'No service satisfying the given arguments is registered and available.';
|
||||
} else {
|
||||
$message = 'No service is registered and available.';
|
||||
}
|
||||
throw new InvalidArgumentException( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $this->service_instances[ $slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata for a given service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return Service_Metadata|null The service metadata, or null if the service is not registered.
|
||||
*/
|
||||
public function get_service_metadata( string $slug ): ?Service_Metadata {
|
||||
if ( ! isset( $this->service_registrations[ $slug ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->service_registrations[ $slug ]->get_metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of all registered service slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string[] The list of registered service slugs.
|
||||
*/
|
||||
public function get_registered_service_slugs(): array {
|
||||
return array_keys( $this->service_registrations );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first available service slug, optionally satisfying the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args Optional. Arguments to filter the services to consider. See
|
||||
* {@see Services_API::has_available_services()} for the possible arguments.
|
||||
* By default, any available service is considered.
|
||||
* @return string The first available service slug, or empty string if no service is available.
|
||||
*/
|
||||
private function get_available_service_slug( array $args = array() ): string {
|
||||
$slugs = $args['slugs'] ?? $this->get_registered_service_slugs();
|
||||
|
||||
foreach ( $slugs as $slug ) {
|
||||
if ( ! $this->is_service_available( $slug ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
$metadata = $this->get_service_metadata( $slug );
|
||||
if ( ! $metadata ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing_capabilities = array_diff( $args['capabilities'], $metadata->get_capabilities() );
|
||||
if ( count( $missing_capabilities ) > 0 ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_API_Instance
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class to provide singleton-like access to the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_API_Instance {
|
||||
|
||||
/**
|
||||
* Retrieve the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Services_API|null The canonical Services_API instance.
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Retrieves the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Services_API The canonical Services_API instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the method is called too early when no instance has been set before.
|
||||
*/
|
||||
public static function get(): Services_API {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
throw new RuntimeException(
|
||||
'Cannot get Services_API instance before it was set.'
|
||||
);
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Services_API $instance The canonical Services_API instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the method is called after the instance has already been set.
|
||||
*/
|
||||
public static function set( Services_API $instance ): void {
|
||||
if ( isset( self::$instance ) ) {
|
||||
throw new RuntimeException(
|
||||
'Cannot set Services_API instance after it has already been set.'
|
||||
);
|
||||
}
|
||||
|
||||
self::$instance = $instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_Loader
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Controller;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Hooks;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
|
||||
/**
|
||||
* Loader class responsible for initializing the AI services functionality, including its public API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_Loader implements With_Hooks {
|
||||
|
||||
/**
|
||||
* Service container for the class's dependencies.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
*/
|
||||
public function __construct( string $main_file ) {
|
||||
$this->container = $this->set_up_container( $main_file );
|
||||
Services_API_Instance::set( $this->container['api'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relevant WordPress hooks.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function add_hooks(): void {
|
||||
$this->add_cleanup_hooks();
|
||||
$this->load_capabilities();
|
||||
$this->load_options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cleanup hooks related to plugin deactivation.
|
||||
*
|
||||
* @since 0.4.0
|
||||
*/
|
||||
private function add_cleanup_hooks(): void {
|
||||
// This function is only available in WordPress 6.4+.
|
||||
if ( ! function_exists( 'wp_set_options_autoload' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable autoloading of plugin options on deactivation.
|
||||
register_deactivation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// For network-wide deactivation, this cleanup cannot be reliably implemented.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Reinstate original autoload settings on (re-)activation.
|
||||
register_activation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// See deactivation hook for network-wide cleanup limitations.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the services capabilities and sets up the relevant filters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function load_capabilities(): void {
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
function () {
|
||||
$controller = $this->container['capability_controller'];
|
||||
|
||||
/**
|
||||
* Fires when the services capabilities are loaded.
|
||||
*
|
||||
* This hook allows you to modify the rules for how these capabilities are granted. The capabilities
|
||||
* available in the controller are:
|
||||
*
|
||||
* - 'ais_manage_services' (base capability)
|
||||
* - 'ais_access_services' (base capability)
|
||||
* - 'ais_access_service' (meta capability, called with the specific service slug as parameter)
|
||||
* - 'ais_use_playground' (meta capability)
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Capability_Controller $controller The capability controller, which can be used to modify the
|
||||
* rules for how capabilities are granted.
|
||||
*/
|
||||
do_action( 'ais_load_services_capabilities', $controller );
|
||||
|
||||
$this->container['capability_filters']->add_hooks();
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the services options.
|
||||
*
|
||||
* The option container is populated with options dynamically based on registered AI services. Each of the relevant
|
||||
* options will be registered here.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function load_options(): void {
|
||||
add_action(
|
||||
'init',
|
||||
function () {
|
||||
$registry = $this->container['option_registry'];
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
$option = $this->container['option_container']->get( $key );
|
||||
$registry->register(
|
||||
$option->get_key(),
|
||||
$option->get_registration_args()
|
||||
);
|
||||
}
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service option names that are autoloaded.
|
||||
*
|
||||
* @since 0.4.0
|
||||
*
|
||||
* @return string[] List of autoloaded service options.
|
||||
*/
|
||||
private function get_autoloaded_options(): array {
|
||||
$autoloaded_options = array();
|
||||
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
// Trigger option instantiation so that the autoload config is populated.
|
||||
$this->container['option_container']->get( $key );
|
||||
|
||||
$autoload = $this->container['option_repository']->get_autoload_config( $key );
|
||||
|
||||
if ( true === $autoload ) {
|
||||
$autoloaded_options[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $autoloaded_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the services service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return Service_Container The services service container.
|
||||
*/
|
||||
private function set_up_container( string $main_file ): Service_Container {
|
||||
$builder = new Services_Service_Container_Builder();
|
||||
|
||||
return $builder->build_env( $main_file )
|
||||
->build_services()
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_Service_Container_Builder
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use ATFPP\AI_Translate\Services\Options\Option_Encrypter;
|
||||
use ATFPP\AI_Translate\Services\Util\Data_Encryption;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Admin_Pages\Admin_Menu;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Base_Capability;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Controller;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Filters;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Meta_Capability;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Plugin_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Site_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Registry;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
|
||||
/**
|
||||
* Service container builder for the services loader.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_Service_Container_Builder {
|
||||
|
||||
/**
|
||||
* Service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->container = new Service_Container();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Service_Container Service container for the plugin.
|
||||
*/
|
||||
public function get(): Service_Container {
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin environment service for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_env( string $main_file ): self {
|
||||
$this->container['plugin_env'] = function () use ( $main_file ) {
|
||||
return new Plugin_Env( $main_file, ATFPP_V );
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_services(): self {
|
||||
$this->build_general_services();
|
||||
$this->build_capability_services();
|
||||
$this->build_http_services();
|
||||
$this->build_option_services();
|
||||
$this->build_entity_services();
|
||||
$this->build_admin_services();
|
||||
|
||||
$this->container['api'] = static function ( $cont ) {
|
||||
return new Services_API(
|
||||
$cont['current_user'],
|
||||
$cont['http'],
|
||||
$cont['option_container'],
|
||||
$cont['option_repository'],
|
||||
$cont['option_encrypter']
|
||||
);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the general services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_general_services(): void {
|
||||
$this->container['current_user'] = static function () {
|
||||
return new Current_User();
|
||||
};
|
||||
$this->container['site_env'] = static function () {
|
||||
return new Site_Env();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the capability services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_capability_services(): void {
|
||||
$this->container['capability_container'] = static function () {
|
||||
$capabilities = new Capability_Container();
|
||||
$capabilities['ais_manage_services'] = static function () {
|
||||
return new Base_Capability(
|
||||
'ais_manage_services',
|
||||
array( 'manage_options' )
|
||||
);
|
||||
};
|
||||
$capabilities['ais_access_services'] = static function () {
|
||||
return new Base_Capability(
|
||||
'ais_access_services',
|
||||
array( 'edit_posts' )
|
||||
);
|
||||
};
|
||||
$capabilities['ais_access_service'] = static function () {
|
||||
return new Meta_Capability(
|
||||
'ais_access_service',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
|
||||
static function ( int $user_id, string $service_slug ) {
|
||||
return array( 'ais_access_services' );
|
||||
}
|
||||
);
|
||||
};
|
||||
$capabilities['ais_use_playground'] = static function () {
|
||||
return new Meta_Capability(
|
||||
'ais_use_playground',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
|
||||
static function ( int $user_id ) {
|
||||
return array( 'ais_access_services' );
|
||||
}
|
||||
);
|
||||
};
|
||||
return $capabilities;
|
||||
};
|
||||
|
||||
$this->container['capability_controller'] = static function ( $cont ) {
|
||||
return new Capability_Controller( $cont['capability_container'] );
|
||||
};
|
||||
$this->container['capability_filters'] = static function ( $cont ) {
|
||||
return new Capability_Filters( $cont['capability_container'] );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the HTTP services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_http_services(): void {
|
||||
$this->container['http'] = static function () {
|
||||
// Custom implementation with additional support for streaming responses.
|
||||
return new HTTP_With_Streams();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the option services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_option_services(): void {
|
||||
$this->container['option_repository'] = static function () {
|
||||
return new Option_Repository();
|
||||
};
|
||||
$this->container['option_container'] = static function () {
|
||||
return new Option_Container();
|
||||
};
|
||||
$this->container['option_registry'] = static function () {
|
||||
return new Option_Registry( 'ais_services' );
|
||||
};
|
||||
$this->container['option_encrypter'] = static function () {
|
||||
return new Option_Encrypter( new Data_Encryption() );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the entity services for the service container.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
private function build_entity_services(): void {
|
||||
$this->container['user_meta_repository'] = static function () {
|
||||
return new Meta_Repository( 'user' );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the admin services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_admin_services(): void {
|
||||
$this->container['admin_settings_menu'] = static function () {
|
||||
return new Admin_Menu( 'options-general.php' );
|
||||
};
|
||||
$this->container['admin_tools_menu'] = static function () {
|
||||
return new Admin_Menu( 'tools.php' );
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Generative_AI_API_Client_Trait
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler;
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for an API client class which implements the Generative_AI_API_Client interface.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
trait Generative_AI_API_Client_Trait {
|
||||
|
||||
/**
|
||||
* Sends the given request to the API and returns the response data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance.
|
||||
* @return Response The response instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while making the request.
|
||||
*/
|
||||
final public function make_request( Request $request ): Response {
|
||||
$request_handler = $this->get_request_handler();
|
||||
|
||||
$options = $request->get_options();
|
||||
if ( isset( $options['stream'] ) && $options['stream'] ) {
|
||||
if ( ! $request_handler instanceof Stream_Request_Handler ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'Streaming requests are not supported by this API client.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $request_handler->request_stream( $request );
|
||||
} catch ( Request_Exception $e ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$response = $request_handler->request( $request );
|
||||
} catch ( Request_Exception $e ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $response->get_status() < 200 || $response->get_status() >= 300 ) {
|
||||
$data = $response->get_data();
|
||||
if ( $data && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
|
||||
$error_message = $data['error']['message'];
|
||||
} elseif ( $data && isset( $data['error'] ) && is_string( $data['error'] ) ) {
|
||||
$error_message = $data['error'];
|
||||
} elseif ( $data && isset( $data['message'] ) && is_string( $data['message'] ) ) {
|
||||
$error_message = $data['message'];
|
||||
} else {
|
||||
$error_message = sprintf(
|
||||
'Bad status code: %d',
|
||||
$response->get_status()
|
||||
);
|
||||
}
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $error_message );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response data from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data as associative array and should return the processed data in the desired
|
||||
* format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
final public function process_response_data( Response $response, $process_callback ) {
|
||||
if ( $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response must not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = $response->get_data();
|
||||
if ( ! $data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data received in response.'
|
||||
);
|
||||
}
|
||||
|
||||
$processed_data = call_user_func( $process_callback, $data );
|
||||
if ( ! $processed_data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data returned by process callback.'
|
||||
);
|
||||
}
|
||||
|
||||
return $processed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response body from the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response body. Receives the response body as
|
||||
* string and should return the processed data in the desired format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response body.
|
||||
*/
|
||||
final public function process_response_body( Response $response, $process_callback ) {
|
||||
if ( $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response must not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = $response->get_body();
|
||||
if ( ! $body ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No body received in response.'
|
||||
);
|
||||
}
|
||||
|
||||
$processed_data = call_user_func( $process_callback, $body );
|
||||
if ( ! $processed_data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data returned by process callback.'
|
||||
);
|
||||
}
|
||||
|
||||
return $processed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response data stream from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must implement With_Stream. The response data will
|
||||
* be processed in chunks, with each chunk of data being passed to the process
|
||||
* callback.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data (associative array) as first parameter, and the previous processed data
|
||||
* as second parameter (or null in case this is the first chunk). It should
|
||||
* return the processed data for the chunk in the desired format.
|
||||
* @return Generator Generator that yields the individual processed response data chunks.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
final public function process_response_stream( Response $response, $process_callback ): Generator {
|
||||
if ( ! $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response does not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$stream_generator = $response->read_stream();
|
||||
|
||||
$previous_processed_data = null;
|
||||
foreach ( $stream_generator as $data ) {
|
||||
$processed_data = call_user_func( $process_callback, $data, $previous_processed_data );
|
||||
if ( ! $processed_data ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$previous_processed_data = $processed_data;
|
||||
yield $processed_data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for a bad request, i.e. invalid or unsupported request data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return InvalidArgumentException The exception instance.
|
||||
*/
|
||||
final public function create_bad_request_exception( string $message ): InvalidArgumentException {
|
||||
return new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid request data for the %1$s API: %2$s',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API request error.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.3.0 Method made public.
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_request_exception( string $message ): Generative_AI_Exception {
|
||||
return new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Error while making request to the %1$s API: %2$s ',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_response_exception( string $message ): Generative_AI_Exception {
|
||||
return new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Error in the response from the %1$s API: %2$s ',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error for a missing key.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $key The missing key in the response data.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_missing_response_key_exception( string $key ): Generative_AI_Exception {
|
||||
return $this->create_response_exception(
|
||||
sprintf(
|
||||
'The response is missing the "%s" key.',
|
||||
$key
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request handler instance to use for requests.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 Renamed from `get_http()`.
|
||||
*
|
||||
* @return Request_Handler The request handler instance.
|
||||
*/
|
||||
abstract protected function get_request_handler(): Request_Handler;
|
||||
|
||||
/**
|
||||
* Returns the human readable API name (without the "API" suffix).
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The API name.
|
||||
*/
|
||||
abstract protected function get_api_name(): string;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_System_Instruction_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses a system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_System_Instruction_Trait {
|
||||
|
||||
/**
|
||||
* The system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Content|null
|
||||
*/
|
||||
private $system_instruction;
|
||||
|
||||
/**
|
||||
* Gets the system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Content|null The system instruction, or null if not set.
|
||||
*/
|
||||
final protected function get_system_instruction(): ?Content {
|
||||
return $this->system_instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content $system_instruction The system instruction.
|
||||
*/
|
||||
final protected function set_system_instruction( Content $system_instruction ): void {
|
||||
$this->system_instruction = $system_instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system instruction if provided in the `systemInstruction` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `systemInstruction` model parameter is invalid.
|
||||
*/
|
||||
protected function set_system_instruction_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['systemInstruction'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$model_params['systemInstruction'] = Formatter::format_system_instruction( $model_params['systemInstruction'] );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid systemInstruction model parameter: %s',
|
||||
htmlspecialchars( $e->getMessage() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_system_instruction( $model_params['systemInstruction'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Text_Generation_Config`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Text_Generation_Config_Trait {
|
||||
|
||||
/**
|
||||
* The text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Text_Generation_Config|null
|
||||
*/
|
||||
private $text_generation_config;
|
||||
|
||||
/**
|
||||
* Gets the text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Text_Generation_Config|null The text generation configuration, or null if not set.
|
||||
*/
|
||||
final protected function get_text_generation_config(): ?Text_Generation_Config {
|
||||
return $this->text_generation_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Text_Generation_Config $text_generation_config The text generation configuration.
|
||||
*/
|
||||
final protected function set_text_generation_config( Text_Generation_Config $text_generation_config ): void {
|
||||
$this->text_generation_config = $text_generation_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text generation configuration if provided in the `generationConfig` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `generationConfig` model parameter is invalid.
|
||||
*/
|
||||
protected function set_text_generation_config_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['generationConfig'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['generationConfig'] ) ) {
|
||||
$model_params['generationConfig'] = Text_Generation_Config::from_array( $model_params['generationConfig'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['generationConfig'] instanceof Text_Generation_Config ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid generationConfig model parameter: The value must be an array or an instance of %s.',
|
||||
Text_Generation_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_text_generation_config( $model_params['generationConfig'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Tool_Config_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Tool_Config`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Tool_Config_Trait {
|
||||
|
||||
/**
|
||||
* The tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Tool_Config|null
|
||||
*/
|
||||
private $tool_config;
|
||||
|
||||
/**
|
||||
* Gets the tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Tool_Config|null The tool configuration, or null if not set.
|
||||
*/
|
||||
final protected function get_tool_config(): ?Tool_Config {
|
||||
return $this->tool_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tool_Config $tool_config The tool configuration.
|
||||
*/
|
||||
final protected function set_tool_config( Tool_Config $tool_config ): void {
|
||||
$this->tool_config = $tool_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tool configuration if provided in the `toolConfig` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `toolConfig` model parameter is invalid.
|
||||
*/
|
||||
protected function set_tool_config_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['toolConfig'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['toolConfig'] ) ) {
|
||||
$model_params['toolConfig'] = Tool_Config::from_array( $model_params['toolConfig'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['toolConfig'] instanceof Tool_Config ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid toolConfig model parameter: The value must be an array or an instance of %s.',
|
||||
Tool_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_tool_config( $model_params['toolConfig'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Tools_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Tools`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Tools_Trait {
|
||||
|
||||
/**
|
||||
* The tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Tools|null
|
||||
*/
|
||||
private $tools;
|
||||
|
||||
/**
|
||||
* Gets the tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Tools|null The tools instance, or null if not set.
|
||||
*/
|
||||
final protected function get_tools(): ?Tools {
|
||||
return $this->tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tools $tools The tools instance.
|
||||
*/
|
||||
final protected function set_tools( Tools $tools ): void {
|
||||
$this->tools = $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tools instance if provided in the `tools` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `tools` model parameter is invalid.
|
||||
*/
|
||||
protected function set_tools_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['tools'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['tools'] ) ) {
|
||||
$model_params['tools'] = Tools::from_array( $model_params['tools'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['tools'] instanceof Tools ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid tools model parameter: The value must be an array or an instance of %s.',
|
||||
Tools::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_tools( $model_params['tools'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for an OpenAI compatible text generation model which implements function calling.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait {
|
||||
use Model_Param_Tool_Config_Trait;
|
||||
use Model_Param_Tools_Trait;
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid tool is provided.
|
||||
*/
|
||||
protected function prepare_generate_text_params( array $contents ): array {
|
||||
$params = parent::prepare_generate_text_params( $contents );
|
||||
|
||||
if ( $this->get_tools() ) {
|
||||
foreach ( $this->get_tools() as $tool ) {
|
||||
$prepared = $this->prepare_tool( $params, $tool );
|
||||
if ( ! $prepared ) {
|
||||
throw $this->get_api_client()->create_bad_request_exception(
|
||||
'Only function declarations tools are supported.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->get_tool_config() ) {
|
||||
$params['tool_choice'] = $this->prepare_tool_choice_param( $this->get_tool_config() );
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Parts instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
|
||||
$parts = parent::prepare_response_candidate_content_parts( $candidate_data );
|
||||
|
||||
if ( isset( $candidate_data['message']['tool_calls'] ) && is_array( $candidate_data['message']['tool_calls'] ) ) {
|
||||
foreach ( $candidate_data['message']['tool_calls'] as $tool_call ) {
|
||||
$prepared = $this->prepare_response_message_tool_call( $parts, $tool_call );
|
||||
if ( ! $prepared ) {
|
||||
throw $this->get_api_client()->create_response_exception(
|
||||
'The response includes a tool call of an unexpected type.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a given tool call from the response message, amending the provided Parts instance as needed.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Parts $parts The Parts instance to amend.
|
||||
* @param array<string, mixed> $tool_call_data The tool call data from the response message.
|
||||
* @return bool True if the tool call was successfully prepared, false otherwise.
|
||||
*/
|
||||
protected function prepare_response_message_tool_call( Parts $parts, array $tool_call_data ): bool {
|
||||
// Not all OpenAI compatible APIs include a 'type' key, so we only check its value if it is set.
|
||||
if (
|
||||
( isset( $tool_call_data['type'] ) && 'function' !== $tool_call_data['type'] ) ||
|
||||
! isset( $tool_call_data['function'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts->add_function_call_part(
|
||||
$tool_call_data['id'],
|
||||
$tool_call_data['function']['name'],
|
||||
is_string( $tool_call_data['function']['arguments'] )
|
||||
? json_decode( $tool_call_data['function']['arguments'], true )
|
||||
: $tool_call_data['function']['arguments']
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a single tool for the API request, amending the provided parameters as needed.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters to prepare the tools for. Passed by reference.
|
||||
* @param Tool $tool The tool to prepare.
|
||||
* @return bool True if the tool was successfully prepared, false otherwise.
|
||||
*/
|
||||
protected function prepare_tool( array &$params, Tool $tool ): bool {
|
||||
if ( ! $tool instanceof Function_Declarations_Tool ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$function_declarations = $tool->get_function_declarations();
|
||||
|
||||
if ( count( $function_declarations ) > 0 ) {
|
||||
if ( ! isset( $params['tools'] ) ) {
|
||||
$params['tools'] = array();
|
||||
}
|
||||
foreach ( $function_declarations as $declaration ) {
|
||||
$params['tools'][] = array(
|
||||
'type' => 'function',
|
||||
'function' => array_filter(
|
||||
array(
|
||||
'name' => $declaration['name'],
|
||||
'description' => $declaration['description'] ?? null,
|
||||
'parameters' => $declaration['parameters'] ?? null,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request tool choice parameter for the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tool_Config $tool_config The tool config to prepare the parameter with.
|
||||
* @return array<string, mixed> The tool config parameter value.
|
||||
*/
|
||||
private function prepare_tool_choice_param( Tool_Config $tool_config ): array {
|
||||
// Either 'auto' or 'any'.
|
||||
$tool_choice_param = $tool_config->get_function_call_mode() === 'any' ? 'required' : 'auto';
|
||||
|
||||
if ( 'required' === $tool_choice_param ) {
|
||||
// If one specific function must be called, the parameter needs to be an object, otherwise a string.
|
||||
$allowed_function_names = $tool_config->get_allowed_function_names();
|
||||
if ( count( $allowed_function_names ) === 1 ) {
|
||||
$tool_choice_param = array(
|
||||
'type' => 'function',
|
||||
'function' => array( 'name' => $allowed_function_names[0] ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tool_choice_param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
protected function get_content_transformers(): array {
|
||||
$api_client = $this->get_api_client();
|
||||
|
||||
$transformers = parent::get_content_transformers();
|
||||
|
||||
$orig_role_transformer = $transformers['role'];
|
||||
$orig_content_transformer = $transformers['content'];
|
||||
|
||||
$transformers['role'] = static function ( Content $content ) use ( $orig_role_transformer ) {
|
||||
// Special case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
return 'tool';
|
||||
}
|
||||
|
||||
return $orig_role_transformer( $content );
|
||||
};
|
||||
|
||||
$transformers['content'] = static function ( Content $content ) use ( $orig_content_transformer, $api_client ) {
|
||||
// Special case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
$response = $parts->get( 0 )->get_response();
|
||||
return json_encode( $response ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
}
|
||||
|
||||
$sanitized_parts = new Parts();
|
||||
foreach ( $parts as $part ) {
|
||||
/*
|
||||
* Special cases: Function call parts are handled as part of a separate `tool_calls` key, and
|
||||
* function response parts are are only supported as the only content of a message. They are
|
||||
* handled as a special case above.
|
||||
*/
|
||||
if ( $part instanceof Function_Response_Part ) {
|
||||
throw $api_client->create_bad_request_exception(
|
||||
'The API only allows a single function response, and it has to be the only content of the message.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $part instanceof Function_Call_Part ) {
|
||||
// Skip function call parts, they are handled in a separate `tool_calls` key.
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized_parts->add_part( $part );
|
||||
}
|
||||
$sanitized_content = new Content( $content->get_role(), $sanitized_parts );
|
||||
|
||||
return $orig_content_transformer( $sanitized_content );
|
||||
};
|
||||
|
||||
$transformers['tool_calls'] = static function ( Content $content ) {
|
||||
// Special key that only applies in case function calls are present.
|
||||
$tool_calls = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Function_Call_Part ) {
|
||||
$tool_calls[] = array(
|
||||
'type' => 'function',
|
||||
'id' => $part->get_id(),
|
||||
'function' => array(
|
||||
'name' => $part->get_name(),
|
||||
'arguments' => json_encode( $part->get_args() ), // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if ( count( $tool_calls ) > 0 ) {
|
||||
return $tool_calls;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$transformers['tool_call_id'] = static function ( Content $content ) {
|
||||
// Special key that only applies in case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
return $parts->get( 0 )->get_id();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return $transformers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Trait for a service or model which implements the With_API_Client interface.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait With_API_Client_Trait {
|
||||
|
||||
/**
|
||||
* The AI API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Generative_AI_API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* Gets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Generative_AI_API_Client The API client instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the API client is not set.
|
||||
*/
|
||||
final public function get_api_client(): Generative_AI_API_Client {
|
||||
if ( ! $this->api_client instanceof Generative_AI_API_Client ) {
|
||||
throw new RuntimeException( 'API client must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The API client instance.
|
||||
*/
|
||||
final protected function set_api_client( Generative_AI_API_Client $api_client ): void {
|
||||
$this->api_client = $api_client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model which implements the With_Text_Generation interface.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
trait With_Text_Generation_Trait {
|
||||
|
||||
/**
|
||||
* Generates text content using the model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final public function generate_text( $content, array $request_options = array() ): Candidates {
|
||||
$contents = $this->sanitize_new_content( $content );
|
||||
return $this->send_generate_text_request( $contents, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates text content using the model, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final public function stream_generate_text( $content, array $request_options = array() ): Generator {
|
||||
$contents = $this->sanitize_new_content( $content );
|
||||
return $this->send_stream_generate_text_request( $contents, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the input content for generating text.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content The input content.
|
||||
* @return Content[] The sanitized content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the input content is invalid.
|
||||
*/
|
||||
private function sanitize_new_content( $content ) {
|
||||
$capabilities = AI_Capabilities::get_model_instance_capabilities( $this );
|
||||
return Formatter::format_and_validate_new_contents( $content, $capabilities );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
abstract protected function send_generate_text_request( array $contents, array $request_options ): Candidates;
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
abstract protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\AI_Capabilities
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Web_Search;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class exposing the available AI capabilities and related static utility methods.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class AI_Capabilities {
|
||||
|
||||
/**
|
||||
* Gets the combined AI capabilities that the given model classes support.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_classes The model class names.
|
||||
* @return string[] The AI capabilities that the model classes support, based on the interfaces they implement.
|
||||
*/
|
||||
public static function get_model_classes_capabilities( array $model_classes ): array {
|
||||
$capabilities = array();
|
||||
foreach ( $model_classes as $model_class ) {
|
||||
$model_capabilities = self::get_model_class_capabilities( $model_class );
|
||||
foreach ( $model_capabilities as $capability ) {
|
||||
$capabilities[] = $capability;
|
||||
}
|
||||
}
|
||||
return array_unique( $capabilities );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AI capabilities that the given model class supports.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $model_class The model class name.
|
||||
* @return string[] The AI capabilities that the model class supports, based on the interfaces it implements.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
public static function get_model_class_capabilities( string $model_class ): array {
|
||||
$interfaces = class_implements( $model_class );
|
||||
|
||||
$capabilities = array();
|
||||
if ( isset( $interfaces[ With_Function_Calling::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::FUNCTION_CALLING;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Multimodal_Input::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Multimodal_Output::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Text_Generation::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::TEXT_GENERATION;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Web_Search::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::WEB_SEARCH;
|
||||
}
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AI capabilities that the given model instance supports.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Generative_AI_Model $model The model instance.
|
||||
* @return string[] The AI capabilities that the model instance supports, based on the interfaces it implements.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
public static function get_model_instance_capabilities( Generative_AI_Model $model ): array {
|
||||
$capabilities = array();
|
||||
if ( $model instanceof With_Function_Calling ) {
|
||||
$capabilities[] = AI_Capability::FUNCTION_CALLING;
|
||||
}
|
||||
if ( $model instanceof With_Multimodal_Input ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
|
||||
}
|
||||
if ( $model instanceof With_Multimodal_Output ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
if ( $model instanceof With_Text_Generation ) {
|
||||
$capabilities[] = AI_Capability::TEXT_GENERATION;
|
||||
}
|
||||
if ( $model instanceof With_Web_Search ) {
|
||||
$capabilities[] = AI_Capability::WEB_SEARCH;
|
||||
}
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model slugs that satisfy the given capabilities.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Now expects an array of model data shapes, mapped by model slug.
|
||||
* @since 0.7.0 Now expects a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, Model_Metadata> $models Metadata for each model, mapped by model slug.
|
||||
* @param string[] $capabilities The required capabilities that the models should satisfy.
|
||||
* @return string[] Slugs of all models that satisfy the given capabilities.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
|
||||
*/
|
||||
public static function get_model_slugs_for_capabilities( array $models, array $capabilities ): array {
|
||||
$model_slugs = array();
|
||||
foreach ( $models as $model_slug => $model_metadata ) {
|
||||
$model_capabilities = $model_metadata->get_capabilities();
|
||||
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
|
||||
$model_slugs[] = $model_slug;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $model_slugs ) {
|
||||
throw new InvalidArgumentException(
|
||||
'No model satisfies the given capabilities.'
|
||||
);
|
||||
}
|
||||
|
||||
return $model_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model class name from the given model class names that satisfies the given capabilities.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_classes The model class names.
|
||||
* @param string[] $capabilities The required capabilities that the models should satisfy.
|
||||
* @return string The model class name that satisfies the given capabilities.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
|
||||
*/
|
||||
public static function get_model_class_for_capabilities( array $model_classes, array $capabilities ): string {
|
||||
foreach ( $model_classes as $model_class ) {
|
||||
$model_capabilities = self::get_model_class_capabilities( $model_class );
|
||||
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
|
||||
return $model_class;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
'No model class satisfies the given capabilities.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Data_Encryption
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
/**
|
||||
* Class responsible for encrypting and decrypting data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @see https://felix-arntz.me/blog/storing-confidential-data-in-wordpress/
|
||||
*/
|
||||
final class Data_Encryption {
|
||||
|
||||
/**
|
||||
* Key to use for encryption.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* Salt to use for encryption.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $salt;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param ?string $key Optional. Key to use for encryption. If not passed, the default key determined by constants
|
||||
* will be used.
|
||||
* @param ?string $salt Optional. Salt to use for encryption. If not passed, the default salt determined by
|
||||
* constants will be used.
|
||||
*/
|
||||
public function __construct( ?string $key = null, ?string $salt = null ) {
|
||||
$this->key = $key ?? $this->get_default_key();
|
||||
$this->salt = $salt ?? $this->get_default_salt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a value.
|
||||
*
|
||||
* If a user-based key is set, that key is used. Otherwise the default key is used.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $value Value to encrypt.
|
||||
* @return string Encrypted value, or empty string on failure.
|
||||
*/
|
||||
public function encrypt( string $value ): string {
|
||||
if ( ! extension_loaded( 'openssl' ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$method = 'aes-256-ctr';
|
||||
$ivlen = openssl_cipher_iv_length( $method );
|
||||
$iv = openssl_random_pseudo_bytes( $ivlen );
|
||||
|
||||
$raw_value = openssl_encrypt( $value . $this->salt, $method, $this->key, 0, $iv );
|
||||
if ( ! $raw_value ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return base64_encode( $iv . $raw_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a value.
|
||||
*
|
||||
* If a user-based key is set, that key is used. Otherwise the default key is used.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $raw_value Value to decrypt.
|
||||
* @return string Decrypted value, or empty string on failure.
|
||||
*/
|
||||
public function decrypt( string $raw_value ): string {
|
||||
if ( ! extension_loaded( 'openssl' ) ) {
|
||||
return $raw_value;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$decoded_value = base64_decode( $raw_value, true );
|
||||
|
||||
if ( false === $decoded_value ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$method = 'aes-256-ctr';
|
||||
$ivlen = openssl_cipher_iv_length( $method );
|
||||
$iv = substr( $decoded_value, 0, $ivlen );
|
||||
|
||||
$decoded_value = substr( $decoded_value, $ivlen );
|
||||
|
||||
$value = openssl_decrypt( $decoded_value, $method, $this->key, 0, $iv );
|
||||
if ( ! $value || substr( $value, - strlen( $this->salt ) ) !== $this->salt ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr( $value, 0, - strlen( $this->salt ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default encryption key to use.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string Default (not user-based) encryption key.
|
||||
*/
|
||||
private function get_default_key(): string {
|
||||
if ( defined( 'AI_SERVICES_ENCRYPTION_KEY' ) && '' !== AI_SERVICES_ENCRYPTION_KEY ) {
|
||||
return AI_SERVICES_ENCRYPTION_KEY;
|
||||
}
|
||||
|
||||
if ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) {
|
||||
return LOGGED_IN_KEY;
|
||||
}
|
||||
|
||||
// If this is reached, you're either not on a live site or have a serious security issue.
|
||||
return 'test-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default encryption salt to use.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string Encryption salt.
|
||||
*/
|
||||
private function get_default_salt(): string {
|
||||
if ( defined( 'AI_SERVICES_ENCRYPTION_SALT' ) && '' !== AI_SERVICES_ENCRYPTION_SALT ) {
|
||||
return AI_SERVICES_ENCRYPTION_SALT;
|
||||
}
|
||||
|
||||
if ( defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT ) {
|
||||
return LOGGED_IN_SALT;
|
||||
}
|
||||
|
||||
// If this is reached, you're either not on a live site or have a serious security issue.
|
||||
return 'test-salt';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Formatter
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static methods for formatting content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Formatter {
|
||||
|
||||
/**
|
||||
* Formats and validates the various supported formats of a user prompt into a consistent list of Content instances.
|
||||
*
|
||||
* This method takes into account whether the provided content is supported by the given model, based on its capabilities.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content The content to format.
|
||||
* @param string[] $capabilities The AI capabilities that the model supports.
|
||||
* @return Content[] The formatted Content instances.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the content is invalid or the model does not support it.
|
||||
*/
|
||||
public static function format_and_validate_new_contents( $content, array $capabilities ): array {
|
||||
if ( is_array( $content ) ) {
|
||||
$contents = array_map(
|
||||
array( __CLASS__, 'format_new_content' ),
|
||||
$content
|
||||
);
|
||||
} else {
|
||||
$contents = array( self::format_new_content( $content ) );
|
||||
}
|
||||
|
||||
if ( count( $contents ) === 0 ) {
|
||||
throw new InvalidArgumentException(
|
||||
'No prompt was provided.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( Content_Role::USER !== $contents[0]->get_role() ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The first Content instance in the conversation or prompt must be user content.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! in_array( AI_Capability::CHAT_HISTORY, $capabilities, true ) && count( $contents ) > 1 ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The model does not support chat history. Only one content prompt must be provided.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! in_array( AI_Capability::MULTIMODAL_INPUT, $capabilities, true ) ) {
|
||||
// For performance reasons, only check the last content prompt, which likely is the only new one.
|
||||
$last_content = $contents[ count( $contents ) - 1 ];
|
||||
$last_parts = $last_content->get_parts();
|
||||
$last_parts_text_only = $last_parts->filter( array( 'class_name' => Text_Part::class ) );
|
||||
if ( count( $last_parts_text_only ) < count( $last_parts ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The model does not support multimodal input. Only text parts must be provided.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of new user content into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $content The content to format.
|
||||
* @return Content The formatted new content.
|
||||
*/
|
||||
public static function format_new_content( $content ): Content {
|
||||
return self::format_content( $content, Content_Role::USER );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of a system instruction into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $input The system instruction to format.
|
||||
* @return Content The formatted system instruction.
|
||||
*/
|
||||
public static function format_system_instruction( $input ): Content {
|
||||
return self::format_content( $input, Content_Role::SYSTEM );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of content into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $input The content to format.
|
||||
* @param string $role The role for the content.
|
||||
* @return Content The formatted content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the value is not a string, a Parts instance, or a Content instance.
|
||||
*/
|
||||
public static function format_content( $input, string $role ): Content {
|
||||
if ( is_string( $input ) ) {
|
||||
$parts = new Parts();
|
||||
$parts->add_text_part( $input );
|
||||
|
||||
return new Content( $role, $parts );
|
||||
}
|
||||
|
||||
if ( $input instanceof Parts ) {
|
||||
return new Content( $role, $input );
|
||||
}
|
||||
|
||||
if ( ! $input instanceof Content ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The value must be a string, a Parts instance, or a Content instance.'
|
||||
);
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Strings
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
/**
|
||||
* Class providing static methods for string operations.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Strings {
|
||||
|
||||
/**
|
||||
* Converts a snake_case string to a camelCase string.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $input The snake_case string.
|
||||
* @return string The camelCase string.
|
||||
*/
|
||||
public static function snake_case_to_camel_case( string $input ): string {
|
||||
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Transformer
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static methods for transforming data.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Transformer {
|
||||
|
||||
/**
|
||||
* Transforms the given content using the provided transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content $content The content to transform.
|
||||
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
|
||||
* the content as its only parameter and return the transformed value
|
||||
* for its key.
|
||||
* @return array<string, mixed> The transformed content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
|
||||
*/
|
||||
public static function transform_content( Content $content, array $transformers ): array {
|
||||
$data = array();
|
||||
|
||||
foreach ( $transformers as $key => $transformer ) {
|
||||
if ( ! is_callable( $transformer ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The transformer for key %s is invalid.',
|
||||
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Transform the value and set it if truthy.
|
||||
$value = $transformer( $content );
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
$data[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given Generation_Config instance into the given parameters using the provided transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters to merge the generation config into.
|
||||
* @param Generation_Config $config The generation config to use for the transformation.
|
||||
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
|
||||
* the generation config as its only parameter and return the
|
||||
* transformed value for its key.
|
||||
* @return array<string, mixed> The transformed parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
|
||||
*/
|
||||
public static function transform_generation_config_params( array $params, Generation_Config $config, array $transformers ): array {
|
||||
foreach ( $transformers as $key => $transformer ) {
|
||||
if ( ! is_callable( $transformer ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The transformer for key %s is invalid.',
|
||||
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Already set parameters take precedence.
|
||||
if ( isset( $params[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transform the value and set it if truthy.
|
||||
$value = $transformer( $config );
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* API functions.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
use ATFPP\AI_Translate\Services\Services_API;
|
||||
use ATFPP\AI_Translate\Services\Services_API_Instance;
|
||||
|
||||
/**
|
||||
* Returns the AI services API instance, which is used to interact with the AI services.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```
|
||||
* atfpp_ai_services()->get_available_service()
|
||||
* ->get_model( array( 'feature' => 'my-test-feature' ) )
|
||||
* ->generate_text( 'How can you help me?' )
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* atfpp_ai_services()->get_available_service( 'google' )
|
||||
* ->get_model(
|
||||
* array(
|
||||
* 'feature' => 'my-test-feature',
|
||||
* 'model' => 'gemini-1.5-pro',
|
||||
* )
|
||||
* )
|
||||
* ->generate_text( 'How can you help me?' )
|
||||
* ```
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Services_API The API instance.
|
||||
*/
|
||||
function atfpp_ai_services() {
|
||||
return Services_API_Instance::get();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"_yoast_wpseo_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_focuskw": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_metadesc": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_bctitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_opengraph-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_opengraph-description": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_twitter-title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_yoast_wpseo_twitter-description": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_focus_keyword": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_facebook_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_facebook_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_twitter_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_twitter_description": {
|
||||
"type": "string"
|
||||
},
|
||||
"rank_math_breadcrumb_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_titles_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_titles_desc": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_social_fb_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_social_fb_desc": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_social_twitter_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_social_twitter_desc": {
|
||||
"type": "string"
|
||||
},
|
||||
"_seopress_analysis_target_kw": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
if(!defined('ABSPATH')) exit;
|
||||
|
||||
if(!class_exists('ATFPP_Bulk_Translation')):
|
||||
class ATFPP_Bulk_Translation
|
||||
{
|
||||
private static $instance;
|
||||
|
||||
public static function get_instance()
|
||||
{
|
||||
if(!isset(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
add_action('current_screen', array($this, 'bulk_translate_btn'));
|
||||
}
|
||||
|
||||
public function bulk_translate_btn($screen)
|
||||
{
|
||||
global $polylang;
|
||||
|
||||
if(!$polylang || !property_exists($polylang, 'model')){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!class_exists('ATFPP_Helper') || !ATFPP_Helper::bulk_translation_render($screen)){
|
||||
return;
|
||||
}
|
||||
|
||||
$post_status=isset($_GET['post_status']) ? sanitize_text_field(wp_unslash($_GET['post_status'])) : '';
|
||||
|
||||
if('trash' === $post_status){
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( "views_{$screen->id}", array($this, 'atfpp_bulk_translate_button') );
|
||||
|
||||
add_action('admin_footer', array($this, 'bulk_translate_container'));
|
||||
}
|
||||
|
||||
public function atfpp_bulk_translate_button($views)
|
||||
{
|
||||
echo "<button class='button atfpp-bulk-translate-btn' style='display:none;'>Bulk Translate</button>";
|
||||
|
||||
return $views;
|
||||
}
|
||||
|
||||
public function bulk_translate_container()
|
||||
{
|
||||
echo "<div id='atfpp-bulk-translate-wrapper'></div>";
|
||||
}
|
||||
}
|
||||
endif;
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AutoPoly - AI Translation For Polylang (Pro)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Model for synchronizing posts
|
||||
*
|
||||
* @since 2.6
|
||||
*/
|
||||
class ATFP_Posts_Clone {
|
||||
/**
|
||||
* Stores the plugin options.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options;
|
||||
|
||||
/**
|
||||
* @var PLL_Model
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* @var PLL_Sync
|
||||
*/
|
||||
public $sync;
|
||||
|
||||
/**
|
||||
* @var PLL_Sync_Content
|
||||
*/
|
||||
public $sync_content;
|
||||
|
||||
/**
|
||||
* Stores temporary a synchronization information.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $temp_synchronized;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 2.6
|
||||
*
|
||||
* @param object $polylang Polylang object.
|
||||
*/
|
||||
public function __construct( &$polylang ) {
|
||||
$this->options = &$polylang->options;
|
||||
$this->model = &$polylang->model;
|
||||
$this->sync = &$polylang->sync;
|
||||
$this->sync_content = new ATFP_Sync_Content($polylang);
|
||||
|
||||
add_filter( 'pll_copy_taxonomies', array( $this, 'copy_taxonomies' ), 5, 4 );
|
||||
add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 5, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all taxonomies.
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string[] $taxonomies List of taxonomy names.
|
||||
* @param bool $sync True for a synchronization, false for a simple copy.
|
||||
* @param int $from Source post id.
|
||||
* @param int $to Target post id.
|
||||
* @return string[]
|
||||
*/
|
||||
public function copy_taxonomies( $taxonomies, $sync, $from, $to ) {
|
||||
if ( ! empty( $from ) && ! empty( $to ) && $this->are_synchronized( $from, $to ) ) {
|
||||
$taxonomies = array_diff( get_post_taxonomies( $from ), get_taxonomies( array( '_pll' => true ) ) );
|
||||
}
|
||||
return $taxonomies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all custom fields.
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param string[] $keys List of custom fields names.
|
||||
* @param bool $sync True if it is synchronization, false if it is a copy.
|
||||
* @param int $from Id of the post from which we copy the information.
|
||||
* @param int $to Id of the post to which we paste the information.
|
||||
* @return string[]
|
||||
*/
|
||||
public function copy_post_metas( $keys, $sync, $from, $to ) {
|
||||
if ( ! empty( $from ) && ! empty( $to ) && $this->are_synchronized( $from, $to ) ) {
|
||||
$from_keys = array_keys( get_post_custom( $from ) ); // *All* custom fields.
|
||||
$to_keys = array_keys( get_post_custom( $to ) ); // Adding custom fields of the destination allow to synchronize deleted custom fields.
|
||||
$keys = array_merge( $from_keys, $to_keys );
|
||||
$keys = array_unique( $keys );
|
||||
$keys = array_diff( $keys, array( '_edit_last', '_edit_lock' ) );
|
||||
}
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates the post to one language and optionally saves the synchronization group
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param int $post_id Post id of the source post.
|
||||
* @param string $source_language Source language slug.
|
||||
* @param string $target_language Target language slug.
|
||||
* @param bool $save_group True to update the synchronization group, false otherwise.
|
||||
* @return int Id of the target post, 0 on failure.
|
||||
*/
|
||||
public function copy_post( $post_id, $source_language, $target_language, $save_group = true, $post_data = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$tr_id = $this->model->post->get( $post_id, $this->model->get_language( $target_language ) );
|
||||
$tr_post = get_post( $post_id );
|
||||
$languages = array_keys( $this->get( $post_id ) );
|
||||
|
||||
if ( ! $tr_post instanceof WP_Post ) {
|
||||
// Something went wrong!
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach($tr_post as $key => $value){
|
||||
if(isset($post_data[$key]) && $key !== 'post_meta_fields'){
|
||||
$tr_post->$key = $post_data[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($tr_post->post_content)){
|
||||
$tr_post->post_content=ATFPP_Helper::replace_links_with_translations($tr_post->post_content, $target_language, $source_language);
|
||||
}
|
||||
|
||||
// If it does not exist, create it.
|
||||
if ( ! $tr_id ) {
|
||||
$tr_post->ID = 0;
|
||||
$tr_post->post_status = get_option('atfp_bulk_post_status', 'draft');
|
||||
|
||||
$tr_id = wp_insert_post( wp_slash( $tr_post->to_array() ) );
|
||||
$this->model->post->set_language( $tr_id, $target_language ); // Necessary to do it now to share slug.
|
||||
|
||||
$translations = $this->model->post->get_translations( $post_id );
|
||||
$translations[ $target_language ] = $tr_id;
|
||||
$this->model->post->save_translations( $post_id, $translations ); // Saves translations in case we created a post.
|
||||
|
||||
$languages[] = $target_language;
|
||||
|
||||
// Temporarily sync group, even if false === $save_group as we need synchronized posts to copy *all* taxonomies and post metas.
|
||||
$this->temp_synchronized[ $post_id ][ $tr_id ] = true;
|
||||
|
||||
// Maybe duplicates the featured image.
|
||||
if ( $this->options['media_support'] ) {
|
||||
add_filter( 'pll_translate_post_meta', array( $this->sync_content, 'duplicate_thumbnail' ), 10, 3 );
|
||||
}
|
||||
|
||||
add_filter( 'pll_maybe_translate_term', array( $this->sync_content, 'duplicate_term' ), 10, 3 );
|
||||
|
||||
$this->sync->taxonomies->copy( $post_id, $tr_id, $target_language );
|
||||
$this->sync->post_metas->copy( $post_id, $tr_id, $target_language );
|
||||
|
||||
$_POST['post_tr_lang'][ $target_language ] = $tr_id; // Hack to avoid creating multiple posts if the original post is saved several times (ex WooCommerce 3.0+).
|
||||
|
||||
/**
|
||||
* Fires after a synchronized post has been created
|
||||
*
|
||||
* @since 2.3.11
|
||||
*
|
||||
* @param int $post_id Id of the source post.
|
||||
* @param int $tr_id Id of the newly created post.
|
||||
* @param string $lang Language of the newly created post.
|
||||
*/
|
||||
do_action( 'pll_created_sync_post', $post_id, $tr_id, $target_language );
|
||||
|
||||
$post=get_post($post_id);
|
||||
do_action( 'pll_save_post', $post_id, $post, $translations ); // Fire again as we just updated $translations.
|
||||
|
||||
unset( $this->temp_synchronized[ $post_id ][ $tr_id ] );
|
||||
}
|
||||
|
||||
if ( $save_group ) {
|
||||
$this->save_group( $post_id, $languages );
|
||||
}
|
||||
|
||||
$tr_post->ID = $tr_id;
|
||||
$post=get_post($post_id);
|
||||
|
||||
$tr_post->post_parent = (int) $this->model->post->get( $post->post_parent, $target_language ); // Translates post parent.
|
||||
|
||||
$post = clone $tr_post;
|
||||
$post->ID=$post_id;
|
||||
|
||||
$tr_post = $this->sync_content->copy_content( $post, $tr_post, $target_language );
|
||||
|
||||
// The columns to copy in DB.
|
||||
$columns = array(
|
||||
'post_author',
|
||||
'post_date',
|
||||
'post_date_gmt',
|
||||
'post_content',
|
||||
'post_title',
|
||||
'post_excerpt',
|
||||
'comment_status',
|
||||
'ping_status',
|
||||
'post_name',
|
||||
'post_modified',
|
||||
'post_modified_gmt',
|
||||
'post_parent',
|
||||
'menu_order',
|
||||
'post_mime_type',
|
||||
);
|
||||
|
||||
$columns[] = 'post_status';
|
||||
|
||||
is_sticky( $post_id ) ? stick_post( $tr_id ) : unstick_post( $tr_id );
|
||||
|
||||
/**
|
||||
* Filters the post fields to synchronize when synchronizing posts
|
||||
*
|
||||
* @since 2.3
|
||||
*
|
||||
* @param array $fields WP_Post fields to synchronize.
|
||||
* @param int $post_id Post id of the source post.
|
||||
* @param string $lang Target language slug.
|
||||
* @param bool $save_group True to update the synchronization group, false otherwise.
|
||||
*/
|
||||
$columns = apply_filters( 'pll_sync_post_fields', array_combine( $columns, $columns ), $post_id, $target_language, $save_group );
|
||||
|
||||
$tr_post = array_intersect_key( (array) $tr_post, $columns );
|
||||
|
||||
$wpdb->update( $wpdb->posts, $tr_post, array( 'ID' => (int) $tr_id ) ); // Don't use wp_update_post to avoid conflict (reverse sync).
|
||||
clean_post_cache( $tr_id );
|
||||
|
||||
$post_meta_sync=true;
|
||||
|
||||
if (!isset(PLL()->options['sync']) || (isset(PLL()->options['sync']) && !in_array('post_meta', PLL()->options['sync']))) {
|
||||
$post_meta_sync = false;
|
||||
}
|
||||
|
||||
if(!$post_meta_sync && isset($post_data['post_meta_fields']) && count($post_data['post_meta_fields']) > 0){
|
||||
$this->update_post_custom_fields($post_data['post_meta_fields'], $tr_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after a post has been synchronized.
|
||||
*
|
||||
* @since 2.6.3
|
||||
*
|
||||
* @param int $post_id Id of the source post.
|
||||
* @param int $tr_id Id of the target post.
|
||||
* @param string $lang Language of the target post.
|
||||
* @param string $strategy `copy`.
|
||||
*/
|
||||
do_action( 'pll_post_synchronized', $post_id, $tr_id, $target_language, 'copy' );
|
||||
|
||||
// Update Elementor Translations
|
||||
$this->update_elementor_data($tr_id, $post_data, $post_id);
|
||||
|
||||
return $tr_id;
|
||||
}
|
||||
|
||||
private function update_post_custom_fields($fields, $post_id){
|
||||
$post_meta_sync = true;
|
||||
|
||||
if (!isset(PLL()->options['sync']) || (isset(PLL()->options['sync']) && !in_array('post_meta', PLL()->options['sync']))) {
|
||||
$post_meta_sync = false;
|
||||
}
|
||||
|
||||
if($post_meta_sync){
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed_meta_fields=ATFPP_Helper::get_allowed_custom_fields();
|
||||
|
||||
if($fields && is_array($fields) && count($fields) > 0){
|
||||
$valid_meta_fields=array_intersect(array_keys($fields), array_keys($allowed_meta_fields));
|
||||
if(count($valid_meta_fields) > 0){
|
||||
foreach($valid_meta_fields as $key){
|
||||
if(isset($allowed_meta_fields[$key]) && $allowed_meta_fields[$key]['status']){
|
||||
$value=is_array($fields[$key]) ? $this->sanitize_array_value($fields[$key], array()) : sanitize_text_field($fields[$key]);
|
||||
|
||||
update_post_meta(absint($post_id), sanitize_text_field($key), $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitize_array_value($value, $arr){
|
||||
foreach($value as $key => $item){
|
||||
$arr[sanitize_text_field($key)]=is_array($item) ? $this->sanitize_array_value($item, array()) : sanitize_text_field($item);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Elementor data
|
||||
*
|
||||
* @param int $tr_id The ID of the translated post.
|
||||
* @param string $elementor_data The Elementor data to update.
|
||||
* @return void
|
||||
*/
|
||||
private function update_elementor_data($tr_id, $post_data, $parent_post_id = 0){
|
||||
$current_post_elementor_data = get_post_meta($tr_id, '_elementor_data', true);
|
||||
|
||||
if(!isset($post_data['meta_fields']['_elementor_data'])){
|
||||
return;
|
||||
}
|
||||
|
||||
$elementor_data=$post_data['meta_fields']['_elementor_data'];
|
||||
|
||||
// Check if the current post has Elementor data
|
||||
if('' !== $current_post_elementor_data && $elementor_data && '' !== $elementor_data){
|
||||
if(class_exists('Elementor\Plugin')){
|
||||
$plugin=\Elementor\Plugin::$instance;
|
||||
$document=$plugin->documents->get($tr_id);
|
||||
|
||||
$document->save( [
|
||||
'elements' => json_decode($elementor_data, true),
|
||||
] );
|
||||
|
||||
$plugin->files_manager->clear_cache();
|
||||
}else{
|
||||
|
||||
if($parent_post_id > 0){
|
||||
$elementor_data=\Elementor\Plugin::$instance->documents->get($parent_post_id)->get_elements_data();
|
||||
$elementor_data=wp_json_encode($elementor_data);
|
||||
$elementor_data=preg_replace('#(?<!\\\\)/#', '\\/', $elementor_data);
|
||||
update_post_meta($tr_id, '_elementor_data', $elementor_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the synchronization group
|
||||
* This is stored as an array beside the translations in the post_translations term description
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param int $post_id ID of the post currently being saved.
|
||||
* @param array $sync_post Array of languages to sync with this post.
|
||||
* @return void
|
||||
*/
|
||||
public function save_group( $post_id, $sync_post ) {
|
||||
$term = $this->model->post->get_object_term( $post_id, 'post_translations' );
|
||||
|
||||
if ( empty( $term ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$d = maybe_unserialize( $term->description );
|
||||
$lang = $this->model->post->get_language( $post_id );
|
||||
|
||||
if ( ! is_array( $d ) || empty( $lang ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lang = $lang->slug;
|
||||
|
||||
if ( empty( $sync_post ) ) {
|
||||
if ( isset( $d['sync'][ $lang ] ) ) {
|
||||
$d['sync'] = array_diff( $d['sync'], array( $d['sync'][ $lang ] ) );
|
||||
}
|
||||
} else {
|
||||
$sync_post[] = $lang;
|
||||
$d['sync'] = empty( $d['sync'] ) ? array_fill_keys( $sync_post, $lang ) : array_merge( array_diff( $d['sync'], array( $lang ) ), array_fill_keys( $sync_post, $lang ) );
|
||||
}
|
||||
|
||||
wp_update_term( (int) $term->term_id, 'post_translations', array( 'description' => maybe_serialize( $d ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all posts synchronized with a given post
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param int $post_id The id of the post.
|
||||
* @return array An associative array of arrays with language code as key and post id as value.
|
||||
*/
|
||||
public function get( $post_id ) {
|
||||
$term = $this->model->post->get_object_term( $post_id, 'post_translations' );
|
||||
|
||||
if ( ! empty( $term ) ) {
|
||||
$lang = $this->model->post->get_language( $post_id );
|
||||
$d = maybe_unserialize( $term->description );
|
||||
|
||||
if ( ! is_array( $d ) || empty( $lang ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( ! empty( $d['sync'][ $lang->slug ] ) ) {
|
||||
$keys = array_keys( $d['sync'], $d['sync'][ $lang->slug ] );
|
||||
return array_intersect_key( $d, array_flip( $keys ) );
|
||||
}
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two posts are synchronized
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @param int $post_id The id of a first post to compare.
|
||||
* @param int $other_id The id of the other post to compare.
|
||||
* @return bool
|
||||
*/
|
||||
public function are_synchronized( $post_id, $other_id ) {
|
||||
return isset( $this->temp_synchronized[ $post_id ][ $other_id ] ) || in_array( $other_id, $this->get( $post_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can synchronize a post in other language
|
||||
*
|
||||
* @since 2.6
|
||||
*
|
||||
* @param int $post_id Post to synchronize.
|
||||
* @param string $lang Language code.
|
||||
* @return bool
|
||||
*/
|
||||
public function current_user_can_synchronize( $post_id, $lang ) {
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tr_id = $this->model->post->get( $post_id, $this->model->get_language( $lang ) );
|
||||
|
||||
// If we don't have a translation yet, check if we have the right to create a new one?
|
||||
if ( empty( $tr_id ) ) {
|
||||
$post_type = get_post_type( $post_id );
|
||||
$post_type_object = get_post_type_object( $post_type );
|
||||
return current_user_can( $post_type_object->cap->create_posts );
|
||||
}
|
||||
|
||||
// Do we have the right to edit this translation?
|
||||
if ( ! current_user_can( 'edit_post', $tr_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is this translation synchronized with a post that we can't edit?
|
||||
$ids = $this->get( $tr_id );
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
if ( ! current_user_can( 'edit_post', $id ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AutoPoly - AI Translation For Polylang (Pro)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Smart copy of post content
|
||||
*
|
||||
* @since 2.6
|
||||
*/
|
||||
class ATFP_Sync_Content {
|
||||
/**
|
||||
* Stores the plugin options.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* @var PLL_Model
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Instance of a child class of PLL_Links_Model.
|
||||
*
|
||||
* @var PLL_Links_Model
|
||||
*/
|
||||
protected $links_model;
|
||||
|
||||
/**
|
||||
* @var PLL_CRUD_Posts
|
||||
*/
|
||||
protected $posts;
|
||||
|
||||
/**
|
||||
* The post object to fill with translated data.
|
||||
*
|
||||
* @var WP_Post
|
||||
*/
|
||||
protected $target_post;
|
||||
|
||||
/**
|
||||
* Language of the target post.
|
||||
*
|
||||
* @var PLL_Language
|
||||
*/
|
||||
protected $target_language;
|
||||
|
||||
/**
|
||||
* Language of the source post.
|
||||
*
|
||||
* @var PLL_Language
|
||||
*/
|
||||
protected $from_language;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.9
|
||||
*
|
||||
* @param PLL_Frontend|PLL_Admin|PLL_Settings|PLL_REST_Request $polylang Polylang object.
|
||||
*/
|
||||
public function __construct( &$polylang ) {
|
||||
$this->options = &$polylang->options;
|
||||
$this->model = &$polylang->model;
|
||||
$this->posts = &$polylang->posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the content from one post to the other
|
||||
*
|
||||
* @since 1.9
|
||||
*
|
||||
* @param WP_Post $from_post The post to copy from.
|
||||
* @param WP_Post $target_post The post to copy to.
|
||||
* @param PLL_Language|string $target_language The language of the post to copy to.
|
||||
* @return WP_Post|void
|
||||
*/
|
||||
public function copy_content( $from_post, $target_post, $target_language ) {
|
||||
$from_language = $this->model->post->get_language( $from_post->ID );
|
||||
$target_language = $this->model->get_language( $target_language );
|
||||
|
||||
if ( ! $from_language || ! $target_language ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_post->post_title = $from_post->post_title;
|
||||
$target_post->post_name = wp_unique_post_slug(
|
||||
$from_post->post_name,
|
||||
$target_post->ID,
|
||||
$target_post->post_status,
|
||||
$target_post->post_type,
|
||||
$target_post->post_parent
|
||||
);
|
||||
$target_post->post_excerpt = $this->translate_content(
|
||||
$from_post->post_excerpt,
|
||||
$target_post,
|
||||
$from_language,
|
||||
$target_language
|
||||
);
|
||||
$target_post->post_content = $this->translate_content(
|
||||
$from_post->post_content,
|
||||
$target_post,
|
||||
$from_language,
|
||||
$target_language
|
||||
);
|
||||
|
||||
return $target_post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate shortcodes and <img> attributes in a given text
|
||||
*
|
||||
* @since 1.9
|
||||
* @since 3.3 Requires $target_post, $from_language and $target_language parameters.
|
||||
* @global array $shortcode_tags
|
||||
*
|
||||
* @param string $content Text to translate.
|
||||
* @param WP_Post $target_post The post object to populate with translated content.
|
||||
* @param PLL_Language $from_language The source language .
|
||||
* @param PLL_Language $target_language The language to translate to.
|
||||
* @return string Translated text
|
||||
*/
|
||||
public function translate_content( $content, $target_post, PLL_Language $from_language, PLL_Language $target_language ) {
|
||||
global $shortcode_tags;
|
||||
|
||||
$this->target_post = $target_post;
|
||||
$this->from_language = $from_language;
|
||||
$this->target_language = $target_language;
|
||||
|
||||
// Hack shortcodes.
|
||||
$backup = $shortcode_tags;
|
||||
$shortcode_tags = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
// Add our own shorcode actions.
|
||||
if ( $this->options['media_support'] ) {
|
||||
add_shortcode( 'gallery', array( $this, 'ids_list_shortcode' ) );
|
||||
add_shortcode( 'playlist', array( $this, 'ids_list_shortcode' ) );
|
||||
add_shortcode( 'caption', array( $this, 'caption_shortcode' ) );
|
||||
add_shortcode( 'wp_caption', array( $this, 'caption_shortcode' ) );
|
||||
}
|
||||
|
||||
if ( has_blocks( $content ) ) {
|
||||
$blocks = parse_blocks( $content );
|
||||
$blocks = $this->translate_blocks( $blocks );
|
||||
$content = serialize_blocks( $blocks );
|
||||
} else {
|
||||
$content = do_shortcode( $content ); // Translate shortcodes.
|
||||
$content = $this->translate_html( $content );
|
||||
}
|
||||
|
||||
// Get the shorcodes back.
|
||||
$shortcode_tags = $backup; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates the feature image if the translation does not exist yet.
|
||||
*
|
||||
* @since 2.3
|
||||
*
|
||||
* @param int $id Thumbnail ID.
|
||||
* @param string $key Meta key.
|
||||
* @param string $lang Language code.
|
||||
* @return int
|
||||
*/
|
||||
public function duplicate_thumbnail( $id, $key, $lang ) {
|
||||
if ( '_thumbnail_id' === $key && ! $tr_id = $this->model->post->get( $id, $lang ) ) {
|
||||
$tr_id = $this->model->post->create_media_translation( $id, $lang );
|
||||
}
|
||||
return empty( $tr_id ) ? $id : $tr_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates a term if the translation does not exist yet.
|
||||
*
|
||||
* @since 2.3
|
||||
*
|
||||
* @param int $tr_term_id Translated term id.
|
||||
* @param int $term_id Source term id.
|
||||
* @param string $lang Language slug.
|
||||
* @return int The translated term id. O on failure.
|
||||
*/
|
||||
public function duplicate_term( $tr_term_id, $term_id, $lang ) {
|
||||
if ( empty( $tr_term_id ) ) {
|
||||
$term = get_term( $term_id );
|
||||
|
||||
if ( $term instanceof WP_Term ) {
|
||||
$language = $this->model->term->get_language( $term->term_id );
|
||||
|
||||
if ( $language && $language->slug !== $lang ) { // Create a new term translation only if the source term has a language.
|
||||
$tr_parent = empty( $term->parent ) ? 0 : (int) $this->model->term->get_translation( $term->parent, $lang );
|
||||
|
||||
// Duplicate the parent if the parent translation doesn't exist yet.
|
||||
if ( empty( $tr_parent ) && ! empty( $term->parent ) ) {
|
||||
$tr_parent = $this->duplicate_term( 0, $term->parent, $lang );
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'description' => wp_slash( $term->description ),
|
||||
'parent' => $tr_parent,
|
||||
);
|
||||
|
||||
if ( $this->options['force_lang'] ) {
|
||||
// Share slugs
|
||||
$args['slug'] = $term->slug . '___' . $lang;
|
||||
} else {
|
||||
// Language set from the content: assign a different slug
|
||||
// otherwise we would change the current term language instead of creating a new term
|
||||
$args['slug'] = sanitize_title( $term->name ) . '-' . $lang;
|
||||
}
|
||||
|
||||
$t = wp_insert_term( wp_slash( $term->name ), $term->taxonomy, $args );
|
||||
|
||||
$tr_term_id = 0;
|
||||
|
||||
if ( is_array( $t ) ) {
|
||||
$tr_term_id = $t['term_id'];
|
||||
$this->model->term->set_language( $tr_term_id, $lang );
|
||||
$translations = $this->model->term->get_translations( $term->term_id );
|
||||
$translations[ $lang ] = $tr_term_id;
|
||||
$this->model->term->save_translations( $term->term_id, $translations );
|
||||
|
||||
/**
|
||||
* Fires after a term translation is automatically created when duplicating a post.
|
||||
*
|
||||
* @since 2.3.8
|
||||
*
|
||||
* @param int $from Term ID of the source term.
|
||||
* @param int $to Term ID of the new term translation.
|
||||
* @param string $lang Language code of the new translation.
|
||||
*/
|
||||
do_action( 'pll_duplicate_term', $term->term_id, $tr_term_id, $lang );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tr_term_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media translation id
|
||||
* Create the translation if it does not exist
|
||||
* Attach the media to the parent post
|
||||
*
|
||||
* @since 1.9
|
||||
*
|
||||
* @param int $id Media ID.
|
||||
* @return int Translated media ID.
|
||||
*/
|
||||
protected function translate_media( $id ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $tr_id = $this->model->post->get( $id, $this->target_language ) ) {
|
||||
$tr_id = $this->model->post->create_media_translation( $id, $this->target_language );
|
||||
}
|
||||
|
||||
// If we don't have a translation and did not success to create one, return current media
|
||||
if ( empty( $tr_id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
// Attach to the translated post
|
||||
if ( ! wp_get_post_parent_id( $tr_id ) && 0 < $this->target_post->ID ) {
|
||||
// Query inspired by wp_media_attach_action()
|
||||
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_parent = %d WHERE post_type = 'attachment' AND ID = %d", $this->target_post->ID, $tr_id ) );
|
||||
clean_attachment_cache( $tr_id );
|
||||
}
|
||||
|
||||
return $tr_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the 'gallery' and 'playlist' shortcodes
|
||||
*
|
||||
* @since 1.9
|
||||
*
|
||||
* @param array $attr Shortcode attributes.
|
||||
* @param null $null Shortcode content, not used.
|
||||
* @param string $tag Shortcode tag (either 'gallery' or 'playlist').
|
||||
* @return string Translated shortcode.
|
||||
*/
|
||||
public function ids_list_shortcode( $attr, $null, $tag ) {
|
||||
$out = array();
|
||||
|
||||
foreach ( $attr as $k => $v ) {
|
||||
if ( 'ids' === $k ) {
|
||||
$ids = explode( ',', $v );
|
||||
$tr_ids = array();
|
||||
foreach ( $ids as $id ) {
|
||||
$tr_ids[] = $this->translate_media( (int) $id );
|
||||
}
|
||||
$v = implode( ',', $tr_ids );
|
||||
}
|
||||
$out[] = $k . '="' . $v . '"';
|
||||
}
|
||||
|
||||
return '[' . $tag . ' ' . implode( ' ', $out ) . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the caption shortcode
|
||||
* Compatible only with the new style introduced in WP 3.4
|
||||
*
|
||||
* @since 1.9
|
||||
*
|
||||
* @param array $attr Shortcode attrbute.
|
||||
* @param string $content Shortcode content.
|
||||
* @param string $tag Shortcode tag (either 'caption' or 'wp-caption').
|
||||
* @return string Translated shortcode.
|
||||
*/
|
||||
public function caption_shortcode( $attr, $content, $tag ) {
|
||||
// Translate the caption id
|
||||
$out = array();
|
||||
|
||||
foreach ( $attr as $k => $v ) {
|
||||
if ( 'id' === $k ) {
|
||||
$idarr = explode( '_', $v );
|
||||
$id = $idarr[1]; // Remember this
|
||||
$tr_id = $idarr[1] = $this->translate_media( (int) $id );
|
||||
$v = implode( '_', $idarr );
|
||||
}
|
||||
$out[] = $k . '="' . $v . '"';
|
||||
}
|
||||
|
||||
// Translate the caption content
|
||||
if ( ! empty( $id ) && ! empty( $tr_id ) ) {
|
||||
$p = get_post( (int) $id );
|
||||
$tr_p = get_post( $tr_id );
|
||||
if ( $p && $tr_p ) {
|
||||
$content = str_replace( $p->post_excerpt, $tr_p->post_excerpt, $content );
|
||||
}
|
||||
}
|
||||
|
||||
return '[' . $tag . ' ' . implode( ' ', $out ) . ']' . $content . '[/' . $tag . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate images and caption in inner html
|
||||
*
|
||||
* Since 2.5
|
||||
*
|
||||
* @param string $content HTML string.
|
||||
* @return string
|
||||
*/
|
||||
protected function translate_html( $content ) {
|
||||
if ( $this->options['media_support'] ) {
|
||||
$textarr = wp_html_split( $content ); // Since 4.2.3
|
||||
|
||||
$img_ids = array();
|
||||
foreach ( $textarr as $i => $text ) {
|
||||
// Translate img class and alternative text
|
||||
if ( 0 === strpos( $text, '<img' ) ) {
|
||||
$img_ids[] = $this->translate_img( $textarr[ $i ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $img_ids ) ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$new_content = implode( $textarr );
|
||||
$key = 0;
|
||||
$new_content = preg_replace_callback(
|
||||
'@(?<before><figcaption.*?>)(.+?)(?<after></figcaption>)@',
|
||||
function ( $matches ) use ( $img_ids, &$key ) {
|
||||
$tr_post = get_post( $img_ids[ $key ] );
|
||||
$key++;
|
||||
if ( ! empty( $tr_post->post_excerpt ) ) {
|
||||
return $matches['before'] . $tr_post->post_excerpt . $matches['after'];
|
||||
} else {
|
||||
return $matches[0];
|
||||
}
|
||||
},
|
||||
$new_content
|
||||
);
|
||||
|
||||
if ( is_string( $new_content ) ) {
|
||||
return $new_content;
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates <img> 'class' and 'alt' attributes.
|
||||
*
|
||||
* @since 1.9
|
||||
* @since 2.5 The html is passed by reference and the return value is the image ID.
|
||||
*
|
||||
* @param string $text Reference to <img> html with attributes.
|
||||
* @return null|int Translated image id if exist.
|
||||
*/
|
||||
protected function translate_img( &$text ) {
|
||||
$attributes = wp_kses_attr_parse( $text ); // since WP 4.2.3
|
||||
|
||||
if ( ! is_array( $attributes ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Replace class
|
||||
foreach ( $attributes as $k => $attr ) {
|
||||
if ( 0 === strpos( $attr, 'class' ) && preg_match( '#wp\-image\-([0-9]+)#', $attr, $matches ) && ! empty( $matches[1] ) ) {
|
||||
$tr_id = $this->translate_media( (int) $matches[1] );
|
||||
$attributes[ $k ] = str_replace( 'wp-image-' . $matches[1], 'wp-image-' . $tr_id, $attr );
|
||||
|
||||
}
|
||||
|
||||
if ( preg_match( '#^data\-id="([0-9]+)#', $attr, $matches ) && ! empty( $matches[1] ) ) {
|
||||
$tr_id = $this->translate_media( (int) $matches[1] );
|
||||
$attributes[ $k ] = str_replace( 'data-id="' . $matches[1], 'data-id="' . $tr_id, $attr );
|
||||
}
|
||||
|
||||
if ( 0 === strpos( $attr, 'data-link' ) && preg_match( '#attachment_id=([0-9]+)#', $attr, $matches ) && ! empty( $matches[1] ) ) {
|
||||
$tr_id = $this->translate_media( (int) $matches[1] );
|
||||
$attributes[ $k ] = str_replace( 'attachment_id=' . $matches[1], 'attachment_id=' . $tr_id, $attr );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $tr_id ) ) {
|
||||
// Got a tr_id, attempt to replace the alt text
|
||||
$alt = get_post_meta( $tr_id, '_wp_attachment_image_alt', true );
|
||||
if ( is_string( $alt ) && ! empty( $alt ) ) {
|
||||
foreach ( $attributes as $k => $attr ) {
|
||||
if ( 0 === strpos( $attr, 'alt' ) ) {
|
||||
$attributes[ $k ] = 'alt="' . esc_attr( $alt ) . '" ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$text = implode( $attributes );
|
||||
|
||||
return empty( $tr_id ) ? null : $tr_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively translate blocks.
|
||||
*
|
||||
* @param array[] $blocks An array of arrays representing a block.
|
||||
* @return array
|
||||
*/
|
||||
protected function translate_blocks( $blocks ) {
|
||||
foreach ( $blocks as $k => $block ) {
|
||||
switch ( $block['blockName'] ) {
|
||||
case 'core/block':
|
||||
if ( ! $this->model->is_translated_post_type( 'wp_block' ) || ! isset( $block['attrs']['ref'] ) ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$tr_id = $this->model->post->get( $block['attrs']['ref'], $this->target_language );
|
||||
|
||||
if ( ! empty( $tr_id ) ) {
|
||||
$blocks[ $k ]['attrs']['ref'] = $tr_id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'core/latest-posts':
|
||||
if ( isset( $block['attrs']['categories'] ) ) {
|
||||
$tr_ids = array();
|
||||
foreach ( $block['attrs']['categories'] as $term ) {
|
||||
$tr_ids[] = $this->model->term->get( $term['id'], $this->target_language );
|
||||
}
|
||||
|
||||
// Let's remove unfound translation results.
|
||||
$tr_ids = array_filter( $tr_ids );
|
||||
|
||||
// If there is no translation, then the category is unset.
|
||||
if ( empty( $tr_ids ) ) {
|
||||
unset( $blocks[ $k ]['attrs']['categories'] );
|
||||
break;
|
||||
}
|
||||
|
||||
// Query all the translated terms outside the loop to avoid multiple SQL queries with get_term() call.
|
||||
$terms = get_terms( array( 'include' => $tr_ids, 'hide_empty' => false, 'fields' => 'id=>name' ) );
|
||||
|
||||
if ( ! is_array( $terms ) ) {
|
||||
unset( $blocks[ $k ]['attrs']['categories'] );
|
||||
break;
|
||||
}
|
||||
|
||||
$tr_data = array();
|
||||
foreach ( $terms as $id => $term_name ) {
|
||||
$tr_data[] = array(
|
||||
'id' => $id,
|
||||
'value' => $term_name,
|
||||
);
|
||||
}
|
||||
if ( $tr_data ) {
|
||||
$blocks[ $k ]['attrs']['categories'] = $tr_data;
|
||||
} else {
|
||||
unset( $blocks[ $k ]['attrs']['categories'] );
|
||||
}
|
||||
} else {
|
||||
unset( $blocks[ $k ]['attrs']['categories'] );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $this->options['media_support'] ) {
|
||||
$blocks[ $k ] = $this->translate_media_block( $blocks[ $k ] );
|
||||
}
|
||||
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$blocks[ $k ]['innerBlocks'] = $this->translate_blocks( $block['innerBlocks'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters parsed blocks after core blocks have been translated.
|
||||
*
|
||||
* @since 2.5.3
|
||||
*
|
||||
* @param array[] $blocks List of blocks.
|
||||
* @param string $lang Language of target.
|
||||
* @param string $from_lang Language of the source.
|
||||
*/
|
||||
return apply_filters( 'pll_translate_blocks', $blocks, $this->target_language->slug, $this->from_language->slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates media ids in blocks.
|
||||
*
|
||||
* @since 3.3
|
||||
*
|
||||
* @param array $block A representative array of a block.
|
||||
* @return array The translated block.
|
||||
*/
|
||||
protected function translate_media_block( $block ) {
|
||||
switch ( $block['blockName'] ) {
|
||||
case 'core/audio':
|
||||
case 'core/video':
|
||||
if ( array_key_exists( 'id', $block['attrs'] ) ) {
|
||||
$block['attrs']['id'] = $this->translate_media( $block['attrs']['id'] );
|
||||
}
|
||||
break;
|
||||
case 'core/cover':
|
||||
case 'core/image':
|
||||
if ( array_key_exists( 'id', $block['attrs'] ) ) {
|
||||
$block['attrs']['id'] = $this->translate_media( $block['attrs']['id'] );
|
||||
}
|
||||
$block = $this->translate_block_content( $block );
|
||||
break;
|
||||
|
||||
case 'core/file':
|
||||
$source_id = $block['attrs']['id'];
|
||||
$tr_id = $this->translate_media( $source_id );
|
||||
$block['attrs']['id'] = $tr_id;
|
||||
$textarr = wp_html_split( $block['innerHTML'] );
|
||||
$source_post = get_post( $source_id );
|
||||
if ( ! $source_post instanceof WP_Post ) {
|
||||
break;
|
||||
}
|
||||
$replace_file_link_text = 0 === strpos( $textarr[3], '<a' ) && $textarr[4] === $source_post->post_title;
|
||||
if ( $replace_file_link_text ) {
|
||||
$tr_post = get_post( $tr_id );
|
||||
if ( $tr_post ) {
|
||||
$textarr[4] = $tr_post->post_title;
|
||||
$block['innerContent'][0] = implode( $textarr );
|
||||
$block['innerHTML'] = implode( $textarr );
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'core/gallery':
|
||||
if ( isset( $block['attrs']['ids'] ) && is_array( $block['attrs']['ids'] ) ) {
|
||||
// Backward compatibility with WP < 5.9.
|
||||
foreach ( $block['attrs']['ids'] as $n => $id ) {
|
||||
$block['attrs']['ids'][ $n ] = $this->translate_media( $id );
|
||||
}
|
||||
}
|
||||
$block = $this->translate_block_content( $block );
|
||||
break;
|
||||
|
||||
case 'core/media-text':
|
||||
$block['attrs']['mediaId'] = $this->translate_media( $block['attrs']['mediaId'] );
|
||||
$block['innerContent'][0] = $this->translate_html( $block['innerContent'][0] );
|
||||
break;
|
||||
|
||||
case 'core/shortcode':
|
||||
$block['innerContent'][0] = do_shortcode( $block['innerContent'][0] );
|
||||
$block['innerHTML'] = do_shortcode( $block['innerHTML'] );
|
||||
break;
|
||||
|
||||
default:
|
||||
if ( ! empty( $block['innerHTML'] ) ) {
|
||||
$block = $this->translate_block_content( $block );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the block properties with a translation if it is found.
|
||||
*
|
||||
* @since 2.9
|
||||
*
|
||||
* @param array $block An array mimicking the structure of {@see https://github.com/WordPress/WordPress/blob/5.5.1/wp-includes/class-wp-block-parser.php WP_Block_Parser_Block}.
|
||||
* @return array The updated array formatted block.
|
||||
*/
|
||||
protected function translate_block_content( $block ) {
|
||||
$inner_content_nb = count( $block['innerContent'] );
|
||||
for ( $i = 0; $i < $inner_content_nb; $i++ ) {
|
||||
if ( ! empty( $block['innerContent'][ $i ] ) ) {
|
||||
$html = do_shortcode( $block['innerContent'][ $i ] ); // Translate shortcodes.
|
||||
$html = $this->translate_html( $html ); // Translate inline images.
|
||||
|
||||
$block['innerContent'][ $i ] = $html;
|
||||
}
|
||||
}
|
||||
$html = do_shortcode( $block['innerHTML'] ); // Translate shortcodes.
|
||||
$block['innerHTML'] = $this->translate_html( $html );
|
||||
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AutoPoly - AI Translation For Polylang (Pro)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handles cloning and translation of taxonomy terms for AI Translation For Polylang.
|
||||
*
|
||||
* Provides methods to translate taxonomy terms (categories, tags, custom taxonomies)
|
||||
* using translation entries, and to assign parent relationships for translated terms.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ATFP_Term_Clone {
|
||||
|
||||
/**
|
||||
* Main model for managing languages and translations.
|
||||
*
|
||||
* @var ATFP_Model
|
||||
*/
|
||||
private $model;
|
||||
|
||||
/**
|
||||
* Constructor for ATFP_Term_Clone.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param ATFP_Settings|ATFP_Admin $polylang Main plugin object containing model and sync references.
|
||||
*/
|
||||
public function __construct( &$polylang ) {
|
||||
$this->model = &$polylang->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a taxonomy term using translation entries.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param array $entry Array containing term properties and translation data.
|
||||
* @param PLL_Language $target_language Target language object.
|
||||
* @return int|WP_Error The translated term ID, or WP_Error on failure.
|
||||
*/
|
||||
public function translate( array $entry, PLL_Language $target_language ) {
|
||||
if ( ! $entry['data'] instanceof Translations ) {
|
||||
/* translators: %d is a term ID. */
|
||||
return new WP_Error( 'atfp_translate_term_no_translations', sprintf( __( 'The term with ID %d could not be translated.', 'auto-translate-for-polylang' ), (int) $entry['id'] ) );
|
||||
}
|
||||
|
||||
$source_term = get_term( $entry['id'] );
|
||||
|
||||
if ( ! $source_term instanceof WP_Term ) {
|
||||
/* translators: %d is a term ID. */
|
||||
return new WP_Error( 'atfp_translate_term_no_source_term', sprintf( __( 'The term with ID %d could not be translated as it doesn\'t exist.', 'auto-translate-for-polylang' ), (int) $entry['id'] ) );
|
||||
}
|
||||
|
||||
$tr_term_name = $this->get_translated_term_name( $source_term, $entry['data'] );
|
||||
$tr_term_description = $this->get_translated_term_description( $source_term, $entry['data'] );
|
||||
$tr_term_id = $this->model->term->get( $entry['id'], $target_language );
|
||||
$tr_term_slug = $this->get_translated_term_slug( $source_term, $entry['data'] );
|
||||
|
||||
if ( $tr_term_id ) {
|
||||
// The translation already exists.
|
||||
$args = array();
|
||||
// Only update name or description if provided in translations.
|
||||
if ( $source_term->name !== $tr_term_name ) {
|
||||
$args['name'] = $tr_term_name;
|
||||
}
|
||||
if ( !empty($source_term->description) && $source_term->description !== $tr_term_description ) {
|
||||
$args['description'] = $tr_term_description;
|
||||
}
|
||||
|
||||
$tr_term = $this->model->term->update( $tr_term_id, $args );
|
||||
if ( is_wp_error( $tr_term ) ) {
|
||||
/* translators: %d is a term ID. */
|
||||
return new WP_Error( 'atfp_translate_update_term_failed', sprintf( __( 'The term with ID %d could not be updated.', 'auto-translate-for-polylang' ), (int) $tr_term_id ) );
|
||||
}
|
||||
} else {
|
||||
$args = array(
|
||||
'translations' => $this->model->term->get_translations( $source_term->term_id ),
|
||||
);
|
||||
|
||||
if ( !empty( $source_term->description ) ) {
|
||||
$args['description'] = $tr_term_description;
|
||||
}
|
||||
|
||||
if ( $tr_term_slug && ! empty( $tr_term_slug ) ) {
|
||||
$args['slug'] = $tr_term_slug;
|
||||
}
|
||||
|
||||
$tr_term = $this->model->term->insert( $tr_term_name, $source_term->taxonomy, $target_language, $args );
|
||||
if ( is_wp_error( $tr_term ) ) {
|
||||
/* translators: %d is a term ID. */
|
||||
return new WP_Error( 'atfp_translate_term_failed', sprintf( __( 'The term with ID %d could not be translated.', 'auto-translate-for-polylang' ), (int) $entry['id'] ) );
|
||||
}
|
||||
$tr_term_id = (int) $tr_term['term_id'];
|
||||
}
|
||||
|
||||
/** @var WP_Term $tr_term */
|
||||
$tr_term = get_term( $tr_term_id );
|
||||
|
||||
/**
|
||||
* Fires after a term is saved and its translations are updated.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param int $tr_term_id The translated term ID.
|
||||
* @param string $taxonomy The taxonomy slug.
|
||||
* @param array $translations Array of term translations.
|
||||
*/
|
||||
do_action( 'atfp_save_term', $tr_term_id, $source_term->taxonomy, $this->model->term->get_translations( $tr_term_id ) );
|
||||
|
||||
$this->assign_parents( [ $entry['id'] ], $target_language );
|
||||
|
||||
return $tr_term_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated term name, or falls back to the source name.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param WP_Term $source_term The source term object.
|
||||
* @param Translations $translations Translation data object.
|
||||
* @return string The translated name.
|
||||
*/
|
||||
private function get_translated_term_name( WP_Term $source_term, Translations $translations ) {
|
||||
$entry = new Translation_Entry( array( 'singular' => $source_term->name, 'context' => 'name' ) );
|
||||
|
||||
$translated_entry = $translations->translate_entry( $entry );
|
||||
|
||||
$translated_text = isset( $translated_entry->translation[0] ) && ! empty( $translated_entry->translation[0] ) ? $translated_entry->translation[0] : $source_term->name;
|
||||
|
||||
return $translated_text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated term description, or falls back to the source description.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param WP_Term $source_term The source term object.
|
||||
* @param Translations $translations Translation data object.
|
||||
* @return string The translated description.
|
||||
*/
|
||||
private function get_translated_term_description( WP_Term $source_term, Translations $translations ) {
|
||||
$entry = new Translation_Entry( array( 'singular' => $source_term->description, 'context' => 'description' ) );
|
||||
|
||||
$translated_entry = $translations->translate_entry( $entry );
|
||||
|
||||
$translated_text = isset( $translated_entry->translation[0] ) && ! empty( $translated_entry->translation[0] ) ? $translated_entry->translation[0] : '';
|
||||
|
||||
return $translated_text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the translated term slug, or returns an empty string if not available.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param WP_Term $source_term The source term object.
|
||||
* @param Translations $translations Translation data object.
|
||||
* @return string The translated slug.
|
||||
*/
|
||||
private function get_translated_term_slug( WP_Term $source_term, Translations $translations ) {
|
||||
$entry = new Translation_Entry( array( 'singular' => $source_term->slug, 'context' => 'slug' ) );
|
||||
|
||||
$translated_entry = $translations->translate_entry( $entry );
|
||||
|
||||
$translated_text = isset( $translated_entry->translation[0] ) && ! empty( $translated_entry->translation[0] ) ? $translated_entry->translation[0] : '';
|
||||
|
||||
return $translated_text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns parent terms to translated terms after import.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*
|
||||
* @param int[] $ids Array of source term IDs.
|
||||
* @param PLL_Language $target_language Target language object.
|
||||
* @return void
|
||||
*/
|
||||
public function assign_parents( array $ids, PLL_Language $target_language ) {
|
||||
// Get the terms with their parents (or 0).
|
||||
$terms = get_terms(
|
||||
array(
|
||||
'include' => $ids,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'id=>parent',
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! is_array( $terms ) ) {
|
||||
// No terms with parents.
|
||||
return;
|
||||
}
|
||||
|
||||
// ‘id=>parent’ returns an array of numeric strings, so let's cast it into int.
|
||||
$terms = array_map( 'intval', array_filter( $terms, 'is_numeric' ) );
|
||||
|
||||
// Keep only the terms that have a parent.
|
||||
$terms = array_filter( $terms );
|
||||
|
||||
if ( empty( $terms ) ) {
|
||||
// No terms with parents.
|
||||
return;
|
||||
}
|
||||
|
||||
$tr_ids = array();
|
||||
foreach ( $terms as $child => $term_id ) {
|
||||
$tr_ids[ $child ] = $this->model->term->get( $child, $target_language->slug );
|
||||
}
|
||||
$tr_ids = array_filter( $tr_ids );
|
||||
|
||||
if ( empty( $tr_ids ) ) {
|
||||
// No translations.
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $terms as $child => $term_id ) {
|
||||
if ( empty( $tr_ids[ $child ] ) ) {
|
||||
// Not translated.
|
||||
continue;
|
||||
}
|
||||
|
||||
$tr_parent_term = $this->model->term->get( $term_id, $target_language->slug );
|
||||
if ( empty( $tr_parent_term ) ) {
|
||||
// The parent term is not translated.
|
||||
continue;
|
||||
}
|
||||
|
||||
$tr_term_id = $this->model->term->get( $tr_ids[ $child ], $target_language->slug );
|
||||
if ( empty( $tr_term_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tr_term = get_term( $tr_term_id );
|
||||
if ( ! $tr_term instanceof WP_Term ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set term parent for shared slugs.
|
||||
$this->model->term->update( $tr_term->term_id, array( 'parent' => $tr_parent_term ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
|
||||
class ATFPP_Register_Backend_Assets
|
||||
{
|
||||
|
||||
/**
|
||||
* Singleton instance of ATFPP_Register_Backend_Assets.
|
||||
*
|
||||
* @var ATFPP_Register_Backend_Assets
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Get the singleton instance of ATFPP_Register_Backend_Assets.
|
||||
*
|
||||
* @return ATFPP_Register_Backend_Assets
|
||||
*/
|
||||
public static function get_instance()
|
||||
{
|
||||
if (! isset(self::$instance)) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ATFPP_Register_Backend_Assets.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_gutenberg_translate_assets'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_supported_block_scripts'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_classic_translate_assets'));
|
||||
add_action('enqueue_block_assets', array($this, 'block_inline_translation_assets'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'classic_inline_translation_assets'));
|
||||
add_action('elementor/editor/before_enqueue_scripts', array($this, 'enqueue_elementor_translate_assets'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_bulk_translate_assets'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'atfpp_enqueue_admin_assets'));
|
||||
}
|
||||
|
||||
public function atfpp_enqueue_admin_assets(){
|
||||
if(!is_admin()){
|
||||
return;
|
||||
}
|
||||
|
||||
global $polylang;
|
||||
|
||||
if(!$polylang || !property_exists($polylang, 'model') || !function_exists('get_current_screen')){
|
||||
return;
|
||||
}
|
||||
|
||||
$current_screen = get_current_screen();
|
||||
|
||||
if(class_exists('ATFPP_Helper') && ATFPP_Helper::bulk_translation_render($current_screen)){
|
||||
wp_enqueue_script('atfp-views-link-admin', ATFPP_URL . 'assets/js/atfp-admin-views-link.js', array('jquery'), ATFPP_V, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_supported_block_scripts(){
|
||||
if(function_exists('get_current_screen') && property_exists(get_current_screen(), 'post_type') && 'atfp_add_blocks' === get_current_screen()->post_type){
|
||||
wp_enqueue_style('atfp-update-custom-blocks', ATFPP_URL . 'assets/css/atfp-update-custom-blocks.min.css', array(), ATFPP_V);
|
||||
wp_enqueue_script('atfp-update-custom-blocks', ATFPP_URL . 'assets/js/atfp-update-custom-blocks.min.js', array('jquery'), ATFPP_V, true);
|
||||
|
||||
wp_localize_script(
|
||||
'atfp-update-custom-blocks',
|
||||
'atfp_block_update_object',
|
||||
array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'ajax_nonce' => wp_create_nonce('atfp_block_update_nonce'),
|
||||
'atfp_url' => esc_url(ATFPP_URL),
|
||||
'action_get_content' => 'atfp_get_custom_blocks_content',
|
||||
'action_update_content' => 'atfp_update_custom_blocks_content',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register block translator assets.
|
||||
*/
|
||||
public function block_inline_translation_assets()
|
||||
{
|
||||
|
||||
if (defined('POLYLANG_VERSION')) {
|
||||
$this->enqueue_inline_translation_assets('block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register backend assets.
|
||||
*/
|
||||
public function enqueue_gutenberg_translate_assets()
|
||||
{
|
||||
$current_screen = get_current_screen();
|
||||
if (
|
||||
isset($_GET['from_post'], $_GET['new_lang'], $_GET['_wpnonce']) &&
|
||||
wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'new-post-translation')
|
||||
) {
|
||||
if (method_exists($current_screen, 'is_block_editor') && $current_screen->is_block_editor()) {
|
||||
$from_post_id = isset($_GET['from_post']) ? absint($_GET['from_post']) : 0;
|
||||
|
||||
global $post;
|
||||
|
||||
if (null === $post || 0 === $from_post_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lang = isset($_GET['new_lang']) ? sanitize_key($_GET['new_lang']) : '';
|
||||
|
||||
$editor = '';
|
||||
if ('builder' === get_post_meta($from_post_id, '_elementor_edit_mode', true) && defined('ELEMENTOR_VERSION')) {
|
||||
$source_lang_name = pll_get_post_language($from_post_id, 'slug');
|
||||
$this->enqueue_elementor_confirm_box_assets($from_post_id, $lang, $source_lang_name, 'gutenberg');
|
||||
$editor = 'Elementor';
|
||||
}
|
||||
if ('on' === get_post_meta($from_post_id, '_et_pb_use_builder', true) && defined('ET_CORE')) {
|
||||
$editor = 'Divi';
|
||||
}
|
||||
|
||||
if (in_array($editor, array('Elementor', 'Divi'), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$languages = PLL()->model->get_languages_list();
|
||||
|
||||
$lang_object = array();
|
||||
foreach ($languages as $lang_obj) {
|
||||
$lang_object[$lang_obj->slug] = $lang_obj->name;
|
||||
}
|
||||
|
||||
$post_translate = PLL()->model->is_translated_post_type($post->post_type);
|
||||
|
||||
$post_type = isset($_GET['post_type']) ? sanitize_key($_GET['post_type']) : '';
|
||||
|
||||
if ($post_translate && $lang && $post_type) {
|
||||
$data = array(
|
||||
'action_fetch' => 'atfp_fetch_post_content',
|
||||
'action_block_rules' => 'atfp_block_parsing_rules',
|
||||
'parent_post_id' => $from_post_id,
|
||||
);
|
||||
|
||||
$this->enqueue_automatic_translate_assets(pll_get_post_language($from_post_id, 'slug'), $lang, 'gutenberg', $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_classic_translate_assets()
|
||||
{
|
||||
global $post;
|
||||
$current_screen = get_current_screen();
|
||||
$post_translate_status = isset($post) ? get_post_meta($post->ID, '_atfpp_translate_status', true) : '';
|
||||
$post_parent_post_id = isset($post) ? get_post_meta($post->ID, '_atfpp_parent_post_id', true) : '';
|
||||
|
||||
if(isset($current_screen) && isset($current_screen->id) && $current_screen->id === 'edit-page'){
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($_GET['from_post'], $_GET['new_lang'], $_GET['_wpnonce']) &&
|
||||
wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'new-post-translation')) {
|
||||
$current_screen = get_current_screen();
|
||||
|
||||
if (method_exists($current_screen, 'is_block_editor') && !$current_screen->is_block_editor()) {
|
||||
$from_post_id = isset($_GET['from_post']) ? absint($_GET['from_post']) : 0;
|
||||
$from_post_id = !empty($post_parent_post_id) ? $post_parent_post_id : $from_post_id;
|
||||
|
||||
if (null === $post || 0 === $from_post_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lang = isset($_GET['new_lang']) ? sanitize_key($_GET['new_lang']) : '';
|
||||
|
||||
if(!empty($post_translate_status) && $post_translate_status === 'pending') {
|
||||
$lang = pll_get_post_language($post->ID, 'slug');
|
||||
}
|
||||
|
||||
$editor = '';
|
||||
if ('builder' === get_post_meta($from_post_id, '_elementor_edit_mode', true) && defined('ELEMENTOR_VERSION')) {
|
||||
$source_lang_name = pll_get_post_language($from_post_id, 'slug');
|
||||
$this->enqueue_elementor_confirm_box_assets($from_post_id, $lang, $source_lang_name, 'classic');
|
||||
$editor = 'Elementor';
|
||||
}
|
||||
if ('on' === get_post_meta($from_post_id, '_et_pb_use_builder', true) && defined('ET_CORE')) {
|
||||
$editor = 'Divi';
|
||||
}
|
||||
|
||||
if (in_array($editor, array('Elementor', 'Divi'), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$languages = PLL()->model->get_languages_list();
|
||||
|
||||
$lang_object = array();
|
||||
foreach ($languages as $lang_obj) {
|
||||
$lang_object[$lang_obj->slug] = $lang_obj->name;
|
||||
}
|
||||
|
||||
$post_translate = PLL()->model->is_translated_post_type($post->post_type);
|
||||
|
||||
|
||||
if ($post_translate && $lang && !empty($lang)) {
|
||||
|
||||
$data = array(
|
||||
'action_fetch' => 'atfp_fetch_post_content',
|
||||
'parent_post_id' => $from_post_id,
|
||||
'action_update_status' => 'atfp_update_classic_translate_status',
|
||||
'classic_status_key' => wp_create_nonce('atfp_classic_translate_nonce'),
|
||||
);
|
||||
|
||||
$parent_page_content = get_the_content(null, false, $from_post_id);
|
||||
$block_comment_tag = preg_match('/<!--[\s\S]*?-->/s', $parent_page_content) && strpos($parent_page_content, '<!--') < strpos($parent_page_content, '-->');
|
||||
|
||||
if($block_comment_tag){
|
||||
$data['blockCommentTag']="true";
|
||||
}
|
||||
|
||||
$this->enqueue_automatic_translate_assets(pll_get_post_language($from_post_id, 'slug'), $lang, 'classic', $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function classic_inline_translation_assets()
|
||||
{
|
||||
$current_screen = get_current_screen();
|
||||
|
||||
if (method_exists($current_screen, 'is_block_editor') && !$current_screen->is_block_editor()) {
|
||||
$this->enqueue_inline_translation_assets('classic');
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_elementor_translate_assets()
|
||||
{
|
||||
|
||||
$this->elementor_inline_translation_assets();
|
||||
|
||||
$page_translated = get_post_meta(get_the_ID(), '_atfp_elementor_translated', true);
|
||||
$parent_post_language_slug = get_post_meta(get_the_ID(), '_atfp_parent_post_language_slug', true);
|
||||
|
||||
if ((!empty($page_translated) && $page_translated === 'true') || empty($parent_post_language_slug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_language_slug = pll_get_post_language(get_the_ID(), 'slug');
|
||||
$current_post_id = get_the_ID(); // Get the current post ID
|
||||
|
||||
if(!class_exists('\Elementor\Plugin') || !property_exists('\Elementor\Plugin', 'instance') ){
|
||||
return;
|
||||
}
|
||||
|
||||
$elementor_data = \Elementor\Plugin::$instance->documents->get( $current_post_id )->get_elements_data();
|
||||
|
||||
|
||||
if ($parent_post_language_slug === $post_language_slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent_post_id=PLL()->model->post->get_translation($current_post_id, $parent_post_language_slug);
|
||||
|
||||
$data = array(
|
||||
'update_elementor_data' => 'atfp_update_elementor_data',
|
||||
'elementorData' => $elementor_data,
|
||||
'parent_post_id' => $parent_post_id,
|
||||
'parent_post_title' => get_the_title($parent_post_id),
|
||||
);
|
||||
|
||||
wp_enqueue_style('atfp-elementor-translate', ATFPP_URL . 'assets/css/atfp-elementor-translate.css', array(), ATFPP_V);
|
||||
$this->enqueue_automatic_translate_assets($parent_post_language_slug, $post_language_slug, 'elementor', $data);
|
||||
}
|
||||
|
||||
public function enqueue_automatic_translate_assets($source_lang, $target_lang, $editor_type, $extra_data = array())
|
||||
{
|
||||
wp_register_style('atfp-automatic-translate-custom', ATFPP_URL . 'assets/css/atfp-custom.min.css', array(), ATFPP_V);
|
||||
wp_register_script( 'atfp-google-api', 'https://translate.google.com/translate_a/element.js', '', ATFPP_V, true );
|
||||
|
||||
$editor_script_asset = include ATFPP_DIR_PATH . 'assets/automatic-translate/index.asset.php';
|
||||
wp_register_script('atfp-automatic-translate', ATFPP_URL . 'assets/automatic-translate/index.js', array_merge($editor_script_asset['dependencies'], ['atfp-google-api']), $editor_script_asset['version'], true);
|
||||
|
||||
$post_type = get_post_type();
|
||||
|
||||
$languages = PLL()->model->get_languages_list();
|
||||
$lang_object = array();
|
||||
foreach ($languages as $lang) {
|
||||
$lang_object[$lang->slug] = array('name' => $lang->name, 'flag' => $lang->flag_url, 'locale' => $lang->locale);
|
||||
}
|
||||
|
||||
wp_enqueue_style('atfp-automatic-translate-custom');
|
||||
|
||||
wp_enqueue_script('atfp-automatic-translate');
|
||||
wp_set_script_translations('atfp-automatic-translate', 'autopoly-ai-translation-for-polylang-pro', ATFPP_DIR_PATH . 'languages');
|
||||
|
||||
|
||||
$post_id = get_the_ID();
|
||||
|
||||
$services=atfpp_ai_services()->get_registered_service_slugs();
|
||||
$available_ai_services=array();
|
||||
|
||||
foreach($services as $service){
|
||||
$service_status=atfpp_ai_services()->is_service_available($service);
|
||||
if($service_status){
|
||||
array_push($available_ai_services, $service);
|
||||
}
|
||||
}
|
||||
|
||||
$slug_translation_option = get_option('atfp_slug_translation_option','title_translate');
|
||||
|
||||
if(isset($extra_data['parent_post_id'])){
|
||||
$parent_post_id = $extra_data['parent_post_id'];
|
||||
$parent_post_slug = get_post_field('post_name', $parent_post_id);
|
||||
|
||||
$extra_data['slug_name'] = $parent_post_slug;
|
||||
}
|
||||
|
||||
if (!isset(PLL()->options['sync']) || (isset(PLL()->options['sync']) && !in_array('post_meta', PLL()->options['sync']))) {
|
||||
$extra_data['postMetaSync'] = 'false';
|
||||
|
||||
if(in_array($editor_type, array('classic', 'gutenberg'))){
|
||||
$extra_data['update_post_meta_fields'] = 'atfp_update_post_meta_fields';
|
||||
$extra_data['post_meta_fields_key'] = wp_create_nonce('atfp_update_post_meta_fields');
|
||||
}
|
||||
|
||||
} else {
|
||||
$extra_data['postMetaSync'] = 'true';
|
||||
}
|
||||
|
||||
$ai_max_tokens=get_option('atfp_ai_request_token_per_request', 500);
|
||||
$ai_batch_size=get_option('atfp_ai_request_batch_size', 5);
|
||||
|
||||
$data = array_merge(array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'ajax_nonce' => wp_create_nonce('atfp_translate_nonce'),
|
||||
'atfp_url' => esc_url(ATFPP_URL),
|
||||
'admin_url' => admin_url(),
|
||||
'update_translate_data' => 'atfp_update_translate_data',
|
||||
'source_lang' => $source_lang,
|
||||
'target_lang' => $target_lang,
|
||||
'languageObject' => $lang_object,
|
||||
'post_type' => $post_type,
|
||||
'editor_type' => $editor_type,
|
||||
'current_post_id' => $post_id,
|
||||
'ai_translate_route_url' => get_rest_url(null, 'atfpp-translate'),
|
||||
'ai_translate_route_nonce' => wp_create_nonce('wp_rest'),
|
||||
'ai_translate_nonce' => wp_create_nonce('atfp_ai_translate_nonce'),
|
||||
'get_meta_fields' => 'atfp_fetch_post_meta_fields',
|
||||
'meta_fields_key'=>wp_create_nonce('atfp_fetch_post_meta_fields'),
|
||||
'atfpp_glossary_nonce' => wp_create_nonce('atfpp_glossary_nonce'),
|
||||
'slug_translation_option' => $slug_translation_option,
|
||||
'AIServices' => $available_ai_services,
|
||||
'AIRequestMaxTokens' => $ai_max_tokens,
|
||||
'AIRequestBatchSize' => $ai_batch_size,
|
||||
), $extra_data);
|
||||
|
||||
wp_localize_script(
|
||||
'atfp-automatic-translate',
|
||||
'atfp_global_object',
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the elementor widget translator script.
|
||||
*/
|
||||
public function elementor_inline_translation_assets()
|
||||
{
|
||||
if (defined('POLYLANG_VERSION')) {
|
||||
$this->enqueue_inline_translation_assets(
|
||||
'elementor',
|
||||
array(
|
||||
'backbone-marionette',
|
||||
'elementor-common',
|
||||
'elementor-web-cli',
|
||||
'elementor-editor-modules',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_bulk_translate_assets()
|
||||
{
|
||||
global $polylang;
|
||||
|
||||
if(!$polylang || !property_exists($polylang, 'model')){
|
||||
return;
|
||||
}
|
||||
|
||||
$current_screen = function_exists('get_current_screen') ? get_current_screen() : false;
|
||||
|
||||
if(!$current_screen){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!class_exists('ATFPP_Helper') || !ATFPP_Helper::bulk_translation_render($current_screen)){
|
||||
return;
|
||||
}
|
||||
|
||||
$post_status=isset($_GET['post_status']) ? sanitize_text_field(wp_unslash($_GET['post_status'])) : '';
|
||||
|
||||
if('trash' === $post_status){
|
||||
return;
|
||||
}
|
||||
|
||||
$post_label=__("Pages", "autopoly-ai-translation-for-polylang-pro");
|
||||
$taxonomy_page=false;
|
||||
|
||||
if(isset($current_screen->post_type)){
|
||||
$post_type = $current_screen->post_type;
|
||||
|
||||
if(isset(get_post_type_object($post_type)->label) && !empty(get_post_type_object($post_type)->label)){
|
||||
$post_label = get_post_type_object($post_type)->label;
|
||||
}
|
||||
|
||||
if(isset($current_screen->taxonomy) && !empty($current_screen->taxonomy)){
|
||||
$taxonomy_page=$current_screen->taxonomy;
|
||||
$taxonomy_object = get_taxonomy($current_screen->taxonomy);
|
||||
|
||||
if(isset($taxonomy_object->label) && !empty($taxonomy_object->label)){
|
||||
$post_label = $taxonomy_object->label;
|
||||
|
||||
if(isset($taxonomy_object->labels->singular_name) && !empty($taxonomy_object->labels->singular_name)){
|
||||
$post_label = $taxonomy_object->labels->singular_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$slug_translation_option = get_option('atfp_slug_translation_option','title_translate');
|
||||
|
||||
$editor_script_asset = include ATFPP_DIR_PATH . 'assets/bulk-translate/index.asset.php';
|
||||
|
||||
$rtl=function_exists('is_rtl') ? is_rtl() : false;
|
||||
$css_file=$rtl ? 'index-rtl.css' : 'index.css';
|
||||
|
||||
wp_enqueue_script( 'atfpp-google-api', 'https://translate.google.com/translate_a/element.js', '', ATFPP_V, true );
|
||||
wp_enqueue_script('atfpp-bulk-translate', ATFPP_URL . 'assets/bulk-translate/index.js', array_merge($editor_script_asset['dependencies'], ['atfpp-google-api']), $editor_script_asset['version'], true);
|
||||
|
||||
wp_enqueue_style('atfpp-bulk-translate', ATFPP_URL . 'assets/bulk-translate/'.$css_file, array(), $editor_script_asset['version']);
|
||||
|
||||
$languages = PLL()->model->get_languages_list();
|
||||
|
||||
$lang_object = array();
|
||||
foreach ($languages as $lang) {
|
||||
$lang_object[$lang->slug] = array('name' => $lang->name, 'flag' => $lang->flag_url, 'locale' => $lang->locale);
|
||||
}
|
||||
|
||||
$services=atfpp_ai_services()->get_registered_service_slugs();
|
||||
$available_ai_services=array();
|
||||
|
||||
foreach($services as $service){
|
||||
$service_status=atfpp_ai_services()->is_service_available($service);
|
||||
if($service_status){
|
||||
array_push($available_ai_services, $service);
|
||||
}
|
||||
}
|
||||
|
||||
$extra_data = array();
|
||||
|
||||
if(!$taxonomy_page || empty($taxonomy_page)){
|
||||
if (!isset(PLL()->options['sync']) || (isset(PLL()->options['sync']) && !in_array('post_meta', PLL()->options['sync']))) {
|
||||
$extra_data['postMetaSync'] = 'false';
|
||||
} else {
|
||||
$extra_data['postMetaSync'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
$ai_max_tokens=get_option('atfp_ai_request_token_per_request', 500);
|
||||
$ai_batch_size=get_option('atfp_ai_request_batch_size', 5);
|
||||
|
||||
wp_localize_script(
|
||||
'atfpp-bulk-translate',
|
||||
'atfpp_bulk_translate_object',
|
||||
array_merge(array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'languageObject' => $lang_object,
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'bulkTranslateRouteUrl' => get_rest_url(null, 'atfpp-translate'),
|
||||
'bulkTranslatePrivateKey' => wp_create_nonce('atfpp_bulk_translate_entries_nonce'),
|
||||
'atfpp_url' => esc_url(ATFPP_URL),
|
||||
'AIServices' => $available_ai_services,
|
||||
'admin_url' => admin_url(),
|
||||
'ai_translate_route_url' => get_rest_url(null, 'atfpp-translate'),
|
||||
'ai_translate_route_nonce' => wp_create_nonce('wp_rest'),
|
||||
'ai_translate_nonce' => wp_create_nonce('atfp_ai_translate_nonce'),
|
||||
'post_label' => $post_label,
|
||||
'update_translate_data' => 'atfp_update_translate_data',
|
||||
'slug_translation_option' => $slug_translation_option,
|
||||
'taxonomy_page' => $taxonomy_page,
|
||||
'AIRequestMaxTokens' => $ai_max_tokens,
|
||||
'AIRequestBatchSize' => $ai_batch_size,
|
||||
'atfpp_glossary_nonce' => wp_create_nonce('atfpp_glossary_nonce'),
|
||||
), $extra_data)
|
||||
);
|
||||
}
|
||||
|
||||
public function enqueue_elementor_confirm_box_assets($parent_post_id, $target_lang_name, $source_lang_name, $editor_type='gutenberg')
|
||||
{
|
||||
$post_id = get_the_ID();
|
||||
|
||||
$source_lang_name=PLL()->model->get_language($source_lang_name);
|
||||
$target_lang_name=PLL()->model->get_language($target_lang_name);
|
||||
|
||||
wp_enqueue_script('atfp-elementor-confirm-box', ATFPP_URL . 'assets/js/atfp-elementor-translate-confirm-box.js', array('jquery', 'wp-i18n'), ATFPP_V, true);
|
||||
|
||||
wp_localize_script('atfp-elementor-confirm-box', 'atfpElementorConfirmBoxData',
|
||||
array('postId' => $post_id, 'parentPostId' => $parent_post_id, 'sourceLangSlug' => $source_lang_name->slug, 'targetLangSlug' => $target_lang_name->slug, 'sourceLangName' => $source_lang_name->name, 'targetLangName' => $target_lang_name->name, 'editorType' => $editor_type)
|
||||
);
|
||||
|
||||
wp_enqueue_style('atfp-elementor-confirm-box', ATFPP_URL . 'assets/css/atfp-elementor-translate-confirm-box.css', array(), ATFPP_V);
|
||||
}
|
||||
|
||||
private function enqueue_inline_translation_assets( $type = 'block', $extra_dependencies = array() ) {
|
||||
|
||||
global $post;
|
||||
|
||||
if(!isset($post) || !isset($post->ID)){
|
||||
return;
|
||||
}
|
||||
|
||||
if (defined('POLYLANG_VERSION')) {
|
||||
if (function_exists('pll_current_language')) {
|
||||
$current_language = pll_current_language();
|
||||
$current_language_name = pll_current_language('name');
|
||||
} else {
|
||||
$current_language = '';
|
||||
$current_language_name = '';
|
||||
}
|
||||
|
||||
$editor_script_asset = require_once ATFPP_DIR_PATH . 'assets/'.sanitize_file_name( $type ).'-inline-translation/index.asset.php';
|
||||
$core_modal_script_asset = include ATFPP_DIR_PATH . 'assets/inline-translate-modal/index.asset.php';
|
||||
|
||||
if(!is_array($editor_script_asset)) {
|
||||
$editor_script_asset = array(
|
||||
'dependencies' => array(),
|
||||
'version' => ATFPP_V,
|
||||
);
|
||||
}
|
||||
|
||||
if(!is_array($core_modal_script_asset)) {
|
||||
$core_modal_script_asset = array(
|
||||
'dependencies' => array(),
|
||||
'version' => ATFPP_V,
|
||||
);
|
||||
}
|
||||
|
||||
wp_register_script( 'atfp-inline-translate-modal', ATFPP_URL . 'assets/inline-translate-modal/index.js' , array_merge( $core_modal_script_asset['dependencies'] ), $core_modal_script_asset['version'], true );
|
||||
|
||||
$extra_dependencies[] = 'atfp-inline-translate-modal';
|
||||
|
||||
wp_register_script(
|
||||
'atfp-'.sanitize_file_name( $type ).'-inline-translation',
|
||||
ATFPP_URL . 'assets/'.sanitize_file_name( $type ).'-inline-translation/index.js',
|
||||
array_merge(
|
||||
$editor_script_asset['dependencies'], $extra_dependencies
|
||||
),
|
||||
$editor_script_asset['version'],
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'atfp-inline-translate-modal' );
|
||||
|
||||
wp_enqueue_script('atfp-' . sanitize_file_name( $type ) . '-inline-translation');
|
||||
|
||||
if ($current_language && $current_language !== '') {
|
||||
wp_localize_script(
|
||||
'atfp-inline-translate-modal',
|
||||
'atfpInlineTranslation',
|
||||
array(
|
||||
'pageLanguage' => $current_language,
|
||||
'pageLanguageName' => $current_language_name,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Do not access the page directly
|
||||
*/
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
if ( ! class_exists( 'ATFPP_Elementor_Translate' ) ) {
|
||||
/**
|
||||
* Class ATFPP_Elementor_Translate
|
||||
*
|
||||
* This class handles the tran;ation for the Elementor Pages.
|
||||
*
|
||||
* @package ATFPP
|
||||
*/
|
||||
class ATFPP_Elementor_Translate{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var ATFPP_Elementor_Translate
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*
|
||||
* @return ATFPP_Elementor_Translate
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action('add_meta_boxes', array($this, 'atfpp_elementor_post_languages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the languages for translation of elementor pages.
|
||||
*/
|
||||
function atfpp_elementor_post_languages() {
|
||||
if ( isset( $_GET['_wpnonce'] ) &&
|
||||
wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'new-post-translation' ) ) {
|
||||
if ( function_exists( 'PLL' ) ) {
|
||||
global $post;
|
||||
$current_post_id = $post->ID;
|
||||
|
||||
$parent_post_id = absint(isset( $_GET['from_post'] ) ? sanitize_key( $_GET['from_post'] ) : '');
|
||||
$parent_editor=get_post_meta($parent_post_id, '_elementor_edit_mode', true);
|
||||
$parent_elementor_data = get_post_meta( $parent_post_id, '_elementor_data', true );
|
||||
|
||||
if($parent_editor === 'builder' || !empty($parent_elementor_data)){
|
||||
// Delete this old post meta data
|
||||
delete_post_meta( $parent_post_id, 'atfpp_elementor_translated' );
|
||||
delete_post_meta( $parent_post_id, 'atfp_parent_post_language_slug' );
|
||||
delete_post_meta( $current_post_id, 'atfpp_elementor_translated' );
|
||||
delete_post_meta( $current_post_id, 'atfp_parent_post_language_slug' );
|
||||
|
||||
$parent_post_language_slug = pll_get_post_language( $parent_post_id, 'slug' );
|
||||
update_post_meta( $current_post_id, '_atfp_parent_post_language_slug', $parent_post_language_slug );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user