Files
Jacek Pyziak cd264483f8 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.
2025-12-28 12:44:00 +01:00

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