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:
2025-12-28 12:44:00 +01:00
parent cf600ae727
commit cd264483f8
410 changed files with 60841 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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