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,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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user