- 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.
392 lines
14 KiB
PHP
392 lines
14 KiB
PHP
<?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 '';
|
|
}
|
|
}
|