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,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Candidates_Stream_Processor
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Class to process a candidates stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
final class Candidates_Stream_Processor {
|
||||
|
||||
/**
|
||||
* Generator that yields the chunks of response candidates.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Generator<Candidates>
|
||||
*/
|
||||
private $generator;
|
||||
|
||||
/**
|
||||
* The overall candidates instance.
|
||||
*
|
||||
* May be incomplete if the stream has not been fully processed yet.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Candidates|null
|
||||
*/
|
||||
private $candidates;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Generator<Candidates> $generator The generator that yields the chunks of response candidates.
|
||||
*/
|
||||
public function __construct( Generator $generator ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
|
||||
$this->generator = $generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all chunks from the generator and adds them to the overall candidates instance.
|
||||
*
|
||||
* A callback can be passed that is called for each chunk of candidates. You could use such a callback for example
|
||||
* to echo the text contents of each chunk as they are being processed.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param callable|null $chunk_callback Optional. Callback that is called for each chunk of candidates.
|
||||
* @return Candidates The complete candidates instance.
|
||||
*/
|
||||
public function read_all( ?callable $chunk_callback = null ): Candidates {
|
||||
foreach ( $this->generator as $candidates ) {
|
||||
$this->add_chunk( $candidates );
|
||||
if ( null !== $chunk_callback ) {
|
||||
$chunk_callback( $candidates );
|
||||
}
|
||||
}
|
||||
return $this->get_complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a chunk of candidates to the overall candidates instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Candidates $candidates The chunk of candidates to add.
|
||||
*/
|
||||
public function add_chunk( Candidates $candidates ): void {
|
||||
if ( null === $this->candidates ) {
|
||||
$this->candidates = $candidates;
|
||||
return;
|
||||
}
|
||||
|
||||
$existing_candidates = $this->candidates->to_array();
|
||||
$new_candidates = $candidates->to_array();
|
||||
|
||||
foreach ( $new_candidates as $index => $new_candidate ) {
|
||||
if ( ! isset( $existing_candidates[ $index ] ) ) {
|
||||
$existing_candidates[] = $new_candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $existing_candidates[ $index ]['content'] ) && isset( $new_candidate['content'] ) ) {
|
||||
$existing_candidates[ $index ]['content'] = $this->append_content(
|
||||
$existing_candidates[ $index ]['content'],
|
||||
$new_candidate['content']
|
||||
);
|
||||
unset( $new_candidate['content'] );
|
||||
}
|
||||
|
||||
$existing_candidates[ $index ] = array_merge( $existing_candidates[ $index ], $new_candidate );
|
||||
}
|
||||
|
||||
$this->candidates = Candidates::from_array( $existing_candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the complete candidates instance.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Candidates|null The complete candidates instance, or null if the generator is not done yet.
|
||||
*/
|
||||
public function get_complete(): ?Candidates {
|
||||
// Only return the candidates if the generator is done.
|
||||
if ( $this->generator->valid() ) {
|
||||
return null;
|
||||
}
|
||||
return $this->candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the content of a new candidate to the content of an existing candidate.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $existing_content The existing content data.
|
||||
* @param array<string, mixed> $new_content The new content data.
|
||||
* @return array<string, mixed> The combined content data.
|
||||
*/
|
||||
private function append_content( array $existing_content, array $new_content ) {
|
||||
if ( ! isset( $existing_content['parts'] ) || ! isset( $new_content['parts'] ) ) {
|
||||
return $existing_content;
|
||||
}
|
||||
|
||||
foreach ( $new_content['parts'] as $index => $new_part ) {
|
||||
if ( ! isset( $existing_content['parts'][ $index ] ) ) {
|
||||
$existing_content['parts'][] = $new_part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $existing_content['parts'][ $index ]['text'] ) || ! isset( $new_part['text'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing_content['parts'][ $index ]['text'] .= $new_part['text'];
|
||||
}
|
||||
|
||||
return $existing_content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\AI_Capability
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the AI capability enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class AI_Capability extends Abstract_Enum {
|
||||
|
||||
const CHAT_HISTORY = 'chat_history';
|
||||
const FUNCTION_CALLING = 'function_calling';
|
||||
const IMAGE_GENERATION = 'image_generation';
|
||||
const MULTIMODAL_INPUT = 'multimodal_input';
|
||||
const MULTIMODAL_OUTPUT = 'multimodal_output';
|
||||
const TEXT_GENERATION = 'text_generation';
|
||||
const TEXT_TO_SPEECH = 'text_to_speech';
|
||||
const WEB_SEARCH = 'web_search';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::CHAT_HISTORY,
|
||||
self::FUNCTION_CALLING,
|
||||
self::IMAGE_GENERATION,
|
||||
self::MULTIMODAL_INPUT,
|
||||
self::MULTIMODAL_OUTPUT,
|
||||
self::TEXT_GENERATION,
|
||||
self::TEXT_TO_SPEECH,
|
||||
self::WEB_SEARCH,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Abstract_Enum
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Contracts\Enum;
|
||||
|
||||
/**
|
||||
* Base class for an enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
abstract class Abstract_Enum implements Enum {
|
||||
|
||||
/**
|
||||
* The value map, to store in memory which values are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, array<string, bool>>
|
||||
*/
|
||||
private static $value_map = array();
|
||||
|
||||
/**
|
||||
* Checks if the given value is valid for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $value The value to check.
|
||||
* @return bool True if the value is valid, false otherwise.
|
||||
*/
|
||||
final public static function is_valid_value( string $value ): bool {
|
||||
$value_map = self::get_value_map_for_class( static::class );
|
||||
return isset( $value_map[ $value ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of valid values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of valid values.
|
||||
*/
|
||||
final public static function get_values(): array {
|
||||
$value_map = self::get_value_map_for_class( static::class );
|
||||
return array_keys( $value_map );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value map for the given child class name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $class_name The child class name.
|
||||
* @return array<string, bool> The value map.
|
||||
*/
|
||||
private static function get_value_map_for_class( string $class_name ): array {
|
||||
if ( ! isset( self::$value_map[ $class_name ] ) ) {
|
||||
self::$value_map[ $class_name ] = array_fill_keys( call_user_func( array( $class_name, 'get_all_values' ) ), true );
|
||||
}
|
||||
return self::$value_map[ $class_name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
abstract protected static function get_all_values(): array;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Content_Role
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the content role enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Content_Role extends Abstract_Enum {
|
||||
|
||||
const USER = 'user';
|
||||
const MODEL = 'model';
|
||||
const SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::USER,
|
||||
self::MODEL,
|
||||
self::SYSTEM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Enums\Contracts\Enum
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a class for an enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
interface Enum {
|
||||
|
||||
/**
|
||||
* Checks if the given value is valid for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $value The value to check.
|
||||
* @return bool True if the value is valid, false otherwise.
|
||||
*/
|
||||
public static function is_valid_value( string $value ): bool;
|
||||
|
||||
/**
|
||||
* Gets the list of valid values for the enum.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The list of valid values.
|
||||
*/
|
||||
public static function get_values(): array;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Modality
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the modality enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Modality extends Abstract_Enum {
|
||||
|
||||
const TEXT = 'text';
|
||||
const IMAGE = 'image';
|
||||
const AUDIO = 'audio';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::TEXT,
|
||||
self::IMAGE,
|
||||
self::AUDIO,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Enums\Service_Type
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Enums;
|
||||
|
||||
/**
|
||||
* Class for the service type enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Type extends Abstract_Enum {
|
||||
|
||||
const CLOUD = 'cloud';
|
||||
const SERVER = 'server';
|
||||
const CLIENT = 'client';
|
||||
|
||||
/**
|
||||
* Gets all values for the enum.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The list of all values.
|
||||
*/
|
||||
protected static function get_all_values(): array {
|
||||
return array(
|
||||
self::CLOUD,
|
||||
self::SERVER,
|
||||
self::CLIENT,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Helpers
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Blob;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Repository;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static helper methods as part of the public API.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
|
||||
*/
|
||||
final class Helpers {
|
||||
|
||||
/**
|
||||
* Converts a text string to a Content instance.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $text The text.
|
||||
* @param string $role Optional. The role to use for the content. Default 'user'.
|
||||
* @return Content The content instance.
|
||||
*/
|
||||
public static function text_to_content( string $text, string $role = Content_Role::USER ): Content {
|
||||
return Formatter::format_content( $text, $role );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Content instance to a text string.
|
||||
*
|
||||
* This method will return the combined text from all consecutive text parts in the content.
|
||||
* Realistically, this should almost always return the text from just one part, as API responses typically do not
|
||||
* contain multiple text parts in a row - but it might be possible.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content $content The content instance.
|
||||
* @return string The text, or an empty string if there are no text parts.
|
||||
*/
|
||||
public static function content_to_text( Content $content ): string {
|
||||
$parts = $content->get_parts();
|
||||
|
||||
$text_parts = array();
|
||||
foreach ( $parts as $part ) {
|
||||
/*
|
||||
* If there is any non-text part present, we want to ensure that no interrupted text content is returned.
|
||||
* Therefore, we break the loop as soon as we encounter a non-text part, unless no text parts have been
|
||||
* found yet, in which case the text may only start with a later part.
|
||||
*/
|
||||
if ( ! $part instanceof Text_Part ) {
|
||||
if ( count( $text_parts ) > 0 ) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$text_parts[] = trim( $part->get_text() );
|
||||
}
|
||||
|
||||
if ( count( $text_parts ) === 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode( "\n\n", $text_parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text from the first Content instance in the given list which contains text.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content[] $contents The list of Content instances.
|
||||
* @return string The text, or an empty string if no Content instance has text parts.
|
||||
*/
|
||||
public static function get_text_from_contents( array $contents ): string {
|
||||
foreach ( $contents as $content ) {
|
||||
$text = self::content_to_text( $content );
|
||||
if ( '' !== $text ) {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first Content instance in the given list which contains text.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Content[] $contents The list of Content instances.
|
||||
* @return Content|null The Content instance, or null if no Content instance has text parts.
|
||||
*/
|
||||
public static function get_text_content_from_contents( array $contents ): ?Content {
|
||||
foreach ( $contents as $content ) {
|
||||
$text = self::content_to_text( $content );
|
||||
if ( '' !== $text ) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Content instances for each candidate in the given list.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Candidates $candidates The list of candidates.
|
||||
* @return Content[] The list of Content instances.
|
||||
*/
|
||||
public static function get_candidate_contents( Candidates $candidates ): array {
|
||||
$contents = array();
|
||||
|
||||
foreach ( $candidates as $candidate ) {
|
||||
$content = $candidate->get_content();
|
||||
if ( ! $content ) {
|
||||
continue;
|
||||
}
|
||||
$contents[] = $content;
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a stream of candidates, aggregating the candidates chunks into a single candidates instance.
|
||||
*
|
||||
* This method returns a stream processor instance that can be used to read all chunks from the given candidates
|
||||
* generator and process them with a callback. Alternatively, you can read from the generator yourself and provide
|
||||
* all chunks to the processor manually.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Generator<Candidates> $generator The generator that yields the chunks of response candidates.
|
||||
* @return Candidates_Stream_Processor The stream processor instance.
|
||||
*/
|
||||
public static function process_candidates_stream( Generator $generator ): Candidates_Stream_Processor { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
|
||||
return new Candidates_Stream_Processor( $generator );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base64-encoded data URL representation of the given file URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file Absolute path to the file, or its URL.
|
||||
* @param string $mime_type Optional. The MIME type of the file. If provided, the base64-encoded data URL will
|
||||
* be prefixed with `data:{mime_type};base64,`. Default empty string.
|
||||
* @return string The base64-encoded file data URL, or empty string on failure.
|
||||
*/
|
||||
public static function file_to_base64_data_url( string $file, string $mime_type = '' ): string {
|
||||
$blob = self::file_to_blob( $file, $mime_type );
|
||||
if ( ! $blob ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::blob_to_base64_data_url( $blob );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary data blob representation of the given file URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file Absolute path to the file, or its URL.
|
||||
* @param string $mime_type Optional. The MIME type of the file. If provided, the automatically detected MIME type
|
||||
* will be overwritten. Default empty string.
|
||||
* @return Blob|null The binary data blob, or null on failure.
|
||||
*/
|
||||
public static function file_to_blob( string $file, string $mime_type = '' ): ?Blob {
|
||||
try {
|
||||
return Blob::from_file( $file, $mime_type );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base64-encoded data URL representation of the given binary data blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Blob $blob The binary data blob.
|
||||
* @return string The base64-encoded file data URL, or empty string on failure.
|
||||
*/
|
||||
public static function blob_to_base64_data_url( Blob $blob ): string {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
$base64 = base64_encode( $blob->get_binary_data() );
|
||||
$mime_type = $blob->get_mime_type();
|
||||
return "data:$mime_type;base64,$base64";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary data blob representation of the given base64-encoded data URL.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $base64_data_url The base64-encoded data URL.
|
||||
* @return Blob|null The binary data blob, or null on failure.
|
||||
*/
|
||||
public static function base64_data_url_to_blob( string $base64_data_url ): ?Blob {
|
||||
if ( ! preg_match( '/^data:([a-z0-9-]+\/[a-z0-9-]+);base64,/', $base64_data_url, $matches ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$base64 = substr( $base64_data_url, strlen( $matches[0] ) );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$binary_data = base64_decode( $base64 );
|
||||
if ( false === $binary_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Blob( $binary_data, $matches[1] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the given base64 data is prefixed correctly to be a data URL.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param string $base64_data Base64-encoded data. If it is already a data URL, it will be returned as is.
|
||||
* @param string $mime_type MIME type for the data.
|
||||
* @return string The base64 data URL.
|
||||
*/
|
||||
public static function base64_data_to_base64_data_url( string $base64_data, string $mime_type ): string {
|
||||
if ( str_starts_with( $base64_data, 'data:' ) ) {
|
||||
return $base64_data;
|
||||
}
|
||||
|
||||
return 'data:' . $mime_type . ';base64,' . $base64_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the given base64 data URL has its prefix removed to be just the base64 data.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param string $base64_data_url Base64 data URL. If it is already without prefix, it will be returned as is.
|
||||
* @return string The base64-encoded data.
|
||||
*/
|
||||
public static function base64_data_url_to_base64_data( string $base64_data_url ): string {
|
||||
if ( ! str_starts_with( $base64_data_url, 'data:' ) ) {
|
||||
return $base64_data_url;
|
||||
}
|
||||
|
||||
return preg_replace(
|
||||
'/^data:[a-z0-9-]+\/[a-z0-9-]+;base64,/',
|
||||
'',
|
||||
$base64_data_url
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Blob
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Simple value class representing a binary data blob, e.g. from a file.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Blob {
|
||||
|
||||
/**
|
||||
* The binary data of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $binary_data;
|
||||
|
||||
/**
|
||||
* The MIME type of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $mime_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $binary_data The binary data of the blob.
|
||||
* @param string $mime_type The MIME type of the blob.
|
||||
*/
|
||||
public function __construct( string $binary_data, string $mime_type ) {
|
||||
$this->binary_data = $binary_data;
|
||||
$this->mime_type = $mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the binary data of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The binary data.
|
||||
*/
|
||||
public function get_binary_data(): string {
|
||||
return $this->binary_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the MIME type of the blob.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The MIME type.
|
||||
*/
|
||||
public function get_mime_type(): string {
|
||||
return $this->mime_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blob instance from a file.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $file The file path or URL.
|
||||
* @param string $mime_type Optional. MIME type, to override the automatic detection. Default empty string.
|
||||
* @return Blob The blob instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the file could not be read or if the MIME type cannot be determined.
|
||||
*/
|
||||
public static function from_file( string $file, string $mime_type = '' ): self {
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$blob = file_get_contents( $file );
|
||||
if ( ! $blob ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Could not read file %s.',
|
||||
htmlspecialchars( $file ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $mime_type ) {
|
||||
$file_type = wp_check_filetype( $file );
|
||||
if ( ! $file_type['type'] ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Could not determine MIME type of file %s.',
|
||||
htmlspecialchars( $file ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$mime_type = $file_type['type'];
|
||||
}
|
||||
|
||||
return new self( $blob, $mime_type );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Candidate
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a candidate for a content response from a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Candidate implements Arrayable {
|
||||
|
||||
/**
|
||||
* The content, unless no content is available as part of the candidate.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var ?Content
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* Additional data for the candidate, if any.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param ?Content $content The content, or null to indicate no content is available.
|
||||
* @param array<string, mixed> $additional_data Additional data for the candidate, if any.
|
||||
*/
|
||||
public function __construct( ?Content $content, array $additional_data = array() ) {
|
||||
$this->content = $content;
|
||||
|
||||
// Remove the content from the additional data, if present, to prevent conflicts.
|
||||
unset( $additional_data['content'] );
|
||||
$this->additional_data = $additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ?Content The content.
|
||||
*/
|
||||
public function get_content(): ?Content {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a field value from the additional data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $field The field name.
|
||||
* @return mixed|null The field value, or null if not found.
|
||||
*/
|
||||
public function get_field_value( string $field ) {
|
||||
if ( isset( $this->additional_data[ $field ] ) ) {
|
||||
return $this->additional_data[ $field ];
|
||||
}
|
||||
|
||||
if ( str_contains( $field, '_' ) ) {
|
||||
$camel_case_field = $this->underscore_to_camel_case( $field );
|
||||
if ( isset( $this->additional_data[ $camel_case_field ] ) ) {
|
||||
return $this->additional_data[ $camel_case_field ];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A few common special cases.
|
||||
* For instance, "finish_reason" is sometimes called "stop_reason".
|
||||
*/
|
||||
switch ( $field ) {
|
||||
case 'finish_reason':
|
||||
return $this->get_field_value( 'stop_reason' );
|
||||
case 'finishReason':
|
||||
return $this->get_field_value( 'stopReason' );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> The additional data.
|
||||
*/
|
||||
public function get_additional_data(): array {
|
||||
return $this->additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_merge(
|
||||
array(
|
||||
'content' => $this->content ? $this->content->to_array() : null,
|
||||
),
|
||||
$this->additional_data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Candidate instance from an array of content data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Candidate Candidate instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Candidate {
|
||||
if ( ! isset( $data['content'] ) ) {
|
||||
return new Candidate( null, $data );
|
||||
}
|
||||
|
||||
/*
|
||||
* Apparently, the API sometimes omits this.
|
||||
* Given candidates are always part of a model response, we can safely assume the role is 'model'.
|
||||
*/
|
||||
if ( ! isset( $data['content']['role'] ) ) {
|
||||
$data['content']['role'] = Content_Role::MODEL;
|
||||
}
|
||||
|
||||
$content = Content::from_array( $data['content'] );
|
||||
unset( $data['content'] );
|
||||
|
||||
return new Candidate( $content, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'content' => array_merge(
|
||||
array(
|
||||
'description' => __( 'Candidate content.', 'ai-services' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
Content::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a snake_case string to camelCase.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $input The snake_case string.
|
||||
* @return string The camelCase string.
|
||||
*/
|
||||
private function underscore_to_camel_case( string $input ): string {
|
||||
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Candidates
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of response candidates for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Candidates implements Collection, Arrayable {
|
||||
|
||||
/**
|
||||
* The candidates.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Candidate[]
|
||||
*/
|
||||
private $candidates = array();
|
||||
|
||||
/**
|
||||
* Adds a candidate to the collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Candidate $candidate The candidate.
|
||||
*/
|
||||
public function add_candidate( Candidate $candidate ): void {
|
||||
$this->candidates[] = $candidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the candidates collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ArrayIterator<int, Candidate> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the candidates collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->candidates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the parts collection by the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The filter arguments.
|
||||
*
|
||||
* @type string $part_class_name The class name to only allow candidates with content parts of that class.
|
||||
* }
|
||||
* @return Candidates The filtered parts collection.
|
||||
*/
|
||||
public function filter( array $args ): self {
|
||||
if ( isset( $args['part_class_name'] ) ) {
|
||||
$part_class_name = $args['part_class_name'];
|
||||
$map = static function ( Candidate $candidate ) use ( $part_class_name ) {
|
||||
$candidate_content = $candidate->get_content();
|
||||
if ( ! $candidate_content ) {
|
||||
return null;
|
||||
}
|
||||
$filtered_parts = $candidate_content->get_parts()->filter( array( 'class_name' => $part_class_name ) );
|
||||
if ( count( $filtered_parts ) > 0 ) {
|
||||
$candidate_data = $candidate->to_array();
|
||||
$candidate_data['content']['parts'] = $filtered_parts->to_array();
|
||||
return Candidate::from_array( $candidate_data );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
$map = static function ( Candidate $candidate ) {
|
||||
return Candidate::from_array( $candidate->to_array() );
|
||||
};
|
||||
}
|
||||
|
||||
$candidates = new Candidates();
|
||||
foreach ( $this->candidates as $candidate ) {
|
||||
$mapped_candidate = $map( $candidate );
|
||||
if ( $mapped_candidate ) {
|
||||
$candidates->add_candidate( $mapped_candidate );
|
||||
}
|
||||
}
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the candidate at the given index.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Candidate The candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Candidate {
|
||||
if ( ! isset( $this->candidates[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->candidates[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed>[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Candidate $candidate ) {
|
||||
return $candidate->to_array();
|
||||
},
|
||||
$this->candidates
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Candidates instance from an array of candidates data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed>[] $data The candidates data.
|
||||
* @return Candidates The Candidates instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the candidates data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Candidates {
|
||||
$candidates = new Candidates();
|
||||
|
||||
foreach ( $data as $candidate ) {
|
||||
if ( ! is_array( $candidate ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid candidate data.' );
|
||||
}
|
||||
|
||||
$candidates->add_candidate( Candidate::from_array( $candidate ) );
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Content
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing an entry of content for a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Content implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The role of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $role;
|
||||
|
||||
/**
|
||||
* The parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Parts
|
||||
*/
|
||||
private $parts;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $role The role of the content.
|
||||
* @param Parts $parts The parts of the content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given role is invalid.
|
||||
*/
|
||||
public function __construct( string $role, Parts $parts ) {
|
||||
if ( ! Content_Role::is_valid_value( $role ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The role %s is invalid.',
|
||||
htmlspecialchars( $role ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->role = $role;
|
||||
$this->parts = $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The role of the content.
|
||||
*/
|
||||
public function get_role(): string {
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Parts The parts of the content.
|
||||
*/
|
||||
public function get_parts(): Parts {
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'role' => $this->role,
|
||||
'parts' => $this->parts->to_array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Content instance from an array of content data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Content Content instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Content {
|
||||
if ( ! isset( $data['role'], $data['parts'] ) ) {
|
||||
throw new InvalidArgumentException( 'Content data must contain role and parts.' );
|
||||
}
|
||||
|
||||
return new Content( $data['role'], Parts::from_array( $data['parts'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'role' => array(
|
||||
'description' => __( 'The role of the content, i.e. which source it comes from.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
Content_Role::USER,
|
||||
Content_Role::MODEL,
|
||||
Content_Role::SYSTEM,
|
||||
),
|
||||
),
|
||||
'parts' => array_merge(
|
||||
array( 'description' => __( 'Content parts, including optional multimodal input.', 'ai-services' ) ),
|
||||
Parts::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Types\Contracts\Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Part extends Arrayable {
|
||||
|
||||
/**
|
||||
* Sets data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
*/
|
||||
public function set_data( array $data ): void;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\API\Types\Contracts\Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
interface Tool extends Arrayable {
|
||||
|
||||
/**
|
||||
* Sets data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
*/
|
||||
public function set_data( array $data ): void;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\History
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a chat history.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class History {
|
||||
|
||||
/**
|
||||
* The feature the history is associated with.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $feature;
|
||||
|
||||
/**
|
||||
* The history slug, unique within the feature.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* When the history was last updated, as MySQL datetime string in GMT.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var string
|
||||
*/
|
||||
private $last_updated;
|
||||
|
||||
/**
|
||||
* The history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var History_Entry[]
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $feature The feature the history is associated with.
|
||||
* @param string $slug The history slug.
|
||||
* @param string $last_updated When the history was last updated, as MySQL datetime string in GMT.
|
||||
* @param History_Entry[] $entries The history entries.
|
||||
*/
|
||||
public function __construct( string $feature, string $slug, string $last_updated, array $entries ) {
|
||||
$this->feature = $feature;
|
||||
$this->slug = $slug;
|
||||
$this->last_updated = $last_updated;
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the feature the history is associated with.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The feature.
|
||||
*/
|
||||
public function get_feature(): string {
|
||||
return $this->feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history slug.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The history slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets when the history was last updated.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The last updated MySQL datetime string in GMT.
|
||||
*/
|
||||
public function get_last_updated(): string {
|
||||
return $this->last_updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return History_Entry[] The history entries.
|
||||
*/
|
||||
public function get_entries(): array {
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the history entries.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param History_Entry[]|array<string, mixed>[] $entries The history entries.
|
||||
*/
|
||||
public function set_entries( array $entries ): void {
|
||||
$this->entries = array_map(
|
||||
function ( $entry_data ) {
|
||||
if ( ! $entry_data instanceof History_Entry ) {
|
||||
return History_Entry::from_array( $entry_data );
|
||||
}
|
||||
return $entry_data;
|
||||
},
|
||||
$entries
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'feature' => $this->feature,
|
||||
'slug' => $this->slug,
|
||||
'lastUpdated' => $this->last_updated,
|
||||
'entries' => array_map(
|
||||
function ( History_Entry $entry ) {
|
||||
return $entry->to_array();
|
||||
},
|
||||
$this->entries
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a History instance from an array of history data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The history data.
|
||||
* @return History History instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): History {
|
||||
if ( ! isset( $data['feature'], $data['slug'], $data['lastUpdated'], $data['entries'] ) ) {
|
||||
throw new InvalidArgumentException( 'History data must contain feature, slug, lastUpdated, and entries.' );
|
||||
}
|
||||
|
||||
return new History(
|
||||
$data['feature'],
|
||||
$data['slug'],
|
||||
$data['lastUpdated'],
|
||||
array_map(
|
||||
function ( array $entry_data ) {
|
||||
return History_Entry::from_array( $entry_data );
|
||||
},
|
||||
$data['entries']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'feature' => array(
|
||||
'description' => __( 'Unique identifier of the feature. Must only contain lowercase letters, numbers, hyphens.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique identifier of the history within the feature. Must only contain lowercase letters, numbers, hyphens.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'lastUpdated' => array(
|
||||
'description' => __( 'When the history was last updated, as MySQL datetime string in GMT.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'entries' => array(
|
||||
'description' => __( 'The history entries, in ascending order.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => History_Entry::get_json_schema(),
|
||||
'context' => array( 'view', 'edit' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\History_Entry
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing a single entry in a chat history.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class History_Entry {
|
||||
|
||||
/**
|
||||
* The history entry's content.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var Content
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* Additional data for the history entry, if any.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_data;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Content $content The history entry content.
|
||||
* @param array<string, mixed> $additional_data Additional data for the history entry, if any.
|
||||
*/
|
||||
public function __construct( Content $content, array $additional_data = array() ) {
|
||||
$this->content = $content;
|
||||
|
||||
// Remove the content from the additional data, if present, to prevent conflicts.
|
||||
unset( $additional_data['content'] );
|
||||
$this->additional_data = $additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the history entry content.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return Content The content.
|
||||
*/
|
||||
public function get_content(): Content {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the additional data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The additional data.
|
||||
*/
|
||||
public function get_additional_data(): array {
|
||||
return $this->additional_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the candidate to an array.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation of the candidate.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_merge(
|
||||
array(
|
||||
'content' => $this->content->to_array(),
|
||||
),
|
||||
$this->additional_data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a History_Entry instance from an array of content data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return History_Entry History_Entry instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): History_Entry {
|
||||
if ( ! isset( $data['content'] ) ) {
|
||||
throw new InvalidArgumentException( 'History entry data must contain content.' );
|
||||
}
|
||||
|
||||
$content = Content::from_array( $data['content'] );
|
||||
unset( $data['content'] );
|
||||
|
||||
return new History_Entry( $content, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'content' => array_merge(
|
||||
array(
|
||||
'description' => __( 'History entry content.', 'ai-services' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
Content::get_json_schema()
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Model_Metadata
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value class representing metadata about a generative AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Model_Metadata implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The model slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The model name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* List of AI capabilities supported by the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $capabilities;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The arguments for the model metadata.
|
||||
*
|
||||
* @type string $slug The model slug.
|
||||
* @type string $name Optional. The model name. Default will be generated from the slug.
|
||||
* @type string[] $capabilities Optional. The list of AI capabilities supported by the model.
|
||||
* Default empty array.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given slug is invalid.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$args = $this->parse_args( $args );
|
||||
|
||||
$this->slug = $args['slug'];
|
||||
$this->name = $args['name'];
|
||||
$this->capabilities = $args['capabilities'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The model name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of AI capabilities supported by the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] List of AI capabilities supported by the model.
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'capabilities' => $this->capabilities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Model_Metadata instance from an array of model metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The model metadata arguments.
|
||||
* @return Model_Metadata The Model_Metadata instance.
|
||||
*/
|
||||
public static function from_array( array $args ): Model_Metadata {
|
||||
return new Model_Metadata( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the model metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The model metadata arguments.
|
||||
* @return array<string, mixed> The parsed model metadata arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid argument is provided.
|
||||
*/
|
||||
private function parse_args( array $args ): array {
|
||||
if ( ! isset( $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException( 'The slug is required.' );
|
||||
}
|
||||
|
||||
if ( isset( $args['name'] ) ) {
|
||||
$args['name'] = (string) $args['name'];
|
||||
} else {
|
||||
$args['name'] = ucwords( str_replace( array( '-', '_' ), ' ', $args['slug'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
if ( ! is_array( $args['capabilities'] ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities must be an array.' );
|
||||
}
|
||||
foreach ( $args['capabilities'] as $capability ) {
|
||||
if ( ! AI_Capability::is_valid_value( $capability ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities contain an invalid value.' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$args['capabilities'] = array();
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique model slug.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'User-facing model name.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'description' => __( 'List of AI capabilities supported by the model.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => AI_Capability::get_values(),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of content parts for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Parts implements Collection, Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The parts of the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Part[]
|
||||
*/
|
||||
private $parts = array();
|
||||
|
||||
/**
|
||||
* Adds a text part to the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $text The text.
|
||||
*/
|
||||
public function add_text_part( string $text ): void {
|
||||
$this->add_part(
|
||||
Text_Part::from_array( array( 'text' => $text ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function call part to the content.
|
||||
*
|
||||
* Every function call must have at least one of $id and $name provided.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $id The ID of the function call, or an empty string.
|
||||
* @param string $name The name of the function, or an empty string.
|
||||
* @param array<string, mixed> $args The arguments of the function call.
|
||||
*/
|
||||
public function add_function_call_part( string $id, string $name, array $args ): void {
|
||||
$data = array();
|
||||
if ( $id ) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
if ( $name ) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
$data['args'] = $args;
|
||||
|
||||
$this->add_part(
|
||||
Function_Call_Part::from_array(
|
||||
array( 'functionCall' => $data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function response part to the content.
|
||||
*
|
||||
* Every function response must have at least one of $id and $name provided.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string $id The ID of the function response, or an empty string. If present, this must match the
|
||||
* function call ID.
|
||||
* @param string $name The name of the function, or an empty string. If present, this must match the name of
|
||||
* the function called.
|
||||
* @param mixed $response The function output response.
|
||||
*/
|
||||
public function add_function_response_part( string $id, string $name, $response ): void {
|
||||
$data = array();
|
||||
if ( $id ) {
|
||||
$data['id'] = $id;
|
||||
}
|
||||
if ( $name ) {
|
||||
$data['name'] = $name;
|
||||
}
|
||||
$data['response'] = $response;
|
||||
|
||||
$this->add_part(
|
||||
Function_Response_Part::from_array(
|
||||
array( 'functionResponse' => $data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a part to the content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Part $part The part.
|
||||
*/
|
||||
public function add_part( Part $part ): void {
|
||||
$this->parts[] = $part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the parts collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return ArrayIterator<int, Part> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the parts collection.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the parts collection by the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The filter arguments.
|
||||
*
|
||||
* @type string $class_name The class name to only allow parts of that class.
|
||||
* }
|
||||
* @return Parts The filtered parts collection.
|
||||
*/
|
||||
public function filter( array $args ): self {
|
||||
if ( isset( $args['class_name'] ) ) {
|
||||
$class_name = $args['class_name'];
|
||||
$map = static function ( Part $part ) use ( $class_name ) {
|
||||
if ( $part instanceof $class_name ) {
|
||||
return call_user_func( array( $class_name, 'from_array' ), $part->to_array() );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
$map = static function ( Part $part ) {
|
||||
return call_user_func( array( get_class( $part ), 'from_array' ), $part->to_array() );
|
||||
};
|
||||
}
|
||||
|
||||
$parts = new Parts();
|
||||
foreach ( $this->parts as $part ) {
|
||||
$mapped_part = $map( $part );
|
||||
if ( $mapped_part ) {
|
||||
$parts->add_part( $mapped_part );
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the part at the given index.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Part The part.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Part {
|
||||
if ( ! isset( $this->parts[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->parts[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Part $part ) {
|
||||
return $part->to_array();
|
||||
},
|
||||
$this->parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Parts instance from an array of parts data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed[] $data The parts data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the parts data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Parts {
|
||||
$parts = new Parts();
|
||||
|
||||
foreach ( $data as $part ) {
|
||||
if ( ! is_array( $part ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid part data.' );
|
||||
}
|
||||
|
||||
if ( isset( $part['text'] ) ) {
|
||||
$parts->add_part( Text_Part::from_array( $part ) );
|
||||
} elseif ( isset( $part['functionCall'] ) ) {
|
||||
$parts->add_part( Function_Call_Part::from_array( $part ) );
|
||||
} elseif ( isset( $part['functionResponse'] ) ) {
|
||||
$parts->add_part( Function_Response_Part::from_array( $part ) );
|
||||
} else {
|
||||
throw new InvalidArgumentException( 'Invalid part data.' );
|
||||
}
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
$text_part_schema = Text_Part::get_json_schema();
|
||||
$function_call_part_schema = Function_Call_Part::get_json_schema();
|
||||
$function_response_part_schema = Function_Response_Part::get_json_schema();
|
||||
unset(
|
||||
$text_part_schema['type'],
|
||||
$function_call_part_schema['type'],
|
||||
$function_response_part_schema['type']
|
||||
);
|
||||
|
||||
return array(
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'oneOf' => array(
|
||||
$text_part_schema,
|
||||
$function_call_part_schema,
|
||||
$function_response_part_schema,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Abstract_Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Part;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class for a part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
abstract class Abstract_Part implements Part, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The part data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final public function __construct() {
|
||||
// Empty constructor, only to prevent override.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
*/
|
||||
final public function set_data( array $data ): void {
|
||||
$this->data = $this->format_data( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
abstract protected function format_data( array $data ): array;
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
abstract protected function get_default_data(): array;
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
if ( ! $this->data ) {
|
||||
$this->data = $this->get_default_data();
|
||||
}
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a specific Part instance from an array of part data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return Part The Part instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the parts data is invalid.
|
||||
*/
|
||||
final public static function from_array( array $data ): Part {
|
||||
$part = new static();
|
||||
$part->set_data( $data );
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function call part of content for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Call_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the ID of the function call from the part.
|
||||
*
|
||||
* Every function call must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function call ID, or empty string if none set.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionCall']['id'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionCall']['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function name from the part.
|
||||
*
|
||||
* Every function call must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function name, or empty string if none set.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionCall']['name'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionCall']['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function input arguments from the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The function input arguments.
|
||||
*/
|
||||
public function get_args(): array {
|
||||
return $this->to_array()['functionCall']['args'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionCall'] ) || ! is_array( $data['functionCall'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain an associative array functionCall value.' );
|
||||
}
|
||||
|
||||
$function_call = $data['functionCall'];
|
||||
|
||||
if (
|
||||
( ! isset( $function_call['id'] ) || ! is_string( $function_call['id'] ) ) &&
|
||||
( ! isset( $function_call['name'] ) || ! is_string( $function_call['name'] ) )
|
||||
) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain either a string id value or a string name value.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $function_call['args'] ) || ! is_array( $function_call['args'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function call part data must contain an object / associative array args value.' );
|
||||
}
|
||||
|
||||
$function_call_formatted = array();
|
||||
if ( isset( $function_call['id'] ) ) {
|
||||
$function_call_formatted['id'] = $function_call['id'];
|
||||
}
|
||||
if ( isset( $function_call['name'] ) ) {
|
||||
$function_call_formatted['name'] = $function_call['name'];
|
||||
}
|
||||
$function_call_formatted['args'] = $function_call['args'];
|
||||
|
||||
return array(
|
||||
'functionCall' => $function_call_formatted,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionCall' => array(
|
||||
'name' => '',
|
||||
'args' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionCall' => array(
|
||||
'description' => __( 'Function call as part of the prompt.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'ID of the function call. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function to call. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'args' => array(
|
||||
'description' => __( 'Arguments input for the function to call.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function response part of content for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Response_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the ID of the function response from the part.
|
||||
*
|
||||
* If present, this must match the function call ID.
|
||||
* Every function response must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function response ID, or empty string if none set.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionResponse']['id'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionResponse']['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function name from the part.
|
||||
*
|
||||
* If present, this must match the name of the function called.
|
||||
* Every function response must have at least one of 'id' or 'name' present.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function name, or empty string if none set.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$data = $this->to_array();
|
||||
if ( ! isset( $data['functionResponse']['name'] ) ) {
|
||||
return '';
|
||||
}
|
||||
return $data['functionResponse']['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function output response from the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed The function output response.
|
||||
*/
|
||||
public function get_response() {
|
||||
return $this->to_array()['functionResponse']['response'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionResponse'] ) || ! is_array( $data['functionResponse'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain an associative array functionResponse value.' );
|
||||
}
|
||||
|
||||
$function_response = $data['functionResponse'];
|
||||
|
||||
if (
|
||||
( ! isset( $function_response['id'] ) || ! is_string( $function_response['id'] ) ) &&
|
||||
( ! isset( $function_response['name'] ) || ! is_string( $function_response['name'] ) )
|
||||
) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain either a string id value or a string name value.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $function_response['response'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function response part data must contain a response value.' );
|
||||
}
|
||||
|
||||
$function_response_formatted = array();
|
||||
if ( isset( $function_response['id'] ) ) {
|
||||
$function_response_formatted['id'] = $function_response['id'];
|
||||
}
|
||||
if ( isset( $function_response['name'] ) ) {
|
||||
$function_response_formatted['name'] = $function_response['name'];
|
||||
}
|
||||
$function_response_formatted['response'] = $function_response['response'];
|
||||
|
||||
return array(
|
||||
'functionResponse' => $function_response_formatted,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionResponse' => array(
|
||||
'name' => '',
|
||||
'response' => null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionResponse' => array(
|
||||
'description' => __( 'Function response as part of the prompt.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'ID of the function response. If present, it must match the function call ID. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function called. Either this or a name must be present.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'response' => array(
|
||||
'description' => __( 'Response from the function called.', 'ai-services' ),
|
||||
'type' => array( 'string', 'number', 'boolean', 'array', 'object' ),
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a text part of content for a generative model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Text_Part extends Abstract_Part {
|
||||
|
||||
/**
|
||||
* Gets the text from the part.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string The text.
|
||||
*/
|
||||
public function get_text(): string {
|
||||
return $this->to_array()['text'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $data The part data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the part data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['text'] ) || ! is_string( $data['text'] ) ) {
|
||||
throw new InvalidArgumentException( 'The text part data must contain a string text value.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'text' => $data['text'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the part.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'text' => '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'text' => array(
|
||||
'description' => __( 'Prompt text content.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Service_Metadata
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Value class representing metadata about a generative AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Metadata implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The service name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* The service credentials URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $credentials_url;
|
||||
|
||||
/**
|
||||
* The service type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* List of AI capabilities supported by the service and its models.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $capabilities;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* The arguments for the service metadata.
|
||||
*
|
||||
* @type string $slug The service slug.
|
||||
* @type string $name Optional. The service name. Default will be generated from the slug.
|
||||
* @type string $credentials_url Optional. The service credentials URL. Default empty string.
|
||||
* @type string $type Optional. The service type. Default `Service_Type::CLOUD`.
|
||||
* @type string[] $capabilities Optional. The list of AI capabilities supported by the service and its
|
||||
* models. Default empty array.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given slug is invalid.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$args = $this->parse_args( $args );
|
||||
|
||||
$this->slug = $args['slug'];
|
||||
$this->name = $args['name'];
|
||||
$this->credentials_url = $args['credentials_url'];
|
||||
$this->type = $args['type'];
|
||||
$this->capabilities = $args['capabilities'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service credentials URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service credentials URL.
|
||||
*/
|
||||
public function get_credentials_url(): string {
|
||||
return $this->credentials_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service type.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of AI capabilities supported by the service and its models.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] List of AI capabilities supported by the service and its models.
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->name,
|
||||
'credentials_url' => $this->credentials_url,
|
||||
'type' => $this->type,
|
||||
'capabilities' => $this->capabilities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Service_Metadata instance from an array of service metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service metadata arguments.
|
||||
* @return Service_Metadata The Service_Metadata instance.
|
||||
*/
|
||||
public static function from_array( array $args ): Service_Metadata {
|
||||
return new Service_Metadata( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the service metadata arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service metadata arguments.
|
||||
* @return array<string, mixed> The parsed service metadata arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid argument is provided.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
private function parse_args( array $args ): array {
|
||||
if ( ! isset( $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException( 'The slug is required.' );
|
||||
}
|
||||
|
||||
if ( ! preg_match( '/^[a-z0-9-]+$/', $args['slug'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The service slug must only contain lowercase letters, numbers, and hyphens.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $args['name'] ) ) {
|
||||
$args['name'] = (string) $args['name'];
|
||||
} else {
|
||||
$args['name'] = ucwords( str_replace( array( '-', '_' ), ' ', $args['slug'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $args['credentials_url'] ) ) {
|
||||
$args['credentials_url'] = (string) $args['credentials_url'];
|
||||
|
||||
// Basic sanity check to ensure a protocol is present.
|
||||
if ( ! str_contains( $args['credentials_url'], ':' ) && ! in_array( $args['credentials_url'][0], array( '/', '#', '?' ), true ) ) {
|
||||
$args['credentials_url'] = 'https://' . $args['credentials_url'];
|
||||
}
|
||||
} else {
|
||||
$args['credentials_url'] = '';
|
||||
}
|
||||
|
||||
if ( isset( $args['type'] ) ) {
|
||||
if ( ! Service_Type::is_valid_value( $args['type'] ) ) {
|
||||
throw new InvalidArgumentException( 'The service type is invalid.' );
|
||||
}
|
||||
} else {
|
||||
$args['type'] = Service_Type::CLOUD;
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
if ( ! is_array( $args['capabilities'] ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities must be an array.' );
|
||||
}
|
||||
foreach ( $args['capabilities'] as $capability ) {
|
||||
if ( ! AI_Capability::is_valid_value( $capability ) ) {
|
||||
throw new InvalidArgumentException( 'The capabilities contain an invalid value.' );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$args['capabilities'] = array();
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'slug' => array(
|
||||
'description' => __( 'Unique service slug.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => __( 'User-facing service name.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'credentials_url' => array(
|
||||
'description' => __( 'Service credentials URL, or empty string if not specified.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => __( 'Service type.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => Service_Type::get_values(),
|
||||
'readonly' => true,
|
||||
),
|
||||
'capabilities' => array(
|
||||
'description' => __( 'List of AI capabilities supported by the service and its models.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => AI_Capability::get_values(),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Modality;
|
||||
use ATFPP\AI_Translate\Services\Base\Abstract_Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing text configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.5.0 Renamed from `Generation_Config`.
|
||||
* @since 0.7.0 Now extends `Abstract_Generation_Config`.
|
||||
*/
|
||||
class Text_Generation_Config extends Abstract_Generation_Config {
|
||||
|
||||
/**
|
||||
* Returns the stop sequences.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string[] The stop sequences, or empty array if not set.
|
||||
*/
|
||||
public function get_stop_sequences(): array {
|
||||
return $this->get_arg( 'stopSequences' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response MIME type.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return string The response MIME type, or empty string if not set.
|
||||
*/
|
||||
public function get_response_mime_type(): string {
|
||||
return $this->get_arg( 'responseMimeType' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response schema.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The response schema, or empty array if not set.
|
||||
*/
|
||||
public function get_response_schema(): array {
|
||||
return $this->get_arg( 'responseSchema' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the candidate count.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The candidate count (default 1).
|
||||
*/
|
||||
public function get_candidate_count(): int {
|
||||
return $this->get_arg( 'candidateCount' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum output tokens.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The maximum output tokens, or 0 if not set.
|
||||
*/
|
||||
public function get_max_output_tokens(): int {
|
||||
return $this->get_arg( 'maxOutputTokens' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the temperature.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The temperature (between 0.0 and 1.0), or 0.0 if not set.
|
||||
*/
|
||||
public function get_temperature(): float {
|
||||
return $this->get_arg( 'temperature' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top P.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The top P, or 0.0 if not set.
|
||||
*/
|
||||
public function get_top_p(): float {
|
||||
return $this->get_arg( 'topP' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top K.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The top K, or 0 if not set.
|
||||
*/
|
||||
public function get_top_k(): int {
|
||||
return $this->get_arg( 'topK' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presence penalty.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The presence penalty, or 0.0 if not set.
|
||||
*/
|
||||
public function get_presence_penalty(): float {
|
||||
return $this->get_arg( 'presencePenalty' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frequency penalty.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return float The frequency penalty, or 0.0 if not set.
|
||||
*/
|
||||
public function get_frequency_penalty(): float {
|
||||
return $this->get_arg( 'frequencyPenalty' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether to include the response logprobs.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool Whether to include the response logprobs.
|
||||
*/
|
||||
public function get_response_logprobs(): bool {
|
||||
return $this->get_arg( 'responseLogprobs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top logprobs.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return int The top logprobs, or 0 if not set.
|
||||
*/
|
||||
public function get_logprobs(): int {
|
||||
return $this->get_arg( 'logprobs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output modalities.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @return string[] The output modalities, or empty array if not set.
|
||||
*/
|
||||
public function get_output_modalities(): array {
|
||||
return $this->get_arg( 'outputModalities' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the definition for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The supported arguments definition.
|
||||
*/
|
||||
protected function get_supported_args_definition(): array {
|
||||
$schema = self::get_json_schema();
|
||||
return $schema['properties'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value based on the given type.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported or the value is invalid.
|
||||
*/
|
||||
protected function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
if ( 'temperature' === $arg_name && ( (float) $value < 0.0 || (float) $value > 1.0 ) ) {
|
||||
throw new InvalidArgumentException( 'Temperature must be between 0.0 and 1.0.' );
|
||||
}
|
||||
|
||||
return parent::sanitize_arg( $value, $type, $arg_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'stopSequences' => array(
|
||||
'description' => __( 'Set of character sequences that will stop output generation.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
'responseMimeType' => array(
|
||||
'description' => __( 'MIME type of the generated candidate text.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'text/plain', 'application/json' ),
|
||||
),
|
||||
'responseSchema' => array(
|
||||
'description' => __( 'Output schema of the generated candidate text (only relevant if responseMimeType is application/json).', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
'candidateCount' => array(
|
||||
'description' => __( 'Number of response candidates to generate.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
),
|
||||
'maxOutputTokens' => array(
|
||||
'description' => __( 'The maximum number of tokens to include in a response candidate.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
),
|
||||
'temperature' => array(
|
||||
'description' => sprintf(
|
||||
/* translators: 1: Minimum value, 2: Maximum value */
|
||||
__( 'Floating point value to control the randomness of the output, between %1$s and %2$s.', 'ai-services' ),
|
||||
'0.0',
|
||||
'1.0'
|
||||
),
|
||||
'type' => 'number',
|
||||
'minimum' => 0.0,
|
||||
'maximum' => 1.0,
|
||||
),
|
||||
'topP' => array(
|
||||
'description' => __( 'The maximum cumulative probability of tokens to consider when sampling.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'topK' => array(
|
||||
'description' => __( 'The maximum number of tokens to consider when sampling.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'presencePenalty' => array(
|
||||
'description' => __( 'Presence penalty applied to the next token’s logprobs if the token has already been seen in the response.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'frequencyPenalty' => array(
|
||||
'description' => __( 'Frequency penalty applied to the next token’s logprobs, multiplied by the number of times each token has been seen in the response so far.', 'ai-services' ),
|
||||
'type' => 'number',
|
||||
),
|
||||
'responseLogprobs' => array(
|
||||
'description' => __( 'Whether to return log probabilities of the output tokens in the response or not.', 'ai-services' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'logprobs' => array(
|
||||
'description' => __( 'The number of top logprobs to return at each decoding step.', 'ai-services' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'outputModalities' => array(
|
||||
'description' => __( 'The modalities that the response can contain.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
Modality::TEXT,
|
||||
Modality::IMAGE,
|
||||
Modality::AUDIO,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tool_Config
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use ATFPP\AI_Translate\Services\Util\Strings;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class representing tool configuration for a generative AI model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Tool_Config implements Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $sanitized_args;
|
||||
|
||||
/**
|
||||
* Type definitions for the supported arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $supported_args = array(
|
||||
'functionCallMode' => 'string',
|
||||
'allowedFunctionNames' => 'array',
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $args The configuration arguments.
|
||||
*/
|
||||
public function __construct( array $args ) {
|
||||
$this->sanitized_args = $this->sanitize_args( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the function call mode.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The function call mode, or empty string if not set.
|
||||
*/
|
||||
public function get_function_call_mode(): string {
|
||||
return $this->sanitized_args['functionCallMode'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allowed function names.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string[] The allowed function names, or empty array if not set.
|
||||
*/
|
||||
public function get_allowed_function_names(): array {
|
||||
return $this->sanitized_args['allowedFunctionNames'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return $this->sanitized_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tool_Config instance from an array of content data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return Tool_Config Tool_Config instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): Tool_Config {
|
||||
return new Tool_Config( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionCallMode' => array(
|
||||
'description' => __( 'Mode for how to consider function calling.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
'enum' => array( 'auto', 'any' ),
|
||||
),
|
||||
'allowedFunctionNames' => array(
|
||||
'description' => __( 'List of function names allowed to call.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given arguments.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $args The arguments to sanitize.
|
||||
* @return array<string, mixed> Sanitized arguments.
|
||||
*/
|
||||
private function sanitize_args( array $args ): array {
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $args as $key => $value ) {
|
||||
if ( isset( $this->supported_args[ $key ] ) ) {
|
||||
$sanitized[ $key ] = $this->sanitize_arg( $value, $this->supported_args[ $key ], $key );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( str_contains( $key, '_' ) ) {
|
||||
$camelcase_key = Strings::snake_case_to_camel_case( $key );
|
||||
if ( isset( $this->supported_args[ $camelcase_key ] ) ) {
|
||||
$sanitized[ $camelcase_key ] = $this->sanitize_arg( $value, $this->supported_args[ $camelcase_key ], $camelcase_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizies the given value based on the given type.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported.
|
||||
*/
|
||||
private function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
if ( 'functionCallMode' === $arg_name && ! in_array( $value, array( 'auto', 'any' ), true ) ) {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
switch ( $type ) {
|
||||
case 'array':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( ! $value ) {
|
||||
return array();
|
||||
}
|
||||
return array( $value );
|
||||
}
|
||||
return array_values( $value );
|
||||
case 'string':
|
||||
return (string) $value;
|
||||
case 'object':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( is_object( $value ) ) {
|
||||
if ( $value instanceof Arrayable ) {
|
||||
return $value->to_array();
|
||||
}
|
||||
return (array) $value;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
return $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
default:
|
||||
throw new InvalidArgumentException( 'Unsupported type.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types;
|
||||
|
||||
use ArrayIterator;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Web_Search_Tool;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Collection;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class representing a collection of content tools for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Tools implements Collection, Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The tools.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var Tool[]
|
||||
*/
|
||||
private $tools = array();
|
||||
|
||||
/**
|
||||
* Adds a function declarations tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed>[] $function_declarations The function declarations.
|
||||
*/
|
||||
public function add_function_declarations_tool( array $function_declarations ): void {
|
||||
$this->add_tool(
|
||||
Function_Declarations_Tool::from_array(
|
||||
array( 'functionDeclarations' => $function_declarations )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a web search tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $allowed_domains Optional. The allowed domains. Default empty array.
|
||||
* @param string[] $disallowed_domains Optional. The disallowed domains. Default empty array.
|
||||
*/
|
||||
public function add_web_search_tool( array $allowed_domains = array(), array $disallowed_domains = array() ): void {
|
||||
$this->add_tool(
|
||||
Web_Search_Tool::from_array(
|
||||
array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => $allowed_domains,
|
||||
'disallowedDomains' => $disallowed_domains,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Tool $tool The tool.
|
||||
*/
|
||||
public function add_tool( Tool $tool ): void {
|
||||
$this->tools[] = $tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the tools collection.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return ArrayIterator<int, Tool> Collection iterator.
|
||||
*/
|
||||
public function getIterator(): Traversable {
|
||||
return new ArrayIterator( $this->tools );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the tools collection.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return int Collection size.
|
||||
*/
|
||||
public function count(): int {
|
||||
return count( $this->tools );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tool at the given index.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param int $index The index.
|
||||
* @return Tool The tool.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the index is out of bounds.
|
||||
*/
|
||||
public function get( int $index ): Tool {
|
||||
if ( ! isset( $this->tools[ $index ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Index out of bounds.'
|
||||
);
|
||||
}
|
||||
return $this->tools[ $index ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array_map(
|
||||
static function ( Tool $tool ) {
|
||||
return $tool->to_array();
|
||||
},
|
||||
$this->tools
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tools instance from an array of tools data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param mixed[] $data The tools data.
|
||||
* @return Tools The Tools instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tools data is invalid.
|
||||
*/
|
||||
public static function from_array( array $data ): Tools {
|
||||
$tools = new Tools();
|
||||
|
||||
foreach ( $data as $tool ) {
|
||||
if ( ! is_array( $tool ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid tool data.' );
|
||||
}
|
||||
|
||||
if ( isset( $tool['functionDeclarations'] ) ) {
|
||||
$tools->add_tool( Function_Declarations_Tool::from_array( $tool ) );
|
||||
} elseif ( isset( $tool['webSearch'] ) ) {
|
||||
$tools->add_tool( Web_Search_Tool::from_array( $tool ) );
|
||||
} else {
|
||||
throw new InvalidArgumentException( 'Invalid tool data.' );
|
||||
}
|
||||
}
|
||||
|
||||
return $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
$function_declarations_tool_schema = Function_Declarations_Tool::get_json_schema();
|
||||
unset( $function_declarations_tool_schema['type'] );
|
||||
|
||||
$web_search_tool_schema = Web_Search_Tool::get_json_schema();
|
||||
unset( $web_search_tool_schema['type'] );
|
||||
|
||||
return array(
|
||||
'type' => 'array',
|
||||
'minItems' => 1,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'oneOf' => array(
|
||||
$function_declarations_tool_schema,
|
||||
$web_search_tool_schema,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Abstract_Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class for a tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
abstract class Abstract_Tool implements Tool, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* The tool data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final public function __construct() {
|
||||
// Empty constructor, only to prevent override.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
*/
|
||||
final public function set_data( array $data ): void {
|
||||
$this->data = $this->format_data( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
abstract protected function format_data( array $data ): array;
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
abstract protected function get_default_data(): array;
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
if ( ! $this->data ) {
|
||||
$this->data = $this->get_default_data();
|
||||
}
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a specific Tool instance from an array of tool data.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return Tool The Tool instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tools data is invalid.
|
||||
*/
|
||||
final public static function from_array( array $data ): Tool {
|
||||
$tool = new static();
|
||||
$tool->set_data( $data );
|
||||
return $tool;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a function declarations tool for a generative model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
final class Function_Declarations_Tool extends Abstract_Tool {
|
||||
|
||||
/**
|
||||
* Gets the function declarations from the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed>[] The function declarations.
|
||||
*/
|
||||
public function get_function_declarations(): array {
|
||||
return $this->to_array()['functionDeclarations'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( ! isset( $data['functionDeclarations'] ) || ! is_array( $data['functionDeclarations'] ) ) {
|
||||
throw new InvalidArgumentException( 'The function declarations tool data must contain an array functionDeclarations value.' );
|
||||
}
|
||||
|
||||
foreach ( $data['functionDeclarations'] as &$function_declaration ) {
|
||||
if ( ! isset( $function_declaration['name'] ) || ! is_string( $function_declaration['name'] ) ) {
|
||||
throw new InvalidArgumentException( 'Each function declaration data must contain a string name value.' );
|
||||
}
|
||||
if ( isset( $function_declaration['description'] ) && ! is_string( $function_declaration['description'] ) ) {
|
||||
throw new InvalidArgumentException( 'The description value of a function declaration must be a string.' );
|
||||
}
|
||||
if ( isset( $function_declaration['parameters'] ) && ! is_array( $function_declaration['parameters'] ) ) {
|
||||
throw new InvalidArgumentException( 'The parameters value of a function declaration must be an object / associative array.' );
|
||||
}
|
||||
|
||||
$function_declaration['parameters'] = $this->sanitize_parameters( $function_declaration['parameters'] );
|
||||
}
|
||||
|
||||
return array(
|
||||
'functionDeclarations' => $data['functionDeclarations'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'functionDeclarations' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the parameters schema, ensuring every object property is required and additional properties are disallowed.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $schema The schema to sanitize.
|
||||
* @return array<string, mixed> Sanitized schema.
|
||||
*/
|
||||
protected function sanitize_parameters( array $schema ): array {
|
||||
// Every schema must have a type, but that will be checked elsewhere so we can ignore it here.
|
||||
if ( ! isset( $schema['type'] ) ) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$type = (array) $schema['type'];
|
||||
if ( in_array( 'object', $type, true ) ) {
|
||||
if ( isset( $schema['properties'] ) ) {
|
||||
$schema['required'] = array_keys( $schema['properties'] );
|
||||
foreach ( $schema['properties'] as $key => $child_schema ) {
|
||||
$schema['properties'][ $key ] = $this->sanitize_parameters( $child_schema );
|
||||
}
|
||||
}
|
||||
$schema['additionalProperties'] = false;
|
||||
}
|
||||
|
||||
if ( in_array( 'array', $type, true ) && isset( $schema['items'] ) ) {
|
||||
$schema['items'] = $this->sanitize_parameters( $schema['items'] );
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'functionDeclarations' => array(
|
||||
'description' => __( 'Function declarations for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'name' => array(
|
||||
'description' => __( 'Name of the function.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'description' => array(
|
||||
'description' => __( 'Description of the function.', 'ai-services' ),
|
||||
'type' => 'string',
|
||||
),
|
||||
'parameters' => array(
|
||||
'description' => __( 'Supported parameters of the function, as an object in JSON schema.', 'ai-services' ),
|
||||
'type' => 'object',
|
||||
'additionalProperties' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\API\Types\Tools\Web_Search_Tool
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for a web search tool for a generative model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Web_Search_Tool extends Abstract_Tool {
|
||||
|
||||
/**
|
||||
* Gets the allowed domains for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The allowed domains.
|
||||
*/
|
||||
public function get_allowed_domains(): array {
|
||||
return $this->to_array()['webSearch']['allowedDomains'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the disallowed domains for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string[] The disallowed domains.
|
||||
*/
|
||||
public function get_disallowed_domains(): array {
|
||||
return $this->to_array()['webSearch']['disallowedDomains'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the data for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $data The tool data.
|
||||
* @return array<string, mixed> Formatted data.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the tool data is invalid.
|
||||
*/
|
||||
protected function format_data( array $data ): array {
|
||||
if ( isset( $data['webSearch']['allowedDomains'] ) && ! is_array( $data['webSearch']['allowedDomains'] ) ) {
|
||||
throw new InvalidArgumentException( 'The allowedDomains value for the web search tool data must be an array of strings.' );
|
||||
}
|
||||
if ( isset( $data['webSearch']['disallowedDomains'] ) && ! is_array( $data['webSearch']['disallowedDomains'] ) ) {
|
||||
throw new InvalidArgumentException( 'The disallowedDomains value for the web search tool data must be an array of strings.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => isset( $data['webSearch']['allowedDomains'] ) ? array_values( array_filter( $data['webSearch']['allowedDomains'], 'is_string' ) ) : array(),
|
||||
'disallowedDomains' => isset( $data['webSearch']['disallowedDomains'] ) ? array_values( array_filter( $data['webSearch']['disallowedDomains'], 'is_string' ) ) : array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default data for the tool.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> Default data.
|
||||
*/
|
||||
protected function get_default_data(): array {
|
||||
return array(
|
||||
'webSearch' => array(
|
||||
'allowedDomains' => array(),
|
||||
'disallowedDomains' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array {
|
||||
return array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'webSearch' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'allowedDomains' => array(
|
||||
'description' => __( 'Web search allowed domains for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'disallowedDomains' => array(
|
||||
'description' => __( 'Web search disallowed domains for the tool.', 'ai-services' ),
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'additionalProperties' => false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Authentication\API_Key_Authentication
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Authentication;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
|
||||
/**
|
||||
* Class that represents an API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class API_Key_Authentication implements Authentication {
|
||||
|
||||
/**
|
||||
* The API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $api_key;
|
||||
|
||||
/**
|
||||
* The HTTP header to use for the API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $header_name = 'Authorization';
|
||||
|
||||
/**
|
||||
* The authentication scheme to use for the API key.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $authencation_scheme = 'Bearer';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $api_key The API key.
|
||||
*/
|
||||
public function __construct( string $api_key ) {
|
||||
$this->api_key = $api_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the given request with the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance. Updated in place.
|
||||
*/
|
||||
public function authenticate( Request $request ): void {
|
||||
if ( 'authorization' === strtolower( $this->header_name ) ) {
|
||||
$request->add_header( $this->header_name, $this->authencation_scheme . ' ' . $this->api_key );
|
||||
} else {
|
||||
$request->add_header( $this->header_name, $this->api_key );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the header name to use to add the credentials to a request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $header_name The header name.
|
||||
*/
|
||||
public function set_header_name( string $header_name ): void {
|
||||
$this->header_name = $header_name;
|
||||
}
|
||||
|
||||
public function set_authencation_scheme( string $authencation_scheme ): void {
|
||||
$this->authencation_scheme = $authencation_scheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the option definitions needed to store the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug The service slug.
|
||||
* @return array<string, array<string, mixed>> The option definitions.
|
||||
*/
|
||||
public static function get_option_definitions( string $service_slug ): array {
|
||||
$option_slug = sprintf( 'Atfpp_Ai_Translate_%s_api_key', $service_slug );
|
||||
|
||||
return array(
|
||||
$option_slug => array(
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'show_in_rest' => true,
|
||||
'autoload' => true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_AI_Model
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Base class for an AI model.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
abstract class Abstract_AI_Model implements Generative_AI_Model {
|
||||
|
||||
/**
|
||||
* The model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Model_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* The request options.
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $request_options;
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
final public function get_model_slug(): string {
|
||||
return $this->get_model_metadata()->get_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Model_Metadata The model metadata.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the model metadata is not set.
|
||||
*/
|
||||
final public function get_model_metadata(): Model_Metadata {
|
||||
if ( ! $this->metadata instanceof Model_Metadata ) {
|
||||
throw new RuntimeException( 'Model metadata must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request options.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @return array<string, mixed> The request options.
|
||||
*/
|
||||
final protected function get_request_options(): array {
|
||||
return $this->request_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
*/
|
||||
final protected function set_model_metadata( Model_Metadata $metadata ): void {
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
*/
|
||||
final protected function set_request_options( array $request_options ): void {
|
||||
$this->request_options = $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_AI_Service
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Base class for an AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
abstract class Abstract_AI_Service implements Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
final public function get_service_slug(): string {
|
||||
return $this->get_service_metadata()->get_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the service metadata is not set.
|
||||
*/
|
||||
final public function get_service_metadata(): Service_Metadata {
|
||||
if ( ! $this->metadata instanceof Service_Metadata ) {
|
||||
throw new RuntimeException( 'Service metadata must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* In case of a cloud based service, this is typically used to check whether the current service credentials are
|
||||
* valid. For other service types, this may check other requirements, or simply return true.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the connection check cannot be performed.
|
||||
*/
|
||||
public function is_connected(): bool {
|
||||
if ( Service_Type::CLOUD !== $this->get_service_metadata()->get_type() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->list_models();
|
||||
return true;
|
||||
} catch ( Generative_AI_Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
* @throws Generative_AI_Exception Thrown if getting the model fails.
|
||||
*/
|
||||
final public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model {
|
||||
$models_metadata = $this->cached_list_models( $request_options );
|
||||
|
||||
if ( isset( $model_params['model'] ) ) {
|
||||
$model = $model_params['model'];
|
||||
unset( $model_params['model'] );
|
||||
} else {
|
||||
if ( isset( $model_params['capabilities'] ) ) {
|
||||
$model_slugs = AI_Capabilities::get_model_slugs_for_capabilities(
|
||||
$models_metadata,
|
||||
$model_params['capabilities']
|
||||
);
|
||||
} else {
|
||||
$model_slugs = array_keys( $models_metadata );
|
||||
}
|
||||
$model = $this->sort_models_by_preference( $model_slugs )[0];
|
||||
}
|
||||
|
||||
if ( ! isset( $models_metadata[ $model ] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid model slug "%1$s" for the service "%2$s".',
|
||||
htmlspecialchars( $model ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $this->get_service_slug() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$model_metadata = $models_metadata[ $model ];
|
||||
|
||||
return $this->create_model_instance( $model_metadata, $model_params, $request_options );
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Creates a new model instance for the provided model metadata and parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Model_Metadata $model_metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
|
||||
* a list of available parameters.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generative_AI_Model The new model instance.
|
||||
*/
|
||||
abstract protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model;
|
||||
|
||||
/**
|
||||
* Sorts model slugs by preference.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_slugs The model slugs to sort.
|
||||
* @return string[] The model slugs, sorted by preference.
|
||||
*/
|
||||
protected function sort_models_by_preference( array $model_slugs ): array {
|
||||
// By default, no sorting is applied.
|
||||
return $model_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
*/
|
||||
final protected function set_service_metadata( Service_Metadata $metadata ): void {
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata, wrapped in a transient cache.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
private function cached_list_models( array $request_options = array() ): array {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->list_models( $request_options );
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this, 'list_models' ),
|
||||
array( $request_options )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Abstract_Generation_Config
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Util\Strings;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class representing configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.2.0 Originally implemented as non-abstract class `Types\Generation_Config`.
|
||||
* @since 0.7.0
|
||||
*/
|
||||
abstract class Abstract_Generation_Config implements Generation_Config {
|
||||
|
||||
/**
|
||||
* The sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $sanitized_args;
|
||||
|
||||
/**
|
||||
* Any additional arguments, unsanitized.
|
||||
*
|
||||
* These are not used directly by the class, but are passed through to the API.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $additional_args;
|
||||
|
||||
/**
|
||||
* Default values for the sanitized configuration arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $defaults;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $args The configuration arguments.
|
||||
*/
|
||||
final public function __construct( array $args ) {
|
||||
$args_definition = $this->get_supported_args_definition();
|
||||
|
||||
$args = $this->sanitize_args( $args, $args_definition );
|
||||
|
||||
$this->sanitized_args = $args['sanitized'];
|
||||
$this->additional_args = $args['additional'];
|
||||
$this->defaults = $this->get_defaults( $args_definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given supported argument.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $name The argument name.
|
||||
* @return mixed The argument value, or its default value if not set.
|
||||
*/
|
||||
final public function get_arg( string $name ) {
|
||||
if ( ! isset( $this->sanitized_args[ $name ] ) ) {
|
||||
return $this->defaults[ $name ] ?? null;
|
||||
}
|
||||
|
||||
return $this->sanitized_args[ $name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all formally supported arguments.
|
||||
*
|
||||
* Only includes arguments that have an explicit value set, i.e. not defaults.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The arguments.
|
||||
*/
|
||||
final public function get_args(): array {
|
||||
return $this->sanitized_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The additional arguments.
|
||||
*/
|
||||
final public function get_additional_args(): array {
|
||||
return $this->additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return mixed[] Array representation.
|
||||
*/
|
||||
final public function to_array(): array {
|
||||
return $this->sanitized_args + $this->additional_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Generation_Config instance from an array of content data.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $data The content data.
|
||||
* @return static Generation_Config instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the data is missing required fields.
|
||||
*/
|
||||
public static function from_array( array $data ): static {
|
||||
return new static( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the definition for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The supported arguments definition.
|
||||
*/
|
||||
abstract protected function get_supported_args_definition(): array;
|
||||
|
||||
/**
|
||||
* Gets the default values for the supported arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $args_definition The arguments definition.
|
||||
* @return array<string, mixed> The default values.
|
||||
*/
|
||||
private function get_defaults( array $args_definition ): array {
|
||||
$defaults = array();
|
||||
|
||||
foreach ( $args_definition as $key => $definition ) {
|
||||
if ( isset( $definition['default'] ) ) {
|
||||
$defaults[ $key ] = $definition['default'];
|
||||
} elseif ( isset( $definition['type'] ) ) {
|
||||
// Set default to type-safe value that is considered false-y.
|
||||
switch ( $definition['type'] ) {
|
||||
case 'array':
|
||||
$defaults[ $key ] = array();
|
||||
break;
|
||||
case 'string':
|
||||
$defaults[ $key ] = '';
|
||||
break;
|
||||
case 'object':
|
||||
$defaults[ $key ] = array();
|
||||
break;
|
||||
case 'integer':
|
||||
$defaults[ $key ] = 0;
|
||||
break;
|
||||
case 'number':
|
||||
case 'float':
|
||||
$defaults[ $key ] = 0.0;
|
||||
break;
|
||||
case 'boolean':
|
||||
$defaults[ $key ] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given arguments.
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @since 0.7.0 The $args_definition parameter was added.
|
||||
*
|
||||
* @param array<string, mixed> $args The arguments to sanitize.
|
||||
* @param array<string, mixed> $args_definition The arguments definition.
|
||||
* @return array<string, array<string, mixed>> Associative array with keys 'sanitized' and 'additional', each
|
||||
* containing an array of arguments. The 'sanitized' array contains
|
||||
* the supported sanitized arguments, while the 'additional' array
|
||||
* contains any additional arguments that are not supported, but can
|
||||
* be passed through to the API.
|
||||
*/
|
||||
private function sanitize_args( array $args, array $args_definition ): array {
|
||||
$sanitized = array();
|
||||
$additional = array();
|
||||
|
||||
foreach ( $args as $key => $value ) {
|
||||
if ( isset( $args_definition[ $key ] ) ) {
|
||||
$sanitized[ $key ] = $this->sanitize_arg( $value, $args_definition[ $key ]['type'] ?? 'string', $key );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( str_contains( $key, '_' ) ) {
|
||||
$camelcase_key = Strings::snake_case_to_camel_case( $key );
|
||||
if ( isset( $args_definition[ $camelcase_key ] ) ) {
|
||||
$sanitized[ $camelcase_key ] = $this->sanitize_arg( $value, $args_definition[ $camelcase_key ]['type'] ?? 'string', $camelcase_key );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$additional[ $key ] = $value;
|
||||
}
|
||||
|
||||
return array(
|
||||
'sanitized' => $sanitized,
|
||||
'additional' => $additional,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value based on the given type.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param mixed $value The value to sanitize.
|
||||
* @param string $type The type to sanitize the value to. Must be one of 'array', 'string', 'object',
|
||||
* 'integer', 'float', or 'boolean'.
|
||||
* @param string $arg_name The name of the argument being sanitized.
|
||||
* @return mixed The sanitized value.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the type is not supported or the value is invalid.
|
||||
*/
|
||||
protected function sanitize_arg( $value, string $type, string $arg_name ) {
|
||||
switch ( $type ) {
|
||||
case 'array':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( ! $value ) {
|
||||
return array();
|
||||
}
|
||||
return array( $value );
|
||||
}
|
||||
return array_values( $value );
|
||||
case 'string':
|
||||
return (string) $value;
|
||||
case 'object':
|
||||
if ( ! is_array( $value ) ) {
|
||||
if ( is_object( $value ) ) {
|
||||
if ( $value instanceof Arrayable ) {
|
||||
return $value->to_array();
|
||||
}
|
||||
return (array) $value;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
return $value;
|
||||
case 'integer':
|
||||
return (int) $value;
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'boolean':
|
||||
return (bool) $value;
|
||||
default:
|
||||
throw new InvalidArgumentException( 'Unsupported type.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Traits\Generative_AI_API_Client_Trait;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Get_Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\JSON_Post_Request;
|
||||
|
||||
/**
|
||||
* Generic implementation of an AI API client, configured via constructor parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
class Generic_AI_API_Client implements Generative_AI_API_Client {
|
||||
use Generative_AI_API_Client_Trait;
|
||||
|
||||
/**
|
||||
* The base URL for the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $default_base_url;
|
||||
|
||||
/**
|
||||
* The API version.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $default_api_version;
|
||||
|
||||
/**
|
||||
* The (human-readable) API name.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $api_name;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The authentication instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Authentication|null
|
||||
*/
|
||||
private $authentication;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $default_base_url The default base URL for the API.
|
||||
* @param string $default_api_version The default API version.
|
||||
* @param string $api_name The (human-readable) API name.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The authentication instance. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $default_base_url,
|
||||
string $default_api_version,
|
||||
string $api_name,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
$this->default_base_url = $default_base_url;
|
||||
$this->default_api_version = $default_api_version;
|
||||
$this->api_name = $api_name;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->authentication = $authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GET request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_get_request( string $path, array $params, array $request_options = array() ): Request {
|
||||
$request_url = $this->get_request_url( $path, $request_options );
|
||||
$request_options = $this->filter_request_options(
|
||||
$this->add_default_options( $request_options ),
|
||||
$request_url
|
||||
);
|
||||
|
||||
$request = new Get_Request(
|
||||
$request_url,
|
||||
$params,
|
||||
$request_options
|
||||
);
|
||||
$this->authenticate_request( $request );
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a POST request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_post_request( string $path, array $params, array $request_options = array() ): Request {
|
||||
$request_url = $this->get_request_url( $path, $request_options );
|
||||
$request_options = $this->filter_request_options(
|
||||
$this->add_default_options( $request_options ),
|
||||
$request_url
|
||||
);
|
||||
|
||||
$request = new JSON_Post_Request(
|
||||
$request_url,
|
||||
$params,
|
||||
$request_options
|
||||
);
|
||||
$this->authenticate_request( $request );
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request URL for the specified model and task.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return string The request URL.
|
||||
*/
|
||||
protected function get_request_url( string $path, array $request_options = array() ): string {
|
||||
$base_url = $request_options['base_url'] ?? $this->default_base_url;
|
||||
$api_version = $request_options['api_version'] ?? $this->default_api_version;
|
||||
$path = ltrim( $path, '/' );
|
||||
|
||||
if ( isset( $request_options['stream'] ) && $request_options['stream'] && ! str_ends_with( $path, '?alt=sse' ) ) {
|
||||
$path .= '?alt=sse';
|
||||
}
|
||||
|
||||
if ( '' === $api_version ) {
|
||||
return "{$base_url}/{$path}";
|
||||
}
|
||||
|
||||
return "{$base_url}/{$api_version}/{$path}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional default request options to the given request options.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return array<string, mixed> The updated request options.
|
||||
*/
|
||||
protected function add_default_options( array $request_options ): array {
|
||||
if ( ! isset( $request_options['timeout'] ) ) {
|
||||
$request_options['timeout'] = 120;
|
||||
}
|
||||
return $request_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the request, if an authentication instance is set.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Request $request The request to authenticate.
|
||||
*/
|
||||
final protected function authenticate_request( Request $request ): void {
|
||||
if ( $this->authentication ) {
|
||||
$this->authentication->authenticate( $request );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human readable API name (without the "API" suffix).
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The API name.
|
||||
*/
|
||||
final protected function get_api_name(): string {
|
||||
return $this->api_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request handler instance to use for requests.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Request_Handler The request handler instance.
|
||||
*/
|
||||
final protected function get_request_handler(): Request_Handler {
|
||||
return $this->request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the request options, with awareness of the request URL.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @param string $request_url The request URL.
|
||||
* @return array<string, mixed> The filtered request options.
|
||||
*/
|
||||
private function filter_request_options( array $request_options, string $request_url ): array {
|
||||
if ( isset( $request_options['timeout'] ) ) {
|
||||
$timeout = $request_options['timeout'];
|
||||
|
||||
/**
|
||||
* Filters the request timeout to use for an API request to an AI service.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param int $timeout The request timeout in seconds.
|
||||
* @param string $request_url The request URL.
|
||||
*/
|
||||
$request_options['timeout'] = (int) apply_filters( 'ai_services_request_timeout', $timeout, $request_url );
|
||||
|
||||
// If the filtered timeout is invalid, use the original value.
|
||||
if ( $request_options['timeout'] <= 0 ) {
|
||||
$request_options['timeout'] = $timeout;
|
||||
}
|
||||
}
|
||||
|
||||
return $request_options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Base\OpenAI_Compatible_AI_Text_Generation_Model
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Base;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidate;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_System_Instruction_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
|
||||
use ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait;
|
||||
use ATFPP\AI_Translate\Services\Util\Transformer;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Generic implementation of an OpenAI API compatible text generation AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
class OpenAI_Compatible_AI_Text_Generation_Model extends Abstract_AI_Model implements With_API_Client, With_Text_Generation {
|
||||
use With_API_Client_Trait;
|
||||
use With_Text_Generation_Trait;
|
||||
use Model_Param_Text_Generation_Config_Trait;
|
||||
use Model_Param_System_Instruction_Trait;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The AI API client instance.
|
||||
* @param Model_Metadata $metadata The model metadata.
|
||||
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
|
||||
* {@see OpenAI_AI_Service::get_model()} for the list of available
|
||||
* parameters. Default empty array.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
|
||||
$this->set_api_client( $api_client );
|
||||
$this->set_model_metadata( $metadata );
|
||||
|
||||
$this->set_text_generation_config_from_model_params( $model_params );
|
||||
$this->set_system_instruction_from_model_params( $model_params );
|
||||
|
||||
$this->set_request_options( $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final protected function send_generate_text_request( array $contents, array $request_options ): Candidates {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$params['model'] = $this->get_model_slug();
|
||||
|
||||
$request = $api->create_post_request(
|
||||
'chat/completions',
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_data(
|
||||
$response,
|
||||
function ( $response_data ) {
|
||||
return $this->get_response_candidates( $response_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator {
|
||||
$api = $this->get_api_client();
|
||||
$params = $this->prepare_generate_text_params( $contents );
|
||||
|
||||
$params['model'] = $this->get_model_slug();
|
||||
$params['stream'] = true;
|
||||
|
||||
$request = $api->create_post_request(
|
||||
'chat/completions',
|
||||
$params,
|
||||
array_merge(
|
||||
$this->get_request_options(),
|
||||
$request_options,
|
||||
array( 'stream' => true )
|
||||
)
|
||||
);
|
||||
$response = $api->make_request( $request );
|
||||
|
||||
return $api->process_response_stream(
|
||||
$response,
|
||||
function ( $response_data, $prev_chunk_candidates ) {
|
||||
return $this->get_response_candidates( $response_data, $prev_chunk_candidates );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*/
|
||||
protected function prepare_generate_text_params( array $contents ): array {
|
||||
if ( $this->get_system_instruction() ) {
|
||||
$contents = array_merge( array( $this->get_system_instruction() ), $contents );
|
||||
}
|
||||
|
||||
$transformers = $this->get_content_transformers();
|
||||
|
||||
$params = array(
|
||||
'messages' => array_map(
|
||||
static function ( Content $content ) use ( $transformers ) {
|
||||
return Transformer::transform_content( $content, $transformers );
|
||||
},
|
||||
$contents
|
||||
),
|
||||
);
|
||||
|
||||
$generation_config = $this->get_text_generation_config();
|
||||
if ( $generation_config ) {
|
||||
$params = Transformer::transform_generation_config_params(
|
||||
array_merge( $generation_config->get_additional_args(), $params ),
|
||||
$generation_config,
|
||||
$this->get_generation_config_transformers()
|
||||
);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the candidates with content from the response.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $response_data The response data.
|
||||
* @param ?Candidates $prev_chunk_candidates The candidates from the previous chunk in case of a streaming
|
||||
* response, or null.
|
||||
* @return Candidates The candidates with content parts.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response does not have any candidates with content.
|
||||
*/
|
||||
private function get_response_candidates( array $response_data, ?Candidates $prev_chunk_candidates = null ): Candidates {
|
||||
if ( null === $prev_chunk_candidates ) {
|
||||
// Regular (non-streaming) response, or first chunk of a streaming response.
|
||||
if ( ! isset( $response_data['choices'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'choices' );
|
||||
}
|
||||
|
||||
$other_data = $response_data;
|
||||
unset( $other_data['choices'] );
|
||||
|
||||
$candidates = new Candidates();
|
||||
foreach ( $response_data['choices'] as $index => $candidate_data ) {
|
||||
if ( isset( $candidate_data['delta'] ) && ! isset( $candidate_data['message'] ) ) {
|
||||
$candidate_data['message'] = $candidate_data['delta'];
|
||||
unset( $candidate_data['delta'] );
|
||||
}
|
||||
|
||||
$other_candidate_data = $candidate_data;
|
||||
unset( $other_candidate_data['message'] );
|
||||
|
||||
$candidates->add_candidate(
|
||||
new Candidate(
|
||||
$this->prepare_response_candidate_content( $candidate_data, $index ),
|
||||
array_merge( $other_candidate_data, $other_data )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
// Subsequent chunk of a streaming response.
|
||||
$candidates_data = $this->merge_candidates_chunk(
|
||||
$prev_chunk_candidates->to_array(),
|
||||
$response_data
|
||||
);
|
||||
|
||||
return Candidates::from_array( $candidates_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges a streaming response chunk with the previous candidates data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidates_data The candidates data from the previous chunk.
|
||||
* @param array<string, mixed> $chunk_data The response chunk data.
|
||||
* @return array<string, mixed> The merged candidates data.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function merge_candidates_chunk( array $candidates_data, array $chunk_data ): array {
|
||||
if ( ! isset( $chunk_data['choices'] ) ) {
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( 'choices' );
|
||||
}
|
||||
|
||||
$other_data = $chunk_data;
|
||||
unset( $other_data['choices'] );
|
||||
|
||||
foreach ( $chunk_data['choices'] as $index => $candidate_data ) {
|
||||
if ( isset( $candidate_data['delta']['reasoning_content'] ) ) {
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $candidate_data['delta']['reasoning_content'];
|
||||
} elseif ( isset( $candidate_data['delta']['content'] ) ) {
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $candidate_data['delta']['content'];
|
||||
} else {
|
||||
// If there was a previous content block, ensure it is ends in a double newline.
|
||||
if (
|
||||
isset( $candidate_data['finish_reason'] ) &&
|
||||
'stop' === $candidate_data['finish_reason'] &&
|
||||
'' !== $candidates_data[ $index ]['content']['parts'][0]['text'] &&
|
||||
! str_ends_with( $candidates_data[ $index ]['content']['parts'][0]['text'], "\n\n" )
|
||||
) {
|
||||
$text_suffix = str_ends_with( $candidates_data[ $index ]['content']['parts'][0]['text'], "\n" ) ? "\n" : "\n\n";
|
||||
} else {
|
||||
$text_suffix = '';
|
||||
}
|
||||
$candidates_data[ $index ]['content']['parts'][0]['text'] = $text_suffix;
|
||||
}
|
||||
unset( $candidate_data['delta'] );
|
||||
|
||||
$candidates_data[ $index ] = array_merge( $candidates_data[ $index ], $candidate_data, $other_data );
|
||||
}
|
||||
|
||||
return $candidates_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Content instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @param int $index The index of the candidate in the response.
|
||||
* @return Content The Content instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
private function prepare_response_candidate_content( array $candidate_data, int $index ): Content {
|
||||
if ( ! isset( $candidate_data['message'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->get_api_client()->create_missing_response_key_exception( "choices.{$index}.message" );
|
||||
}
|
||||
|
||||
$role = isset( $candidate_data['message']['role'] ) && 'user' === $candidate_data['message']['role']
|
||||
? Content_Role::USER
|
||||
: Content_Role::MODEL;
|
||||
|
||||
$parts = $this->prepare_response_candidate_content_parts( $candidate_data );
|
||||
if ( count( $parts ) === 0 ) {
|
||||
throw $this->get_api_client()->create_response_exception(
|
||||
'Could not resolve content parts: The response includes unexpected content.'
|
||||
);
|
||||
}
|
||||
|
||||
return new Content(
|
||||
$role,
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Parts instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
|
||||
$parts = array();
|
||||
if ( isset( $candidate_data['message']['reasoning_content'] ) && is_string( $candidate_data['message']['reasoning_content'] ) ) {
|
||||
$parts[] = array( 'text' => $candidate_data['message']['reasoning_content'] );
|
||||
}
|
||||
if ( isset( $candidate_data['message']['content'] ) && is_string( $candidate_data['message']['content'] ) ) {
|
||||
$parts[] = array( 'text' => $candidate_data['message']['content'] );
|
||||
}
|
||||
return Parts::from_array( $parts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
protected function get_content_transformers(): array {
|
||||
$api_client = $this->get_api_client();
|
||||
|
||||
return array(
|
||||
'role' => static function ( Content $content ) {
|
||||
if ( $content->get_role() === Content_Role::MODEL ) {
|
||||
return 'assistant';
|
||||
}
|
||||
if ( $content->get_role() === Content_Role::SYSTEM ) {
|
||||
return 'system';
|
||||
}
|
||||
return 'user';
|
||||
},
|
||||
'content' => static function ( Content $content ) use ( $api_client ) {
|
||||
$parts = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Text_Part ) {
|
||||
$parts[] = array(
|
||||
'type' => 'text',
|
||||
'text' => $part->get_text(),
|
||||
);
|
||||
} else {
|
||||
throw $api_client->create_bad_request_exception(
|
||||
'The API only supports text, image, and audio parts.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generation configuration transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The generation configuration transformers.
|
||||
*/
|
||||
protected function get_generation_config_transformers(): array {
|
||||
return array(
|
||||
'stop' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_stop_sequences();
|
||||
},
|
||||
'response_format' => static function ( Text_Generation_Config $config ) {
|
||||
if ( $config->get_response_mime_type() === 'application/json' ) {
|
||||
$schema = $config->get_response_schema();
|
||||
if ( $schema ) {
|
||||
return array(
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => $schema,
|
||||
);
|
||||
}
|
||||
return array( 'type' => 'json_object' );
|
||||
}
|
||||
return array();
|
||||
},
|
||||
'n' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_candidate_count();
|
||||
},
|
||||
'max_completion_tokens' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_max_output_tokens();
|
||||
},
|
||||
'temperature' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_temperature();
|
||||
},
|
||||
'top_p' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_top_p();
|
||||
},
|
||||
'presence_penalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_presence_penalty();
|
||||
},
|
||||
'frequency_penalty' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_frequency_penalty();
|
||||
},
|
||||
'logprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_response_logprobs();
|
||||
},
|
||||
'top_logprobs' => static function ( Text_Generation_Config $config ) {
|
||||
return $config->get_logprobs();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Cache\Service_Request_Cache
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Cache;
|
||||
|
||||
use Exception;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class that allows to wrap service method calls so that their return values are cached.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Service_Request_Cache {
|
||||
|
||||
/**
|
||||
* Wraps the given method call in a WordPress transient so that its return value is cached.
|
||||
*
|
||||
* The transient name is generated based on the method name and arguments. It is unique per service and method
|
||||
* name, so that different services can have methods with the same name that are cached separately. It also
|
||||
* includes a timestamp of when the service configuration was last changed, so that the cache is invalidated as
|
||||
* needed.
|
||||
*
|
||||
* If the method throws an exception, the exception is cached as well, so that it can be rethrown on subsequent
|
||||
* calls.
|
||||
*
|
||||
* The transient is stored for 24 hours.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return mixed Method return value, potentially served from cache.
|
||||
*
|
||||
* @throws Exception Rethrown original exception from the method call, if there was one.
|
||||
*/
|
||||
public static function wrap_transient( string $service_slug, callable $method, array $args = array() ) {
|
||||
$key = self::get_cache_key( $method, $args );
|
||||
$last_changed = self::get_last_changed( $service_slug );
|
||||
|
||||
$transient_name = "ATFPP:{$service_slug}:{$key}:{$last_changed}";
|
||||
|
||||
$value = get_transient( $transient_name );
|
||||
if ( false === $value ) {
|
||||
$value = self::call_method( $method, $args );
|
||||
set_transient( $transient_name, self::sanitize_value_for_cache( $value ), DAY_IN_SECONDS );
|
||||
} else {
|
||||
$value = self::parse_value_from_cache( $value );
|
||||
}
|
||||
if ( $value instanceof Exception ) {
|
||||
throw $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given method call in the WordPress object cache so that its return value is cached.
|
||||
*
|
||||
* The cache key is generated based on the method name and arguments. It is unique per service and method name,
|
||||
* so that different services can have methods with the same name that are cached separately. It also includes
|
||||
* a timestamp of when the service configuration was last changed, so that the cache is invalidated as needed.
|
||||
*
|
||||
* The service slug is used as the cache group.
|
||||
*
|
||||
* If the method throws an exception, the exception is cached as well, so that it can be rethrown on subsequent
|
||||
* calls.
|
||||
*
|
||||
* The cached value is stored for 24 hours.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return mixed Method return value, potentially served from cache.
|
||||
*
|
||||
* @throws Exception Rethrown original exception from the method call, if there was one.
|
||||
*/
|
||||
public static function wrap_cache( string $service_slug, callable $method, array $args = array() ) {
|
||||
$key = self::get_cache_key( $method, $args );
|
||||
$last_changed = self::get_last_changed( $service_slug );
|
||||
|
||||
$cache_name = "{$key}:{$last_changed}";
|
||||
|
||||
$value = wp_cache_get( $cache_name, $service_slug );
|
||||
if ( false === $value ) {
|
||||
$value = self::call_method( $method, $args );
|
||||
wp_cache_set( $cache_name, self::sanitize_value_for_cache( $value ), $service_slug, DAY_IN_SECONDS );
|
||||
} else {
|
||||
$value = self::parse_value_from_cache( $value );
|
||||
}
|
||||
if ( $value instanceof Exception ) {
|
||||
throw $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the caches for a service.
|
||||
*
|
||||
* This method should be called whenever the configuration of a service changes, so that the caches are invalidated
|
||||
* and the next request will fetch fresh data. This encompasses both transients and the object cache.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
*/
|
||||
public static function invalidate_caches( string $service_slug ): void {
|
||||
self::set_last_changed( $service_slug );
|
||||
|
||||
// Not strictly necessary, but if we can clean up, let's do so.
|
||||
if (
|
||||
function_exists( 'wp_cache_flush_group' ) &&
|
||||
function_exists( 'wp_cache_supports' ) &&
|
||||
wp_cache_supports( 'flush_group' )
|
||||
) {
|
||||
wp_cache_flush_group( $service_slug );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given method with the given arguments, catching any exceptions that are thrown.
|
||||
*
|
||||
* If an exception is thrown, it will be returned instead of the method's return value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param callable $method Method to call.
|
||||
* @param mixed[] $args Method arguments.
|
||||
* @return mixed Method return value or exception.
|
||||
*/
|
||||
private static function call_method( callable $method, array $args ) {
|
||||
try {
|
||||
return call_user_func_array( $method, $args );
|
||||
} catch ( Exception $e ) {
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the given value to be stored in the cache.
|
||||
*
|
||||
* If the value is an exception, it is converted to an array with the exception class name and message.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @return mixed Sanitized value.
|
||||
*/
|
||||
private static function sanitize_value_for_cache( $value ) {
|
||||
// Exception thrown.
|
||||
if ( is_object( $value ) && $value instanceof Exception ) {
|
||||
return array(
|
||||
'classname' => get_class( $value ),
|
||||
'message' => $value->getMessage(),
|
||||
);
|
||||
}
|
||||
|
||||
// Arrayable class object.
|
||||
if ( is_object( $value ) && $value instanceof Arrayable && method_exists( get_class( $value ), 'from_array' ) ) {
|
||||
return array(
|
||||
'classname' => get_class( $value ),
|
||||
'data' => $value->to_array(),
|
||||
);
|
||||
}
|
||||
|
||||
// Array (recursion necessary).
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $key => $item ) {
|
||||
$value[ $key ] = self::sanitize_value_for_cache( $item );
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given value from the cache.
|
||||
*
|
||||
* This converts any sanitized exceptions back to their original exception form.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value Value from the cache.
|
||||
* @return mixed Parsed value.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the cached value uses an invalid class.
|
||||
*/
|
||||
private static function parse_value_from_cache( $value ) {
|
||||
// Exception thrown.
|
||||
if ( is_array( $value ) && isset( $value['classname'], $value['message'] ) ) {
|
||||
$class = $value['classname'];
|
||||
if ( ! class_exists( $class ) ) { // This should never be true, but a reasonable safeguard.
|
||||
$class = Exception::class;
|
||||
}
|
||||
$message = $value['message'];
|
||||
return new $class( $message );
|
||||
}
|
||||
|
||||
// Arrayable class object.
|
||||
if ( is_array( $value ) && isset( $value['classname'], $value['data'] ) ) {
|
||||
$class = $value['classname'];
|
||||
if ( ! class_exists( $class ) || ! method_exists( $class, 'from_array' ) ) { // This should never be true, but a reasonable safeguard.
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
/* translators: %s: class name */
|
||||
esc_html__( 'The class %s from the cached value does not exist or does not have a from_array method.', 'ai-services' ),
|
||||
esc_html( $class )
|
||||
)
|
||||
);
|
||||
}
|
||||
$data = $value['data'];
|
||||
return call_user_func( array( $class, 'from_array' ), $data );
|
||||
}
|
||||
|
||||
// Array (recursion necessary).
|
||||
if ( is_array( $value ) ) {
|
||||
foreach ( $value as $key => $item ) {
|
||||
$value[ $key ] = self::parse_value_from_cache( $item );
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache key for a method call.
|
||||
*
|
||||
* The returned key does not include the service slug, so the service slug has to be separately included as part of
|
||||
* the identifier for where to cache the value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param callable $method Method to cache.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return string Cache key.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the method is not a method on a service or model instance.
|
||||
*/
|
||||
private static function get_cache_key( callable $method, array $args = array() ): string {
|
||||
if ( ! is_array( $method ) || ! is_object( $method[0] ) || ! is_string( $method[1] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
esc_html__( 'Only methods on service and model instances can be cached.', 'ai-services' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $method[0] instanceof Generative_AI_Service ) {
|
||||
$type = 'service';
|
||||
} elseif ( $method[0] instanceof Generative_AI_Model ) {
|
||||
$type = 'model';
|
||||
} else {
|
||||
throw new InvalidArgumentException(
|
||||
esc_html__( 'Only methods on service and model instances can be cached.', 'ai-services' )
|
||||
);
|
||||
}
|
||||
|
||||
return $type . ':' . self::get_cache_hash( $method[1], $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache hash for a method call.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $method_name Method name.
|
||||
* @param mixed[] $args Optional. Method arguments. Default empty array.
|
||||
* @return string Cache hash.
|
||||
*/
|
||||
private static function get_cache_hash( string $method_name, array $args = array() ): string {
|
||||
$hash = $method_name;
|
||||
if ( ! empty( $args ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
$hash .= '_' . md5( serialize( $args ) );
|
||||
}
|
||||
return $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last changed value for a service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
* @return string UNIX timestamp for when the configuration of the service was last changed.
|
||||
*/
|
||||
private static function get_last_changed( string $service_slug ): string {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
return wp_cache_get_last_changed( $service_slug );
|
||||
}
|
||||
|
||||
$last_changed_option = (array) get_option( 'ais_services_last_changed', array() );
|
||||
if ( ! isset( $last_changed_option[ $service_slug ] ) ) {
|
||||
$last_changed_option[ $service_slug ] = microtime();
|
||||
update_option( 'ais_services_last_changed', $last_changed_option );
|
||||
}
|
||||
return $last_changed_option[ $service_slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last changed value for a service to the current UNIX timestamp.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug Service slug.
|
||||
*/
|
||||
private static function set_last_changed( string $service_slug ): void {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_set_last_changed( $service_slug );
|
||||
return;
|
||||
}
|
||||
|
||||
$last_changed_option = (array) get_option( 'ais_services_last_changed', array() );
|
||||
$last_changed_option[ $service_slug ] = microtime();
|
||||
update_option( 'ais_services_last_changed', $last_changed_option );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Authentication
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
|
||||
/**
|
||||
* Interface for a class representing authentication credentials of a certain kind for an API client.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Authentication {
|
||||
|
||||
/**
|
||||
* Authenticates the given request with the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance. Updated in place.
|
||||
*/
|
||||
public function authenticate( Request $request ): void;
|
||||
|
||||
/**
|
||||
* Sets the header name to use to add the credentials to a request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $header_name The header name.
|
||||
*/
|
||||
public function set_header_name( string $header_name ): void;
|
||||
|
||||
/**
|
||||
* Returns the option definitions needed to store the credentials.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $service_slug The service slug.
|
||||
* @return array<string, array<string, mixed>> The option definitions.
|
||||
*/
|
||||
public static function get_option_definitions( string $service_slug ): array;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generation_Config
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
|
||||
|
||||
/**
|
||||
* Interface for a class representing configuration options for a generative AI model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface Generation_Config extends Arrayable, With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* Returns the value for the given supported argument.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $name The argument name.
|
||||
* @return mixed The argument value, or its default value if not set.
|
||||
*/
|
||||
public function get_arg( string $name );
|
||||
|
||||
/**
|
||||
* Returns all formally supported arguments.
|
||||
*
|
||||
* Only includes arguments that have an explicit value set, i.e. not defaults.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The arguments.
|
||||
*/
|
||||
public function get_args(): array;
|
||||
|
||||
/**
|
||||
* Returns the additional arguments.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, mixed> The additional arguments.
|
||||
*/
|
||||
public function get_additional_args(): array;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a client for a generative AI web API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_API_Client {
|
||||
|
||||
/**
|
||||
* Creates a GET request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_get_request( string $path, array $params, array $request_options = array() ): Request;
|
||||
|
||||
/**
|
||||
* Creates a POST request instance for the given parameters.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $path The path to the API endpoint, relative to the base URL and version.
|
||||
* @param array<string, mixed> $params The request parameters.
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Request The request instance.
|
||||
*/
|
||||
public function create_post_request( string $path, array $params, array $request_options = array() ): Request;
|
||||
|
||||
/**
|
||||
* Sends the given request to the API and returns the response data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance.
|
||||
* @return Response The response instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while making the request.
|
||||
*/
|
||||
public function make_request( Request $request ): Response;
|
||||
|
||||
/**
|
||||
* Processes the response data from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data as associative array and should return the processed data in the desired
|
||||
* format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
public function process_response_data( Response $response, $process_callback );
|
||||
|
||||
/**
|
||||
* Processes the response body from the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response body. Receives the response body as
|
||||
* string and should return the processed data in the desired format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response body.
|
||||
*/
|
||||
public function process_response_body( Response $response, $process_callback );
|
||||
|
||||
/**
|
||||
* Processes the response data stream from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must implement With_Stream. The response data will
|
||||
* be processed in chunks, with each chunk of data being passed to the process
|
||||
* callback.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data (associative array) as first parameter, and the previous processed data
|
||||
* as second parameter (or null in case this is the first chunk). It should
|
||||
* return the processed data for the chunk in the desired format.
|
||||
* @return Generator Generator that yields the individual processed response data chunks.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
public function process_response_stream( Response $response, $process_callback ): Generator;
|
||||
|
||||
/**
|
||||
* Creates a new exception for a bad request, i.e. invalid or unsupported request data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return InvalidArgumentException The exception instance.
|
||||
*/
|
||||
public function create_bad_request_exception( string $message ): InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API request error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_request_exception( string $message ): Generative_AI_Exception;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_response_exception( string $message ): Generative_AI_Exception;
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error for a missing key.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $key The missing key in the response data.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
public function create_missing_response_key_exception( string $key ): Generative_AI_Exception;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a generative AI model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_Model {
|
||||
|
||||
/**
|
||||
* Gets the model slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The model slug.
|
||||
*/
|
||||
public function get_model_slug(): string;
|
||||
|
||||
/**
|
||||
* Gets the model metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Model_Metadata The model metadata.
|
||||
*/
|
||||
public function get_model_metadata(): Model_Metadata;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a class representing a generative AI service which provides access to models.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_service_slug(): string;
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_service_metadata(): Service_Metadata;
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* This is typically used to check whether the current service credentials are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*/
|
||||
public function is_connected(): bool;
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array;
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Support for the $tools and $toolConfig arguments was added.
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
* @throws Generative_AI_Exception Thrown if getting the model fails.
|
||||
*/
|
||||
public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_API_Client
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a service or model that uses an AI API client.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface With_API_Client {
|
||||
|
||||
/**
|
||||
* Gets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Generative_AI_API_Client The API client instance.
|
||||
*/
|
||||
public function get_api_client(): Generative_AI_API_Client;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Function_Calling
|
||||
*
|
||||
* @since 0.5.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which supports function calling.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
interface With_Function_Calling {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_JSON_Schema
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a class that provides a JSON schema for its input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
interface With_JSON_Schema {
|
||||
|
||||
/**
|
||||
* Returns the JSON schema for the expected input.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return array<string, mixed> The JSON schema.
|
||||
*/
|
||||
public static function get_json_schema(): array;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows multimodal input.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface With_Multimodal_Input {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output
|
||||
*
|
||||
* @since 0.6.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows multimodal output.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*/
|
||||
interface With_Multimodal_Output {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Text_Generation
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Interface for a model which allows generating text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
interface With_Text_Generation {
|
||||
|
||||
/**
|
||||
* Generates text content using the model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function generate_text( $content, array $request_options = array() ): Candidates;
|
||||
|
||||
/**
|
||||
* Generates text content using the model, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function stream_generate_text( $content, array $request_options = array() ): Generator;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\Contracts\With_Web_Search
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Contracts;
|
||||
|
||||
/**
|
||||
* Interface for a model which supports web search.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
interface With_Web_Search {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Decorators\AI_Service_Decorator
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Decorators;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class for an AI service that wraps another AI service through a decorator pattern.
|
||||
*
|
||||
* This class effectively acts as middleware for the underlying AI service, allowing for additional functionality to be
|
||||
* centrally provided.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class AI_Service_Decorator implements Generative_AI_Service {
|
||||
|
||||
/**
|
||||
* The underlying AI service to wrap.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Generative_AI_Service
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Generative_AI_Service $service The underlying AI service to wrap.
|
||||
*/
|
||||
public function __construct( Generative_AI_Service $service ) {
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_service_slug(): string {
|
||||
return $this->service->get_service_slug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_service_metadata(): Service_Metadata {
|
||||
return $this->service->get_service_metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service is connected.
|
||||
*
|
||||
* This is typically used to check whether the current service credentials are valid.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @return bool True if the service is connected, false otherwise.
|
||||
*/
|
||||
public function is_connected(): bool {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->service->is_connected();
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this->service, 'is_connected' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the available generative model slugs and their metadata.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Return type changed to a map of model data shapes.
|
||||
* @since 0.7.0 Return type changed to a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
public function list_models( array $request_options = array() ): array {
|
||||
if ( ! function_exists( 'get_transient' ) ) {
|
||||
// If the transient function is not available, we cannot cache the result.
|
||||
return $this->service->list_models( $request_options );
|
||||
}
|
||||
|
||||
return Service_Request_Cache::wrap_transient(
|
||||
$this->get_service_slug(),
|
||||
array( $this->service, 'list_models' ),
|
||||
array( $request_options )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generative model instance for the provided model parameters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Support for the $tools and $toolConfig arguments was added.
|
||||
*
|
||||
* @param array<string, mixed> $model_params {
|
||||
* Optional. Model parameters. Default empty array.
|
||||
*
|
||||
* @type string $feature Required. Unique identifier of the feature that the model
|
||||
* will be used for. Must only contain lowercase letters,
|
||||
* numbers, hyphens.
|
||||
* @type string $model The model slug. By default, the model will be determined
|
||||
* based on heuristics such as the requested capabilities.
|
||||
* @type string[] $capabilities Capabilities requested for the model to support. It is
|
||||
* recommended to specify this if you do not explicitly specify
|
||||
* a model slug.
|
||||
* @type Tools|null $tools The tools to use for the model. Default none.
|
||||
* @type Tool_Config|null $toolConfig Tool configuration options. Default none.
|
||||
* @type Generation_Config|null $generationConfig Model generation configuration options. Default none.
|
||||
* @type string|Parts|Content $systemInstruction The system instruction for the model. Default none.
|
||||
* }
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generative_AI_Model The generative model.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model slug or parameters are invalid.
|
||||
*/
|
||||
public function get_model( array $model_params = array(), array $request_options = array() ): Generative_AI_Model {
|
||||
if ( ! isset( $model_params['feature'] ) || ! preg_match( '/^[a-z0-9-]+$/', $model_params['feature'] ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'You must provide a "feature" identifier as part of the model parameters, which only contains lowercase letters, numbers, and hyphens.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the AI service model parameters before retrieving the model with them.
|
||||
*
|
||||
* This can be used, for example, to inject additional parameters via server-side logic based on the given
|
||||
* feature identifier.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters. Commonly supports at least the parameters
|
||||
* 'feature', 'capabilities', 'generationConfig' and
|
||||
* 'systemInstruction'.
|
||||
* @param string $service_slug The service slug.
|
||||
*
|
||||
* @return array<string, mixed> The processed model parameters.
|
||||
*/
|
||||
$filtered_model_params = (array) apply_filters( 'ai_services_model_params', $model_params, $this->service->get_service_slug() );
|
||||
|
||||
// Ensure that the feature identifier cannot be changed.
|
||||
$filtered_model_params['feature'] = $model_params['feature'];
|
||||
$model_params = $filtered_model_params;
|
||||
|
||||
// Perform basic validation so that the model classes don't have to.
|
||||
$this->validate_model_params( $model_params );
|
||||
|
||||
return $this->service->get_model( $model_params, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates various model parameters centrally.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
|
||||
*/
|
||||
private function validate_model_params( array $model_params ): void {
|
||||
if (
|
||||
isset( $model_params['tools'] )
|
||||
&& ! $model_params['tools'] instanceof Tools
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The tools argument must be an instance of %s.',
|
||||
Tools::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
isset( $model_params['toolConfig'] )
|
||||
&& ! $model_params['toolConfig'] instanceof Tool_Config
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The tool config argument must be an instance of %s.',
|
||||
Tool_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
isset( $model_params['generationConfig'] )
|
||||
&& ! $model_params['generationConfig'] instanceof Generation_Config
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The generation config argument must be an instance of %s.',
|
||||
Generation_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $model_params['systemInstruction'] )
|
||||
&& ! is_string( $model_params['systemInstruction'] )
|
||||
&& ! $model_params['systemInstruction'] instanceof Parts
|
||||
&& ! $model_params['systemInstruction'] instanceof Content
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The system instruction argument must be either a string, or an instance of %1$s, or an instance of %2$s.',
|
||||
'Parts',
|
||||
'Content'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class for an exception thrown when a runtime error occurs in a generative AI service, e.g. a failing API request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
class Generative_AI_Exception extends RuntimeException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler
|
||||
*
|
||||
* @since 0.6.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP\Contracts;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Stream_Response;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
|
||||
/**
|
||||
* Interface for a request handler that can stream responses.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*/
|
||||
interface Stream_Request_Handler {
|
||||
|
||||
/**
|
||||
* Sends an HTTP request and streams the response.
|
||||
*
|
||||
* @since 0.6.0
|
||||
*
|
||||
* @param Request $request The request to send.
|
||||
* @return Stream_Response The stream response.
|
||||
*
|
||||
* @throws Request_Exception Thrown if the request fails.
|
||||
*/
|
||||
public function request_stream( Request $request ): Stream_Response;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP\Contracts;
|
||||
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Interface for a class that contains a readable stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
interface With_Stream {
|
||||
|
||||
/**
|
||||
* Returns a generator that reads individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The generator for the response stream.
|
||||
*/
|
||||
public function read_stream(): Generator;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\HTTP;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
/**
|
||||
* Extended HTTP class with support for streaming responses.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*/
|
||||
final class HTTP_With_Streams extends HTTP implements Stream_Request_Handler {
|
||||
|
||||
/**
|
||||
* Guzzle client instance.
|
||||
*
|
||||
* Used for streaming requests, as WordPress Core's Requests API does not support this.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var Client
|
||||
*/
|
||||
private $guzzle;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param array<string, mixed> $default_options Optional. Default options to use for all requests. Default empty
|
||||
* array.
|
||||
*/
|
||||
public function __construct( array $default_options = array() ) {
|
||||
parent::__construct( $default_options );
|
||||
|
||||
$this->guzzle = new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request and streams the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Request $request The request to send.
|
||||
* @return Stream_Response The stream response.
|
||||
*
|
||||
* @throws Request_Exception Thrown if the request fails.
|
||||
*/
|
||||
public function request_stream( Request $request ): Stream_Response {
|
||||
$request_args = $this->build_request_args( $request );
|
||||
|
||||
$request_options = array(
|
||||
'allow_redirects' => $request_args['options']['redirection'] > 0 ? array( 'max' => $request_args['options']['redirection'] ) : false,
|
||||
'timeout' => (float) $request_args['options']['timeout'],
|
||||
'stream' => true,
|
||||
);
|
||||
if ( isset( $request_args['data'] ) ) {
|
||||
if ( in_array( $request_args['type'], array( Request::HEAD, Request::GET, Request::DELETE ), true ) ) {
|
||||
$request_options['query'] = $request_args['data'];
|
||||
} else {
|
||||
if ( ! is_string( $request_args['data'] ) ) {
|
||||
$request_args['data'] = http_build_query( $request_args['data'], '', '&' );
|
||||
}
|
||||
$request_options['body'] = $request_args['data'];
|
||||
}
|
||||
}
|
||||
if ( isset( $request_args['headers'] ) ) {
|
||||
if ( ! isset( $request_args['headers']['User-Agent'] ) ) {
|
||||
$request_args['headers']['User-Agent'] = $request_args['options']['user-agent'];
|
||||
}
|
||||
} else {
|
||||
$request_args['headers'] = array(
|
||||
'User-Agent' => $request_args['options']['user-agent'],
|
||||
);
|
||||
}
|
||||
$request_options['headers'] = $request_args['headers'];
|
||||
|
||||
try {
|
||||
$response = $this->guzzle->request(
|
||||
$request_args['type'],
|
||||
$request_args['url'],
|
||||
$request_options
|
||||
);
|
||||
} catch ( ClientException $e ) {
|
||||
throw new Request_Exception(
|
||||
htmlspecialchars( $e->getMessage() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
);
|
||||
}
|
||||
|
||||
$headers = $this->sanitize_headers( $response->getHeaders() );
|
||||
|
||||
return new Stream_Response( $response->getStatusCode(), $response->getBody(), $headers );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\HTTP\Stream_Response
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\HTTP;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Generic_Response;
|
||||
use ATFPP\AI_Translate_Dependencies\Psr\Http\Message\StreamInterface;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* Class for a HTTP response that uses streaming.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @implements IteratorAggregate<Generator>
|
||||
*/
|
||||
class Stream_Response extends Generic_Response implements With_Stream, IteratorAggregate {
|
||||
|
||||
/**
|
||||
* The stream to read from.
|
||||
*
|
||||
* @since 0.3.0
|
||||
* @var StreamInterface
|
||||
*/
|
||||
private $stream;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param int $status The HTTP status code received with the response.
|
||||
* @param StreamInterface $stream The response body stream to read from.
|
||||
* @param array<string, string> $headers The headers received with the response.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the $stream parameter has an invalid type.
|
||||
*/
|
||||
public function __construct( int $status, StreamInterface $stream, array $headers ) {
|
||||
parent::__construct( $status, '', $headers );
|
||||
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a generator that reads individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The generator for the response stream.
|
||||
*/
|
||||
public function read_stream(): Generator {
|
||||
while ( ! $this->stream->eof() ) {
|
||||
$line = $this->read_line( $this->stream );
|
||||
$data = json_decode( $line, true );
|
||||
if ( ! $data ) {
|
||||
continue;
|
||||
}
|
||||
yield $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an iterator reading individual chunks of decoded JSON data from the streamed response body.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @return Generator The iterator for the response stream.
|
||||
*/
|
||||
public function getIterator(): Generator {
|
||||
return $this->read_stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a line from the stream.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param StreamInterface $stream The stream to read from.
|
||||
* @return string The line read from the stream.
|
||||
*/
|
||||
private function read_line( $stream ): string {
|
||||
$buffer = '';
|
||||
|
||||
while ( ! $stream->eof() ) {
|
||||
$buffer .= $stream->read( 1 );
|
||||
|
||||
if ( strlen( $buffer ) === 1 && '{' !== $buffer ) {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
if ( json_decode( $buffer ) !== null ) {
|
||||
return $buffer;
|
||||
}
|
||||
}
|
||||
|
||||
return rtrim( $buffer, ']' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Options\Option_Encrypter
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Options;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Util\Data_Encryption;
|
||||
|
||||
/**
|
||||
* Class that allows for options to be encrypted when stored in the database as well as decrypted when retrieved.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Option_Encrypter {
|
||||
|
||||
const ENCRYPTION_PREFIX = 'enc::';
|
||||
|
||||
/**
|
||||
* The data encryption instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Data_Encryption
|
||||
*/
|
||||
private $data_encryption;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Data_Encryption $data_encryption The data encryption instance.
|
||||
*/
|
||||
public function __construct( Data_Encryption $data_encryption ) {
|
||||
$this->data_encryption = $data_encryption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relevant hooks to handle encryption and decryption of the given option.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $option_slug The option to use encryption with.
|
||||
*/
|
||||
public function add_encryption_hooks( string $option_slug ): void {
|
||||
add_filter( "sanitize_option_{$option_slug}", array( $this, 'encrypt_option' ), 9999, 2 ); // Encrypt late.
|
||||
add_filter( "option_{$option_slug}", array( $this, 'decrypt_option' ), -9999, 2 ); // Decrypt early.
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given option has encryption enabled.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return bool True if the option has encryption enabled, false otherwise.
|
||||
*/
|
||||
public function has_encryption( string $option_slug ): bool {
|
||||
return (
|
||||
has_filter( "sanitize_option_{$option_slug}", array( $this, 'encrypt_option' ) ) &&
|
||||
has_filter( "option_{$option_slug}", array( $this, 'decrypt_option' ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given option value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value The option value to encrypt.
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return string Encrypted option value.
|
||||
*/
|
||||
public function encrypt_option( $value, string $option_slug ): string {
|
||||
// Do not encrypt if the value is empty.
|
||||
if ( '' === $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Bail if the value is already encrypted.
|
||||
if ( is_string( $value ) && str_starts_with( $value, self::ENCRYPTION_PREFIX ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encrypted = $this->data_encryption->encrypt( maybe_serialize( $value ) );
|
||||
|
||||
// If encryption fails, trigger a warning but continue with the unencrypted value. Better not to lose data.
|
||||
if ( ! $encrypted ) {
|
||||
$this->trigger_error(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Option slug */
|
||||
__( 'Failed to encrypt the value for the option "%s".', 'ai-services' ),
|
||||
$option_slug
|
||||
)
|
||||
);
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::ENCRYPTION_PREFIX . $encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given option value.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param mixed $value The option value to decrypt.
|
||||
* @param string $option_slug The identifier/name of the option.
|
||||
* @return mixed Decrypted option value.
|
||||
*/
|
||||
public function decrypt_option( $value, string $option_slug ) {
|
||||
// Bail if the value is already decrypted.
|
||||
if ( ! is_string( $value ) || ! str_starts_with( $value, self::ENCRYPTION_PREFIX ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$decrypted = $this->data_encryption->decrypt( substr( $value, strlen( self::ENCRYPTION_PREFIX ) ) );
|
||||
|
||||
// If decryption fails, trigger a warning and return an empty string.
|
||||
if ( ! $decrypted ) {
|
||||
$this->trigger_error(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
/* translators: %s: Option slug */
|
||||
__( 'Failed to decrypt the value for the option "%s".', 'ai-services' ),
|
||||
$option_slug
|
||||
)
|
||||
);
|
||||
return '';
|
||||
}
|
||||
|
||||
return maybe_unserialize( $decrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an error, if WP_DEBUG is enabled.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $function_name The name of the function that triggered the error.
|
||||
* @param string $message The message explaining the error.
|
||||
* @param int $error_level Optional. The designated error type for this error.
|
||||
* Only works with E_USER family of constants. Default E_USER_NOTICE.
|
||||
*/
|
||||
private function trigger_error( string $function_name, string $message, int $error_level = E_USER_NOTICE ): void {
|
||||
// The wp_trigger_error() function was only added in WordPress 6.4, so this is a minimal shim.
|
||||
if ( ! function_exists( 'wp_trigger_error' ) ) {
|
||||
if ( ! WP_DEBUG ) {
|
||||
return;
|
||||
}
|
||||
if ( ! empty( $function_name ) ) {
|
||||
$message = sprintf( '%s(): %s', $function_name, $message );
|
||||
}
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error( esc_html( $message ), $error_level );
|
||||
return;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
wp_trigger_error( $function_name, $message, $error_level );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Service_Registration
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Service_Type;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Authentication\API_Key_Authentication;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Decorators\AI_Service_Decorator;
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class representing a service registration.
|
||||
*
|
||||
* This is an internal class and NOT the actual service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Service_Registration {
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* Whether the service can be overridden through another registration with the same slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var bool
|
||||
*/
|
||||
private $allow_override;
|
||||
|
||||
/**
|
||||
* The service creator.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var callable
|
||||
*/
|
||||
private $creator;
|
||||
|
||||
/**
|
||||
* The service instance arguments.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $instance_args;
|
||||
|
||||
/**
|
||||
* The authentication option slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string[]
|
||||
*/
|
||||
private $authentication_option_slugs;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 The service argument keys were updated.
|
||||
*
|
||||
* @param string $slug The service slug. Must only contain lowercase letters, numbers, hyphens.
|
||||
* @param callable $creator The service creator. Receives the Service_Registration_Context as sole
|
||||
* parameter and must return a Generative_AI_Service instance. The parameter
|
||||
* provides access to the service metadata and other relevant dependencies.
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. The service arguments. Default empty array.
|
||||
*
|
||||
* @type string $name The service name. Default is the slug with spaces and uppercase
|
||||
* first letters.
|
||||
* @type string $credentials_url The URL to manage credentials for the service. Default empty
|
||||
* string.
|
||||
* @type string $type The service type. Default is Service_Type::CLOUD.
|
||||
* @type string[] $capabilities The list of AI capabilities supported by the service and its
|
||||
* models. Default empty array.
|
||||
* @type bool $allow_override Whether the service can be overridden by another service with
|
||||
* the same slug. Default true.
|
||||
* @type Request_Handler $request_handler The request handler instance. Default is a new HTTP_With_Streams
|
||||
* instance.
|
||||
* @type Container $container The container instance with data for the API key options.
|
||||
* Default is a new Option_Container instance.
|
||||
* @type Key_Value_Repository $repository The repository instance to read API keys Default is a new
|
||||
* Option_Repository instance.
|
||||
* }
|
||||
*/
|
||||
public function __construct( string $slug, callable $creator, array $args = array() ) {
|
||||
$this->metadata = Service_Metadata::from_array( array_merge( array( 'slug' => $slug ), $args ) );
|
||||
|
||||
$this->creator = $creator;
|
||||
$this->allow_override = isset( $args['allow_override'] ) ? (bool) $args['allow_override'] : true;
|
||||
$this->instance_args = $this->parse_instance_args( $args );
|
||||
|
||||
$option_definitions = array();
|
||||
if ( $this->metadata->get_type() === Service_Type::CLOUD ) {
|
||||
$option_definitions = API_Key_Authentication::get_option_definitions( $slug );
|
||||
}
|
||||
|
||||
$this->authentication_option_slugs = array();
|
||||
foreach ( $option_definitions as $option_slug => $option_args ) {
|
||||
$this->authentication_option_slugs[] = $option_slug;
|
||||
$this->instance_args['container'][ $option_slug ] = function () use ( $option_slug, $option_args ) {
|
||||
return new Option(
|
||||
$this->instance_args['repository'],
|
||||
$option_slug,
|
||||
$option_args
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_metadata(): Service_Metadata {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication option instances.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Option[] The authentication option instances.
|
||||
*/
|
||||
public function get_authentication_options(): array {
|
||||
return array_map(
|
||||
function ( string $option_slug ) {
|
||||
return $this->instance_args['container'][ $option_slug ];
|
||||
},
|
||||
$this->authentication_option_slugs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authentication option slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string[] The authentication option slugs.
|
||||
*/
|
||||
public function get_authentication_option_slugs(): array {
|
||||
return $this->authentication_option_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the service.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Generative_AI_Service The service instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if no API key is set for the service or if the service creator's return value is
|
||||
* not a valid Generative_AI_Service instance.
|
||||
*/
|
||||
public function create_instance(): Generative_AI_Service {
|
||||
$authentication_options = $this->get_authentication_options();
|
||||
|
||||
$slug = $this->metadata->get_slug();
|
||||
|
||||
$authentication = null;
|
||||
if ( count( $authentication_options ) > 0 ) {
|
||||
// For now an API key is the only authentication method supported.
|
||||
$api_key = $authentication_options[0]->get_value();
|
||||
if ( ! $api_key ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Cannot instantiate service %s without an API key.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$authentication = new API_Key_Authentication( $api_key );
|
||||
}
|
||||
|
||||
$context = new Service_Registration_Context(
|
||||
$slug,
|
||||
$this->metadata,
|
||||
$this->instance_args['request_handler'],
|
||||
$authentication
|
||||
);
|
||||
|
||||
$instance = ( $this->creator )( $context );
|
||||
if ( ! $instance instanceof Generative_AI_Service ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %s must return an instance of Generative_AI_Service.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $instance->get_service_slug() !== $slug ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %1$s must return an instance of Generative_AI_Service with the same slug, but instead it returned another slug %2$s.',
|
||||
htmlspecialchars( $slug ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $instance->get_service_slug() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $instance->get_service_metadata() !== $this->metadata ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'The service creator for %s must return an instance of Generative_AI_Service with the same metadata, but instead it returned different metadata.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the instance in a decorator for centralized functionality.
|
||||
return new AI_Service_Decorator( $instance );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service can be overridden.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return bool True if the service can be overridden, false otherwise.
|
||||
*/
|
||||
public function allows_override(): bool {
|
||||
return $this->allow_override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the service registration instance arguments.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args The service registration instance arguments.
|
||||
* @return array<string, mixed> The parsed service registration instance arguments.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid instance argument is provided.
|
||||
*/
|
||||
private function parse_instance_args( array $args ): array {
|
||||
$requirements_map = array(
|
||||
'request_handler' => array( Request_Handler::class, HTTP_With_Streams::class ),
|
||||
'container' => array( Container::class, Option_Container::class ),
|
||||
'repository' => array( Key_Value_Repository::class, Option_Repository::class ),
|
||||
);
|
||||
|
||||
$instance_args = array();
|
||||
foreach ( $requirements_map as $key => $requirements ) {
|
||||
list( $interface_name, $class_name ) = $requirements;
|
||||
|
||||
if ( isset( $args[ $key ] ) ) {
|
||||
if ( ! $args[ $key ] instanceof $interface_name ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The %1$s argument must be an instance of %2$s.',
|
||||
htmlspecialchars( $key ), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
htmlspecialchars( $interface_name ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
$instance_args[ $key ] = $args[ $key ];
|
||||
} else {
|
||||
$instance_args[ $key ] = new $class_name();
|
||||
}
|
||||
}
|
||||
|
||||
return $instance_args;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Service_Registration_Context
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Authentication;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
|
||||
/**
|
||||
* Value class with service context dependencies and data that can be used to create a service instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
final class Service_Registration_Context {
|
||||
|
||||
/**
|
||||
* The service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var string
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* The service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Service_Metadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* The service request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The service authentication instance, if any.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Authentication|null
|
||||
*/
|
||||
private $authentication;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @param Service_Metadata $metadata The service metadata.
|
||||
* @param Request_Handler $request_handler The service request handler instance.
|
||||
* @param Authentication|null $authentication Optional. The service authentication instance, if any. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
string $slug,
|
||||
Service_Metadata $metadata,
|
||||
Request_Handler $request_handler,
|
||||
?Authentication $authentication = null
|
||||
) {
|
||||
$this->slug = $slug;
|
||||
$this->metadata = $metadata;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->authentication = $authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return string The service slug.
|
||||
*/
|
||||
public function get_slug(): string {
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Service_Metadata The service metadata.
|
||||
*/
|
||||
public function get_metadata(): Service_Metadata {
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service request handler instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Request_Handler The service request handler instance.
|
||||
*/
|
||||
public function get_request_handler(): Request_Handler {
|
||||
return $this->request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service authentication instance, if any.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Authentication|null The service authentication instance, if any.
|
||||
*/
|
||||
public function get_authentication(): ?Authentication {
|
||||
return $this->authentication;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_API
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Cache\Service_Request_Cache;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Service;
|
||||
use ATFPP\AI_Translate\Services\Options\Option_Encrypter;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Key_Value_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Main API class providing the entry point to the generative AI services.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_API {
|
||||
|
||||
/**
|
||||
* The service registration definitions, keyed by service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, Service_Registration>
|
||||
*/
|
||||
private $service_registrations = array();
|
||||
|
||||
/**
|
||||
* The service instances, keyed by service slug.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array<string, Generative_AI_Service>
|
||||
*/
|
||||
private $service_instances = array();
|
||||
|
||||
/**
|
||||
* The current user instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Current_User
|
||||
*/
|
||||
private $current_user;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The container instance with data for the API key options.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* The repository instance to read API keys.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Key_Value_Repository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* The option encrypter instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Option_Encrypter
|
||||
*/
|
||||
private $option_encrypter;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 The constructor parameters were updated.
|
||||
*
|
||||
* @param Current_User $current_user The current user instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Container $container The container instance with data for the API key options.
|
||||
* @param Key_Value_Repository $repository The repository instance to read API keys.
|
||||
* @param Option_Encrypter|null $option_encrypter Optional. The option encrypter instance. If not provided, the
|
||||
* API key options are assumed to not be encrypted. Default null.
|
||||
*/
|
||||
public function __construct(
|
||||
Current_User $current_user,
|
||||
Request_Handler $request_handler,
|
||||
Container $container,
|
||||
Key_Value_Repository $repository,
|
||||
?Option_Encrypter $option_encrypter = null
|
||||
) {
|
||||
$this->current_user = $current_user;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->container = $container;
|
||||
$this->repository = $repository;
|
||||
$this->option_encrypter = $option_encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a generative AI service.
|
||||
*
|
||||
* An AI service consists at least of a service class that implements the Generative_AI_Service interface and a
|
||||
* model class that implements the Generative_AI_Model interface.
|
||||
*
|
||||
* Consumers of the service will access the service class through a proxy wrapper class which automatically handles
|
||||
* caching and other infrastructure concerns. It is therefore advised to not implement any caching concerns in the
|
||||
* service class itself as well as to not implement any public methods other than those required by the relevant
|
||||
* interfaces.
|
||||
*
|
||||
* The $creator parameter of this method needs to return the instance of the service class.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @see Generative_AI_Service
|
||||
*
|
||||
* @param string $slug The service slug. Must only contain lowercase letters, numbers, hyphens. It
|
||||
* must be unique and must match the service slug returned by the service
|
||||
* class.
|
||||
* @param callable $creator The service creator. Receives the Service_Registration_Context as sole
|
||||
* parameter and must return a Generative_AI_Service instance. The parameter
|
||||
* provides access to the service metadata and other relevant dependencies.
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. The service arguments. Default empty array.
|
||||
*
|
||||
* @type string $name The user-facing service name. Default is the slug with spaces and uppercase
|
||||
* first letters.
|
||||
* @type string $credentials_url The URL to manage credentials for the service. Default empty string.
|
||||
* @type bool $allow_override Whether the service can be overridden by another service with the same slug.
|
||||
* Default true.
|
||||
* }
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an already registered slug or invalid arguments are provided.
|
||||
*/
|
||||
public function register_service( string $slug, callable $creator, array $args = array() ): void {
|
||||
if ( 'browser' === $slug ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is reserved for in-browser AI and cannot be registered.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $this->service_registrations[ $slug ] ) && ! $this->service_registrations[ $slug ]->allows_override() ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is already registered and cannot be overridden.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$args['request_handler'] = $this->request_handler;
|
||||
$args['container'] = $this->container;
|
||||
$args['repository'] = $this->repository;
|
||||
|
||||
$this->service_registrations[ $slug ] = new Service_Registration( $slug, $creator, $args );
|
||||
|
||||
$option_slugs = $this->service_registrations[ $slug ]->get_authentication_option_slugs();
|
||||
foreach ( $option_slugs as $option_slug ) {
|
||||
// Ensure the authentication options are encrypted.
|
||||
if ( null !== $this->option_encrypter && ! $this->option_encrypter->has_encryption( $option_slug ) ) {
|
||||
$this->option_encrypter->add_encryption_hooks( $option_slug );
|
||||
}
|
||||
|
||||
/*
|
||||
* If the repository uses WordPress options, ensure the authentication options are invalidated when the
|
||||
* credentials change.
|
||||
*/
|
||||
if ( $this->repository instanceof Option_Repository ) {
|
||||
$invalid_service_caches = static function () use ( $slug ) {
|
||||
Service_Request_Cache::invalidate_caches( $slug );
|
||||
};
|
||||
add_action( "add_option_{$option_slug}", $invalid_service_caches );
|
||||
add_action( "update_option_{$option_slug}", $invalid_service_caches );
|
||||
add_action( "delete_option_{$option_slug}", $invalid_service_caches );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a service is registered.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return bool True if the service is registered, false otherwise.
|
||||
*/
|
||||
public function is_service_registered( string $slug ): bool {
|
||||
return isset( $this->service_registrations[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a service is available.
|
||||
*
|
||||
* For a service to be considered available, all of the following conditions must be met:
|
||||
* - The service is registered.
|
||||
* - The service has an API key set.
|
||||
* - The API key is valid.
|
||||
* - The current user has the necessary capabilities.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return bool True if the service is available, false otherwise.
|
||||
*/
|
||||
public function is_service_available( string $slug ): bool {
|
||||
/*
|
||||
* If the service was already instantiated in the class, it is available.
|
||||
* In that case, the only thing left to check is whether the current user has the necessary capabilities.
|
||||
*/
|
||||
if ( isset( $this->service_instances[ $slug ] ) ) {
|
||||
if ( ! $this->current_user->has_cap( 'ais_access_service', $slug ) ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the service is not registered, it is not available.
|
||||
if ( ! isset( $this->service_registrations[ $slug ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any authentication credentials are missing for the service, it is not available.
|
||||
$authentication_options = $this->service_registrations[ $slug ]->get_authentication_options();
|
||||
foreach ( $authentication_options as $option ) {
|
||||
if ( ! $option->get_value() ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test whether the API key is valid by listing the models.
|
||||
$instance = $this->service_registrations[ $slug ]->create_instance();
|
||||
if ( ! $instance->is_connected() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If so, the service is available so we can store the instance.
|
||||
$this->service_instances[ $slug ] = $instance;
|
||||
|
||||
// Finally, check whether the current user has the necessary capabilities.
|
||||
return $this->current_user->has_cap( 'ais_access_service', $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether any services are available.
|
||||
*
|
||||
* For some use-cases it may be acceptable to use any AI service. In those cases, this method can be used to check
|
||||
* whether any services are available. If so, an arbitrary available service can be retrieved using the
|
||||
* {@see Services_API::get_available_service()} method.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args {
|
||||
* Optional. Arguments to filter the services to consider. By default, any available service is considered.
|
||||
*
|
||||
* @type string[] $slugs List of service slugs, to only consider any of these services.
|
||||
* @type string[] $capabilities List of AI capabilities, to only consider services that support all of these
|
||||
* capabilities.
|
||||
* }
|
||||
* @return bool True if any of the services are available, false otherwise.
|
||||
*/
|
||||
public function has_available_services( array $args = array() ): bool {
|
||||
$slug = $this->get_available_service_slug( $args );
|
||||
return '' !== $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a generative AI service instance that is available for use.
|
||||
*
|
||||
* If you intend to call this method with a specific service slug, you should first check whether the service is
|
||||
* available using {@see Services_API::is_service_available()}.
|
||||
*
|
||||
* If you intend to call this method to get any service (optionally with additional criteria to satisfy), you
|
||||
* should first check if any of the services are available using {@see Services_API::has_available_services()}.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|array<string, mixed> $args Optional. Either a single service slug to get that service, or
|
||||
* arguments to get any service that satisfies the criteria from these
|
||||
* arguments. See {@see Services_API::has_available_services()} for the
|
||||
* possible arguments. Default is an empty array so that any available
|
||||
* service is considered.
|
||||
* @return Generative_AI_Service The available service instance.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no service corresponding to the given arguments is available.
|
||||
*/
|
||||
public function get_available_service( $args = array() ): Generative_AI_Service {
|
||||
if ( is_string( $args ) ) {
|
||||
$slug = $args;
|
||||
if ( ! $this->is_service_available( $slug ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Service %s is either not registered or not available.',
|
||||
htmlspecialchars( $slug ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->service_instances[ $slug ];
|
||||
}
|
||||
|
||||
$slug = $this->get_available_service_slug( $args );
|
||||
if ( '' === $slug ) {
|
||||
if ( count( $args ) > 0 ) {
|
||||
$message = 'No service satisfying the given arguments is registered and available.';
|
||||
} else {
|
||||
$message = 'No service is registered and available.';
|
||||
}
|
||||
throw new InvalidArgumentException( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $this->service_instances[ $slug ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service metadata for a given service slug.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $slug The service slug.
|
||||
* @return Service_Metadata|null The service metadata, or null if the service is not registered.
|
||||
*/
|
||||
public function get_service_metadata( string $slug ): ?Service_Metadata {
|
||||
if ( ! isset( $this->service_registrations[ $slug ] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->service_registrations[ $slug ]->get_metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of all registered service slugs.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string[] The list of registered service slugs.
|
||||
*/
|
||||
public function get_registered_service_slugs(): array {
|
||||
return array_keys( $this->service_registrations );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first available service slug, optionally satisfying the given criteria.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array<string, mixed> $args Optional. Arguments to filter the services to consider. See
|
||||
* {@see Services_API::has_available_services()} for the possible arguments.
|
||||
* By default, any available service is considered.
|
||||
* @return string The first available service slug, or empty string if no service is available.
|
||||
*/
|
||||
private function get_available_service_slug( array $args = array() ): string {
|
||||
$slugs = $args['slugs'] ?? $this->get_registered_service_slugs();
|
||||
|
||||
foreach ( $slugs as $slug ) {
|
||||
if ( ! $this->is_service_available( $slug ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $args['capabilities'] ) ) {
|
||||
$metadata = $this->get_service_metadata( $slug );
|
||||
if ( ! $metadata ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing_capabilities = array_diff( $args['capabilities'], $metadata->get_capabilities() );
|
||||
if ( count( $missing_capabilities ) > 0 ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_API_Instance
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class to provide singleton-like access to the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_API_Instance {
|
||||
|
||||
/**
|
||||
* Retrieve the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Services_API|null The canonical Services_API instance.
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Retrieves the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Services_API The canonical Services_API instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the method is called too early when no instance has been set before.
|
||||
*/
|
||||
public static function get(): Services_API {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
throw new RuntimeException(
|
||||
'Cannot get Services_API instance before it was set.'
|
||||
);
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the canonical Services_API instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Services_API $instance The canonical Services_API instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the method is called after the instance has already been set.
|
||||
*/
|
||||
public static function set( Services_API $instance ): void {
|
||||
if ( isset( self::$instance ) ) {
|
||||
throw new RuntimeException(
|
||||
'Cannot set Services_API instance after it has already been set.'
|
||||
);
|
||||
}
|
||||
|
||||
self::$instance = $instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_Loader
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Controller;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\With_Hooks;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
|
||||
/**
|
||||
* Loader class responsible for initializing the AI services functionality, including its public API.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_Loader implements With_Hooks {
|
||||
|
||||
/**
|
||||
* Service container for the class's dependencies.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
*/
|
||||
public function __construct( string $main_file ) {
|
||||
$this->container = $this->set_up_container( $main_file );
|
||||
Services_API_Instance::set( $this->container['api'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds relevant WordPress hooks.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function add_hooks(): void {
|
||||
$this->add_cleanup_hooks();
|
||||
$this->load_capabilities();
|
||||
$this->load_options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cleanup hooks related to plugin deactivation.
|
||||
*
|
||||
* @since 0.4.0
|
||||
*/
|
||||
private function add_cleanup_hooks(): void {
|
||||
// This function is only available in WordPress 6.4+.
|
||||
if ( ! function_exists( 'wp_set_options_autoload' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable autoloading of plugin options on deactivation.
|
||||
register_deactivation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// For network-wide deactivation, this cleanup cannot be reliably implemented.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
false
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Reinstate original autoload settings on (re-)activation.
|
||||
register_activation_hook(
|
||||
$this->container['plugin_env']->main_file(),
|
||||
function ( $network_wide ) {
|
||||
// See deactivation hook for network-wide cleanup limitations.
|
||||
if ( $network_wide ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$autoloaded_options = $this->get_autoloaded_options();
|
||||
if ( ! $autoloaded_options ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_set_options_autoload(
|
||||
$autoloaded_options,
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the services capabilities and sets up the relevant filters.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function load_capabilities(): void {
|
||||
add_action(
|
||||
'plugins_loaded',
|
||||
function () {
|
||||
$controller = $this->container['capability_controller'];
|
||||
|
||||
/**
|
||||
* Fires when the services capabilities are loaded.
|
||||
*
|
||||
* This hook allows you to modify the rules for how these capabilities are granted. The capabilities
|
||||
* available in the controller are:
|
||||
*
|
||||
* - 'ais_manage_services' (base capability)
|
||||
* - 'ais_access_services' (base capability)
|
||||
* - 'ais_access_service' (meta capability, called with the specific service slug as parameter)
|
||||
* - 'ais_use_playground' (meta capability)
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Capability_Controller $controller The capability controller, which can be used to modify the
|
||||
* rules for how capabilities are granted.
|
||||
*/
|
||||
do_action( 'ais_load_services_capabilities', $controller );
|
||||
|
||||
$this->container['capability_filters']->add_hooks();
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the services options.
|
||||
*
|
||||
* The option container is populated with options dynamically based on registered AI services. Each of the relevant
|
||||
* options will be registered here.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function load_options(): void {
|
||||
add_action(
|
||||
'init',
|
||||
function () {
|
||||
$registry = $this->container['option_registry'];
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
$option = $this->container['option_container']->get( $key );
|
||||
$registry->register(
|
||||
$option->get_key(),
|
||||
$option->get_registration_args()
|
||||
);
|
||||
}
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service option names that are autoloaded.
|
||||
*
|
||||
* @since 0.4.0
|
||||
*
|
||||
* @return string[] List of autoloaded service options.
|
||||
*/
|
||||
private function get_autoloaded_options(): array {
|
||||
$autoloaded_options = array();
|
||||
|
||||
foreach ( $this->container['option_container']->get_keys() as $key ) {
|
||||
// Trigger option instantiation so that the autoload config is populated.
|
||||
$this->container['option_container']->get( $key );
|
||||
|
||||
$autoload = $this->container['option_repository']->get_autoload_config( $key );
|
||||
|
||||
if ( true === $autoload ) {
|
||||
$autoloaded_options[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $autoloaded_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the services service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return Service_Container The services service container.
|
||||
*/
|
||||
private function set_up_container( string $main_file ): Service_Container {
|
||||
$builder = new Services_Service_Container_Builder();
|
||||
|
||||
return $builder->build_env( $main_file )
|
||||
->build_services()
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Services_Service_Container_Builder
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services;
|
||||
|
||||
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
|
||||
use ATFPP\AI_Translate\Services\Options\Option_Encrypter;
|
||||
use ATFPP\AI_Translate\Services\Util\Data_Encryption;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Admin_Pages\Admin_Menu;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Base_Capability;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Controller;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Capability_Filters;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Capabilities\Meta_Capability;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Current_User;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Plugin_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Service_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Site_Env;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Meta\Meta_Repository;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Container;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Registry;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\Options\Option_Repository;
|
||||
|
||||
/**
|
||||
* Service container builder for the services loader.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Services_Service_Container_Builder {
|
||||
|
||||
/**
|
||||
* Service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var Service_Container
|
||||
*/
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->container = new Service_Container();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Service_Container Service container for the plugin.
|
||||
*/
|
||||
public function get(): Service_Container {
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the plugin environment service for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $main_file Absolute path to the plugin main file.
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_env( string $main_file ): self {
|
||||
$this->container['plugin_env'] = function () use ( $main_file ) {
|
||||
return new Plugin_Env( $main_file, ATFPP_V );
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return self The builder instance, for chaining.
|
||||
*/
|
||||
public function build_services(): self {
|
||||
$this->build_general_services();
|
||||
$this->build_capability_services();
|
||||
$this->build_http_services();
|
||||
$this->build_option_services();
|
||||
$this->build_entity_services();
|
||||
$this->build_admin_services();
|
||||
|
||||
$this->container['api'] = static function ( $cont ) {
|
||||
return new Services_API(
|
||||
$cont['current_user'],
|
||||
$cont['http'],
|
||||
$cont['option_container'],
|
||||
$cont['option_repository'],
|
||||
$cont['option_encrypter']
|
||||
);
|
||||
};
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the general services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_general_services(): void {
|
||||
$this->container['current_user'] = static function () {
|
||||
return new Current_User();
|
||||
};
|
||||
$this->container['site_env'] = static function () {
|
||||
return new Site_Env();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the capability services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_capability_services(): void {
|
||||
$this->container['capability_container'] = static function () {
|
||||
$capabilities = new Capability_Container();
|
||||
$capabilities['ais_manage_services'] = static function () {
|
||||
return new Base_Capability(
|
||||
'ais_manage_services',
|
||||
array( 'manage_options' )
|
||||
);
|
||||
};
|
||||
$capabilities['ais_access_services'] = static function () {
|
||||
return new Base_Capability(
|
||||
'ais_access_services',
|
||||
array( 'edit_posts' )
|
||||
);
|
||||
};
|
||||
$capabilities['ais_access_service'] = static function () {
|
||||
return new Meta_Capability(
|
||||
'ais_access_service',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
|
||||
static function ( int $user_id, string $service_slug ) {
|
||||
return array( 'ais_access_services' );
|
||||
}
|
||||
);
|
||||
};
|
||||
$capabilities['ais_use_playground'] = static function () {
|
||||
return new Meta_Capability(
|
||||
'ais_use_playground',
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
|
||||
static function ( int $user_id ) {
|
||||
return array( 'ais_access_services' );
|
||||
}
|
||||
);
|
||||
};
|
||||
return $capabilities;
|
||||
};
|
||||
|
||||
$this->container['capability_controller'] = static function ( $cont ) {
|
||||
return new Capability_Controller( $cont['capability_container'] );
|
||||
};
|
||||
$this->container['capability_filters'] = static function ( $cont ) {
|
||||
return new Capability_Filters( $cont['capability_container'] );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the HTTP services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_http_services(): void {
|
||||
$this->container['http'] = static function () {
|
||||
// Custom implementation with additional support for streaming responses.
|
||||
return new HTTP_With_Streams();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the option services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_option_services(): void {
|
||||
$this->container['option_repository'] = static function () {
|
||||
return new Option_Repository();
|
||||
};
|
||||
$this->container['option_container'] = static function () {
|
||||
return new Option_Container();
|
||||
};
|
||||
$this->container['option_registry'] = static function () {
|
||||
return new Option_Registry( 'ais_services' );
|
||||
};
|
||||
$this->container['option_encrypter'] = static function () {
|
||||
return new Option_Encrypter( new Data_Encryption() );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the entity services for the service container.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
private function build_entity_services(): void {
|
||||
$this->container['user_meta_repository'] = static function () {
|
||||
return new Meta_Repository( 'user' );
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the admin services for the service container.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
private function build_admin_services(): void {
|
||||
$this->container['admin_settings_menu'] = static function () {
|
||||
return new Admin_Menu( 'options-general.php' );
|
||||
};
|
||||
$this->container['admin_tools_menu'] = static function () {
|
||||
return new Admin_Menu( 'tools.php' );
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Generative_AI_API_Client_Trait
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\Stream_Request_Handler;
|
||||
use ATFPP\AI_Translate\Services\HTTP\Contracts\With_Stream;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Response;
|
||||
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Exception\Request_Exception;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for an API client class which implements the Generative_AI_API_Client interface.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
trait Generative_AI_API_Client_Trait {
|
||||
|
||||
/**
|
||||
* Sends the given request to the API and returns the response data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Request $request The request instance.
|
||||
* @return Response The response instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while making the request.
|
||||
*/
|
||||
final public function make_request( Request $request ): Response {
|
||||
$request_handler = $this->get_request_handler();
|
||||
|
||||
$options = $request->get_options();
|
||||
if ( isset( $options['stream'] ) && $options['stream'] ) {
|
||||
if ( ! $request_handler instanceof Stream_Request_Handler ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'Streaming requests are not supported by this API client.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $request_handler->request_stream( $request );
|
||||
} catch ( Request_Exception $e ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $e->getMessage() );
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$response = $request_handler->request( $request );
|
||||
} catch ( Request_Exception $e ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $response->get_status() < 200 || $response->get_status() >= 300 ) {
|
||||
$data = $response->get_data();
|
||||
if ( $data && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
|
||||
$error_message = $data['error']['message'];
|
||||
} elseif ( $data && isset( $data['error'] ) && is_string( $data['error'] ) ) {
|
||||
$error_message = $data['error'];
|
||||
} elseif ( $data && isset( $data['message'] ) && is_string( $data['message'] ) ) {
|
||||
$error_message = $data['message'];
|
||||
} else {
|
||||
$error_message = sprintf(
|
||||
'Bad status code: %d',
|
||||
$response->get_status()
|
||||
);
|
||||
}
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
throw $this->create_request_exception( $error_message );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response data from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data as associative array and should return the processed data in the desired
|
||||
* format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
final public function process_response_data( Response $response, $process_callback ) {
|
||||
if ( $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response must not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data = $response->get_data();
|
||||
if ( ! $data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data received in response.'
|
||||
);
|
||||
}
|
||||
|
||||
$processed_data = call_user_func( $process_callback, $data );
|
||||
if ( ! $processed_data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data returned by process callback.'
|
||||
);
|
||||
}
|
||||
|
||||
return $processed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response body from the API.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Response $response The response instance. Must not be a stream response, i.e. not implement the
|
||||
* With_Stream interface.
|
||||
* @param callable $process_callback The callback to process the response body. Receives the response body as
|
||||
* string and should return the processed data in the desired format.
|
||||
* @return mixed The processed response data.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response body.
|
||||
*/
|
||||
final public function process_response_body( Response $response, $process_callback ) {
|
||||
if ( $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response must not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = $response->get_body();
|
||||
if ( ! $body ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No body received in response.'
|
||||
);
|
||||
}
|
||||
|
||||
$processed_data = call_user_func( $process_callback, $body );
|
||||
if ( ! $processed_data ) {
|
||||
throw new Generative_AI_Exception(
|
||||
'No data returned by process callback.'
|
||||
);
|
||||
}
|
||||
|
||||
return $processed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response data stream from the API.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Response $response The response instance. Must implement With_Stream. The response data will
|
||||
* be processed in chunks, with each chunk of data being passed to the process
|
||||
* callback.
|
||||
* @param callable $process_callback The callback to process the response data. Receives the JSON-decoded response
|
||||
* data (associative array) as first parameter, and the previous processed data
|
||||
* as second parameter (or null in case this is the first chunk). It should
|
||||
* return the processed data for the chunk in the desired format.
|
||||
* @return Generator Generator that yields the individual processed response data chunks.
|
||||
*
|
||||
* @throws Generative_AI_Exception If an error occurs while processing the response data.
|
||||
*/
|
||||
final public function process_response_stream( Response $response, $process_callback ): Generator {
|
||||
if ( ! $response instanceof With_Stream ) {
|
||||
throw new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Response does not implement %s.',
|
||||
With_Stream::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$stream_generator = $response->read_stream();
|
||||
|
||||
$previous_processed_data = null;
|
||||
foreach ( $stream_generator as $data ) {
|
||||
$processed_data = call_user_func( $process_callback, $data, $previous_processed_data );
|
||||
if ( ! $processed_data ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$previous_processed_data = $processed_data;
|
||||
yield $processed_data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for a bad request, i.e. invalid or unsupported request data.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return InvalidArgumentException The exception instance.
|
||||
*/
|
||||
final public function create_bad_request_exception( string $message ): InvalidArgumentException {
|
||||
return new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid request data for the %1$s API: %2$s',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API request error.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.3.0 Method made public.
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_request_exception( string $message ): Generative_AI_Exception {
|
||||
return new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Error while making request to the %1$s API: %2$s ',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $message The error message to include in the exception.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_response_exception( string $message ): Generative_AI_Exception {
|
||||
return new Generative_AI_Exception(
|
||||
sprintf(
|
||||
'Error in the response from the %1$s API: %2$s ',
|
||||
$this->get_api_name(),
|
||||
$message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new exception for an AI API response error for a missing key.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string $key The missing key in the response data.
|
||||
* @return Generative_AI_Exception The exception instance.
|
||||
*/
|
||||
final public function create_missing_response_key_exception( string $key ): Generative_AI_Exception {
|
||||
return $this->create_response_exception(
|
||||
sprintf(
|
||||
'The response is missing the "%s" key.',
|
||||
$key
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request handler instance to use for requests.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.6.0 Renamed from `get_http()`.
|
||||
*
|
||||
* @return Request_Handler The request handler instance.
|
||||
*/
|
||||
abstract protected function get_request_handler(): Request_Handler;
|
||||
|
||||
/**
|
||||
* Returns the human readable API name (without the "API" suffix).
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string The API name.
|
||||
*/
|
||||
abstract protected function get_api_name(): string;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_System_Instruction_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses a system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_System_Instruction_Trait {
|
||||
|
||||
/**
|
||||
* The system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Content|null
|
||||
*/
|
||||
private $system_instruction;
|
||||
|
||||
/**
|
||||
* Gets the system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Content|null The system instruction, or null if not set.
|
||||
*/
|
||||
final protected function get_system_instruction(): ?Content {
|
||||
return $this->system_instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system instruction.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content $system_instruction The system instruction.
|
||||
*/
|
||||
final protected function set_system_instruction( Content $system_instruction ): void {
|
||||
$this->system_instruction = $system_instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system instruction if provided in the `systemInstruction` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `systemInstruction` model parameter is invalid.
|
||||
*/
|
||||
protected function set_system_instruction_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['systemInstruction'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$model_params['systemInstruction'] = Formatter::format_system_instruction( $model_params['systemInstruction'] );
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid systemInstruction model parameter: %s',
|
||||
htmlspecialchars( $e->getMessage() ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_system_instruction( $model_params['systemInstruction'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Text_Generation_Config_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Text_Generation_Config`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Text_Generation_Config_Trait {
|
||||
|
||||
/**
|
||||
* The text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Text_Generation_Config|null
|
||||
*/
|
||||
private $text_generation_config;
|
||||
|
||||
/**
|
||||
* Gets the text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Text_Generation_Config|null The text generation configuration, or null if not set.
|
||||
*/
|
||||
final protected function get_text_generation_config(): ?Text_Generation_Config {
|
||||
return $this->text_generation_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text generation configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Text_Generation_Config $text_generation_config The text generation configuration.
|
||||
*/
|
||||
final protected function set_text_generation_config( Text_Generation_Config $text_generation_config ): void {
|
||||
$this->text_generation_config = $text_generation_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text generation configuration if provided in the `generationConfig` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `generationConfig` model parameter is invalid.
|
||||
*/
|
||||
protected function set_text_generation_config_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['generationConfig'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['generationConfig'] ) ) {
|
||||
$model_params['generationConfig'] = Text_Generation_Config::from_array( $model_params['generationConfig'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['generationConfig'] instanceof Text_Generation_Config ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid generationConfig model parameter: The value must be an array or an instance of %s.',
|
||||
Text_Generation_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_text_generation_config( $model_params['generationConfig'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Tool_Config_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Tool_Config`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Tool_Config_Trait {
|
||||
|
||||
/**
|
||||
* The tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Tool_Config|null
|
||||
*/
|
||||
private $tool_config;
|
||||
|
||||
/**
|
||||
* Gets the tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Tool_Config|null The tool configuration, or null if not set.
|
||||
*/
|
||||
final protected function get_tool_config(): ?Tool_Config {
|
||||
return $this->tool_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tool configuration.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tool_Config $tool_config The tool configuration.
|
||||
*/
|
||||
final protected function set_tool_config( Tool_Config $tool_config ): void {
|
||||
$this->tool_config = $tool_config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tool configuration if provided in the `toolConfig` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `toolConfig` model parameter is invalid.
|
||||
*/
|
||||
protected function set_tool_config_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['toolConfig'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['toolConfig'] ) ) {
|
||||
$model_params['toolConfig'] = Tool_Config::from_array( $model_params['toolConfig'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['toolConfig'] instanceof Tool_Config ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid toolConfig model parameter: The value must be an array or an instance of %s.',
|
||||
Tool_Config::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_tool_config( $model_params['toolConfig'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\Model_Param_Tools_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model that uses `Tools`.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait Model_Param_Tools_Trait {
|
||||
|
||||
/**
|
||||
* The tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Tools|null
|
||||
*/
|
||||
private $tools;
|
||||
|
||||
/**
|
||||
* Gets the tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Tools|null The tools instance, or null if not set.
|
||||
*/
|
||||
final protected function get_tools(): ?Tools {
|
||||
return $this->tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tools instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tools $tools The tools instance.
|
||||
*/
|
||||
final protected function set_tools( Tools $tools ): void {
|
||||
$this->tools = $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tools instance if provided in the `tools` model parameter.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $model_params The model parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the `tools` model parameter is invalid.
|
||||
*/
|
||||
protected function set_tools_from_model_params( array $model_params ): void {
|
||||
if ( ! isset( $model_params['tools'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_array( $model_params['tools'] ) ) {
|
||||
$model_params['tools'] = Tools::from_array( $model_params['tools'] );
|
||||
}
|
||||
|
||||
if ( ! $model_params['tools'] instanceof Tools ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Invalid tools model parameter: The value must be an array or an instance of %s.',
|
||||
Tools::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->set_tools( $model_params['tools'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Call_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Function_Response_Part;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tool_Config;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Tools\Function_Declarations_Tool;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for an OpenAI compatible text generation model which implements function calling.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait {
|
||||
use Model_Param_Tool_Config_Trait;
|
||||
use Model_Param_Tools_Trait;
|
||||
|
||||
/**
|
||||
* Prepares the API request parameters for generating text content.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Content[] $contents The contents to generate text for.
|
||||
* @return array<string, mixed> The parameters for generating text content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if an invalid tool is provided.
|
||||
*/
|
||||
protected function prepare_generate_text_params( array $contents ): array {
|
||||
$params = parent::prepare_generate_text_params( $contents );
|
||||
|
||||
if ( $this->get_tools() ) {
|
||||
foreach ( $this->get_tools() as $tool ) {
|
||||
$prepared = $this->prepare_tool( $params, $tool );
|
||||
if ( ! $prepared ) {
|
||||
throw $this->get_api_client()->create_bad_request_exception(
|
||||
'Only function declarations tools are supported.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->get_tool_config() ) {
|
||||
$params['tool_choice'] = $this->prepare_tool_choice_param( $this->get_tool_config() );
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given candidate from the API response into a Parts instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $candidate_data The API response candidate data.
|
||||
* @return Parts The Parts instance.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the response is invalid.
|
||||
*/
|
||||
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
|
||||
$parts = parent::prepare_response_candidate_content_parts( $candidate_data );
|
||||
|
||||
if ( isset( $candidate_data['message']['tool_calls'] ) && is_array( $candidate_data['message']['tool_calls'] ) ) {
|
||||
foreach ( $candidate_data['message']['tool_calls'] as $tool_call ) {
|
||||
$prepared = $this->prepare_response_message_tool_call( $parts, $tool_call );
|
||||
if ( ! $prepared ) {
|
||||
throw $this->get_api_client()->create_response_exception(
|
||||
'The response includes a tool call of an unexpected type.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a given tool call from the response message, amending the provided Parts instance as needed.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Parts $parts The Parts instance to amend.
|
||||
* @param array<string, mixed> $tool_call_data The tool call data from the response message.
|
||||
* @return bool True if the tool call was successfully prepared, false otherwise.
|
||||
*/
|
||||
protected function prepare_response_message_tool_call( Parts $parts, array $tool_call_data ): bool {
|
||||
// Not all OpenAI compatible APIs include a 'type' key, so we only check its value if it is set.
|
||||
if (
|
||||
( isset( $tool_call_data['type'] ) && 'function' !== $tool_call_data['type'] ) ||
|
||||
! isset( $tool_call_data['function'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts->add_function_call_part(
|
||||
$tool_call_data['id'],
|
||||
$tool_call_data['function']['name'],
|
||||
is_string( $tool_call_data['function']['arguments'] )
|
||||
? json_decode( $tool_call_data['function']['arguments'], true )
|
||||
: $tool_call_data['function']['arguments']
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a single tool for the API request, amending the provided parameters as needed.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters to prepare the tools for. Passed by reference.
|
||||
* @param Tool $tool The tool to prepare.
|
||||
* @return bool True if the tool was successfully prepared, false otherwise.
|
||||
*/
|
||||
protected function prepare_tool( array &$params, Tool $tool ): bool {
|
||||
if ( ! $tool instanceof Function_Declarations_Tool ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$function_declarations = $tool->get_function_declarations();
|
||||
|
||||
if ( count( $function_declarations ) > 0 ) {
|
||||
if ( ! isset( $params['tools'] ) ) {
|
||||
$params['tools'] = array();
|
||||
}
|
||||
foreach ( $function_declarations as $declaration ) {
|
||||
$params['tools'][] = array(
|
||||
'type' => 'function',
|
||||
'function' => array_filter(
|
||||
array(
|
||||
'name' => $declaration['name'],
|
||||
'description' => $declaration['description'] ?? null,
|
||||
'parameters' => $declaration['parameters'] ?? null,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the API request tool choice parameter for the model.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Tool_Config $tool_config The tool config to prepare the parameter with.
|
||||
* @return array<string, mixed> The tool config parameter value.
|
||||
*/
|
||||
private function prepare_tool_choice_param( Tool_Config $tool_config ): array {
|
||||
// Either 'auto' or 'any'.
|
||||
$tool_choice_param = $tool_config->get_function_call_mode() === 'any' ? 'required' : 'auto';
|
||||
|
||||
if ( 'required' === $tool_choice_param ) {
|
||||
// If one specific function must be called, the parameter needs to be an object, otherwise a string.
|
||||
$allowed_function_names = $tool_config->get_allowed_function_names();
|
||||
if ( count( $allowed_function_names ) === 1 ) {
|
||||
$tool_choice_param = array(
|
||||
'type' => 'function',
|
||||
'function' => array( 'name' => $allowed_function_names[0] ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tool_choice_param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content transformers.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return array<string, callable> The content transformers.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
||||
*/
|
||||
protected function get_content_transformers(): array {
|
||||
$api_client = $this->get_api_client();
|
||||
|
||||
$transformers = parent::get_content_transformers();
|
||||
|
||||
$orig_role_transformer = $transformers['role'];
|
||||
$orig_content_transformer = $transformers['content'];
|
||||
|
||||
$transformers['role'] = static function ( Content $content ) use ( $orig_role_transformer ) {
|
||||
// Special case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
return 'tool';
|
||||
}
|
||||
|
||||
return $orig_role_transformer( $content );
|
||||
};
|
||||
|
||||
$transformers['content'] = static function ( Content $content ) use ( $orig_content_transformer, $api_client ) {
|
||||
// Special case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
$response = $parts->get( 0 )->get_response();
|
||||
return json_encode( $response ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
}
|
||||
|
||||
$sanitized_parts = new Parts();
|
||||
foreach ( $parts as $part ) {
|
||||
/*
|
||||
* Special cases: Function call parts are handled as part of a separate `tool_calls` key, and
|
||||
* function response parts are are only supported as the only content of a message. They are
|
||||
* handled as a special case above.
|
||||
*/
|
||||
if ( $part instanceof Function_Response_Part ) {
|
||||
throw $api_client->create_bad_request_exception(
|
||||
'The API only allows a single function response, and it has to be the only content of the message.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $part instanceof Function_Call_Part ) {
|
||||
// Skip function call parts, they are handled in a separate `tool_calls` key.
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized_parts->add_part( $part );
|
||||
}
|
||||
$sanitized_content = new Content( $content->get_role(), $sanitized_parts );
|
||||
|
||||
return $orig_content_transformer( $sanitized_content );
|
||||
};
|
||||
|
||||
$transformers['tool_calls'] = static function ( Content $content ) {
|
||||
// Special key that only applies in case function calls are present.
|
||||
$tool_calls = array();
|
||||
foreach ( $content->get_parts() as $part ) {
|
||||
if ( $part instanceof Function_Call_Part ) {
|
||||
$tool_calls[] = array(
|
||||
'type' => 'function',
|
||||
'id' => $part->get_id(),
|
||||
'function' => array(
|
||||
'name' => $part->get_name(),
|
||||
'arguments' => json_encode( $part->get_args() ), // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if ( count( $tool_calls ) > 0 ) {
|
||||
return $tool_calls;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$transformers['tool_call_id'] = static function ( Content $content ) {
|
||||
// Special key that only applies in case of a function response.
|
||||
$parts = $content->get_parts();
|
||||
if ( count( $parts ) === 1 && $parts->get( 0 ) instanceof Function_Response_Part ) {
|
||||
return $parts->get( 0 )->get_id();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return $transformers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Trait for a service or model which implements the With_API_Client interface.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*/
|
||||
trait With_API_Client_Trait {
|
||||
|
||||
/**
|
||||
* The AI API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
* @var Generative_AI_API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* Gets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @return Generative_AI_API_Client The API client instance.
|
||||
*
|
||||
* @throws RuntimeException Thrown if the API client is not set.
|
||||
*/
|
||||
final public function get_api_client(): Generative_AI_API_Client {
|
||||
if ( ! $this->api_client instanceof Generative_AI_API_Client ) {
|
||||
throw new RuntimeException( 'API client must be set in the constructor.' );
|
||||
}
|
||||
|
||||
return $this->api_client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API client instance.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param Generative_AI_API_Client $api_client The API client instance.
|
||||
*/
|
||||
final protected function set_api_client( Generative_AI_API_Client $api_client ): void {
|
||||
$this->api_client = $api_client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait ATFPP\AI_Translate\Services\Traits\With_Text_Generation_Trait
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Traits;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Candidates;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
|
||||
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
|
||||
use ATFPP\AI_Translate\Services\Util\Formatter;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Trait for a model which implements the With_Text_Generation interface.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
trait With_Text_Generation_Trait {
|
||||
|
||||
/**
|
||||
* Generates text content using the model.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final public function generate_text( $content, array $request_options = array() ): Candidates {
|
||||
$contents = $this->sanitize_new_content( $content );
|
||||
return $this->send_generate_text_request( $contents, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates text content using the model, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content Prompt for the content to generate. Optionally, an array
|
||||
* can be passed for additional context (e.g. chat history).
|
||||
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the given content is invalid.
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
final public function stream_generate_text( $content, array $request_options = array() ): Generator {
|
||||
$contents = $this->sanitize_new_content( $content );
|
||||
return $this->send_stream_generate_text_request( $contents, $request_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the input content for generating text.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content The input content.
|
||||
* @return Content[] The sanitized content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the input content is invalid.
|
||||
*/
|
||||
private function sanitize_new_content( $content ) {
|
||||
$capabilities = AI_Capabilities::get_model_instance_capabilities( $this );
|
||||
return Formatter::format_and_validate_new_contents( $content, $capabilities );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Candidates The response candidates with generated text content - usually just one.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
abstract protected function send_generate_text_request( array $contents, array $request_options ): Candidates;
|
||||
|
||||
/**
|
||||
* Sends a request to generate text content, streaming the response.
|
||||
*
|
||||
* @since 0.3.0
|
||||
*
|
||||
* @param Content[] $contents Prompts for the content to generate.
|
||||
* @param array<string, mixed> $request_options The request options.
|
||||
* @return Generator<Candidates> Generator that yields the chunks of response candidates with generated text
|
||||
* content - usually just one candidate.
|
||||
*
|
||||
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
|
||||
*/
|
||||
abstract protected function send_stream_generate_text_request( array $contents, array $request_options ): Generator;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\AI_Capabilities
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Text_Generation;
|
||||
use ATFPP\AI_Translate\Services\Contracts\With_Web_Search;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class exposing the available AI capabilities and related static utility methods.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class AI_Capabilities {
|
||||
|
||||
/**
|
||||
* Gets the combined AI capabilities that the given model classes support.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_classes The model class names.
|
||||
* @return string[] The AI capabilities that the model classes support, based on the interfaces they implement.
|
||||
*/
|
||||
public static function get_model_classes_capabilities( array $model_classes ): array {
|
||||
$capabilities = array();
|
||||
foreach ( $model_classes as $model_class ) {
|
||||
$model_capabilities = self::get_model_class_capabilities( $model_class );
|
||||
foreach ( $model_capabilities as $capability ) {
|
||||
$capabilities[] = $capability;
|
||||
}
|
||||
}
|
||||
return array_unique( $capabilities );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AI capabilities that the given model class supports.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $model_class The model class name.
|
||||
* @return string[] The AI capabilities that the model class supports, based on the interfaces it implements.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
public static function get_model_class_capabilities( string $model_class ): array {
|
||||
$interfaces = class_implements( $model_class );
|
||||
|
||||
$capabilities = array();
|
||||
if ( isset( $interfaces[ With_Function_Calling::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::FUNCTION_CALLING;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Multimodal_Input::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Multimodal_Output::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Text_Generation::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::TEXT_GENERATION;
|
||||
}
|
||||
if ( isset( $interfaces[ With_Web_Search::class ] ) ) {
|
||||
$capabilities[] = AI_Capability::WEB_SEARCH;
|
||||
}
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AI capabilities that the given model instance supports.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param Generative_AI_Model $model The model instance.
|
||||
* @return string[] The AI capabilities that the model instance supports, based on the interfaces it implements.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*/
|
||||
public static function get_model_instance_capabilities( Generative_AI_Model $model ): array {
|
||||
$capabilities = array();
|
||||
if ( $model instanceof With_Function_Calling ) {
|
||||
$capabilities[] = AI_Capability::FUNCTION_CALLING;
|
||||
}
|
||||
if ( $model instanceof With_Multimodal_Input ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_INPUT;
|
||||
}
|
||||
if ( $model instanceof With_Multimodal_Output ) {
|
||||
$capabilities[] = AI_Capability::MULTIMODAL_OUTPUT;
|
||||
}
|
||||
if ( $model instanceof With_Text_Generation ) {
|
||||
$capabilities[] = AI_Capability::TEXT_GENERATION;
|
||||
}
|
||||
if ( $model instanceof With_Web_Search ) {
|
||||
$capabilities[] = AI_Capability::WEB_SEARCH;
|
||||
}
|
||||
return $capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model slugs that satisfy the given capabilities.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @since 0.5.0 Now expects an array of model data shapes, mapped by model slug.
|
||||
* @since 0.7.0 Now expects a map of model metadata objects.
|
||||
*
|
||||
* @param array<string, Model_Metadata> $models Metadata for each model, mapped by model slug.
|
||||
* @param string[] $capabilities The required capabilities that the models should satisfy.
|
||||
* @return string[] Slugs of all models that satisfy the given capabilities.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
|
||||
*/
|
||||
public static function get_model_slugs_for_capabilities( array $models, array $capabilities ): array {
|
||||
$model_slugs = array();
|
||||
foreach ( $models as $model_slug => $model_metadata ) {
|
||||
$model_capabilities = $model_metadata->get_capabilities();
|
||||
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
|
||||
$model_slugs[] = $model_slug;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $model_slugs ) {
|
||||
throw new InvalidArgumentException(
|
||||
'No model satisfies the given capabilities.'
|
||||
);
|
||||
}
|
||||
|
||||
return $model_slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the model class name from the given model class names that satisfies the given capabilities.
|
||||
*
|
||||
* @since 0.7.0
|
||||
*
|
||||
* @param string[] $model_classes The model class names.
|
||||
* @param string[] $capabilities The required capabilities that the models should satisfy.
|
||||
* @return string The model class name that satisfies the given capabilities.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if no model satisfies the given capabilities.
|
||||
*/
|
||||
public static function get_model_class_for_capabilities( array $model_classes, array $capabilities ): string {
|
||||
foreach ( $model_classes as $model_class ) {
|
||||
$model_capabilities = self::get_model_class_capabilities( $model_class );
|
||||
if ( ! array_diff( $capabilities, $model_capabilities ) ) {
|
||||
return $model_class;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
'No model class satisfies the given capabilities.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Data_Encryption
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
/**
|
||||
* Class responsible for encrypting and decrypting data.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @see https://felix-arntz.me/blog/storing-confidential-data-in-wordpress/
|
||||
*/
|
||||
final class Data_Encryption {
|
||||
|
||||
/**
|
||||
* Key to use for encryption.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* Salt to use for encryption.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private $salt;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param ?string $key Optional. Key to use for encryption. If not passed, the default key determined by constants
|
||||
* will be used.
|
||||
* @param ?string $salt Optional. Salt to use for encryption. If not passed, the default salt determined by
|
||||
* constants will be used.
|
||||
*/
|
||||
public function __construct( ?string $key = null, ?string $salt = null ) {
|
||||
$this->key = $key ?? $this->get_default_key();
|
||||
$this->salt = $salt ?? $this->get_default_salt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a value.
|
||||
*
|
||||
* If a user-based key is set, that key is used. Otherwise the default key is used.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $value Value to encrypt.
|
||||
* @return string Encrypted value, or empty string on failure.
|
||||
*/
|
||||
public function encrypt( string $value ): string {
|
||||
if ( ! extension_loaded( 'openssl' ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$method = 'aes-256-ctr';
|
||||
$ivlen = openssl_cipher_iv_length( $method );
|
||||
$iv = openssl_random_pseudo_bytes( $ivlen );
|
||||
|
||||
$raw_value = openssl_encrypt( $value . $this->salt, $method, $this->key, 0, $iv );
|
||||
if ( ! $raw_value ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return base64_encode( $iv . $raw_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a value.
|
||||
*
|
||||
* If a user-based key is set, that key is used. Otherwise the default key is used.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string $raw_value Value to decrypt.
|
||||
* @return string Decrypted value, or empty string on failure.
|
||||
*/
|
||||
public function decrypt( string $raw_value ): string {
|
||||
if ( ! extension_loaded( 'openssl' ) ) {
|
||||
return $raw_value;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
$decoded_value = base64_decode( $raw_value, true );
|
||||
|
||||
if ( false === $decoded_value ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$method = 'aes-256-ctr';
|
||||
$ivlen = openssl_cipher_iv_length( $method );
|
||||
$iv = substr( $decoded_value, 0, $ivlen );
|
||||
|
||||
$decoded_value = substr( $decoded_value, $ivlen );
|
||||
|
||||
$value = openssl_decrypt( $decoded_value, $method, $this->key, 0, $iv );
|
||||
if ( ! $value || substr( $value, - strlen( $this->salt ) ) !== $this->salt ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr( $value, 0, - strlen( $this->salt ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default encryption key to use.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string Default (not user-based) encryption key.
|
||||
*/
|
||||
private function get_default_key(): string {
|
||||
if ( defined( 'AI_SERVICES_ENCRYPTION_KEY' ) && '' !== AI_SERVICES_ENCRYPTION_KEY ) {
|
||||
return AI_SERVICES_ENCRYPTION_KEY;
|
||||
}
|
||||
|
||||
if ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) {
|
||||
return LOGGED_IN_KEY;
|
||||
}
|
||||
|
||||
// If this is reached, you're either not on a live site or have a serious security issue.
|
||||
return 'test-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default encryption salt to use.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return string Encryption salt.
|
||||
*/
|
||||
private function get_default_salt(): string {
|
||||
if ( defined( 'AI_SERVICES_ENCRYPTION_SALT' ) && '' !== AI_SERVICES_ENCRYPTION_SALT ) {
|
||||
return AI_SERVICES_ENCRYPTION_SALT;
|
||||
}
|
||||
|
||||
if ( defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT ) {
|
||||
return LOGGED_IN_SALT;
|
||||
}
|
||||
|
||||
// If this is reached, you're either not on a live site or have a serious security issue.
|
||||
return 'test-salt';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Formatter
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
|
||||
use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts;
|
||||
use ATFPP\AI_Translate\Services\API\Types\Parts\Text_Part;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static methods for formatting content.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
final class Formatter {
|
||||
|
||||
/**
|
||||
* Formats and validates the various supported formats of a user prompt into a consistent list of Content instances.
|
||||
*
|
||||
* This method takes into account whether the provided content is supported by the given model, based on its capabilities.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*
|
||||
* @param string|Parts|Content|Content[] $content The content to format.
|
||||
* @param string[] $capabilities The AI capabilities that the model supports.
|
||||
* @return Content[] The formatted Content instances.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the content is invalid or the model does not support it.
|
||||
*/
|
||||
public static function format_and_validate_new_contents( $content, array $capabilities ): array {
|
||||
if ( is_array( $content ) ) {
|
||||
$contents = array_map(
|
||||
array( __CLASS__, 'format_new_content' ),
|
||||
$content
|
||||
);
|
||||
} else {
|
||||
$contents = array( self::format_new_content( $content ) );
|
||||
}
|
||||
|
||||
if ( count( $contents ) === 0 ) {
|
||||
throw new InvalidArgumentException(
|
||||
'No prompt was provided.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( Content_Role::USER !== $contents[0]->get_role() ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The first Content instance in the conversation or prompt must be user content.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! in_array( AI_Capability::CHAT_HISTORY, $capabilities, true ) && count( $contents ) > 1 ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The model does not support chat history. Only one content prompt must be provided.'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! in_array( AI_Capability::MULTIMODAL_INPUT, $capabilities, true ) ) {
|
||||
// For performance reasons, only check the last content prompt, which likely is the only new one.
|
||||
$last_content = $contents[ count( $contents ) - 1 ];
|
||||
$last_parts = $last_content->get_parts();
|
||||
$last_parts_text_only = $last_parts->filter( array( 'class_name' => Text_Part::class ) );
|
||||
if ( count( $last_parts_text_only ) < count( $last_parts ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The model does not support multimodal input. Only text parts must be provided.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of new user content into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $content The content to format.
|
||||
* @return Content The formatted new content.
|
||||
*/
|
||||
public static function format_new_content( $content ): Content {
|
||||
return self::format_content( $content, Content_Role::USER );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of a system instruction into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $input The system instruction to format.
|
||||
* @return Content The formatted system instruction.
|
||||
*/
|
||||
public static function format_system_instruction( $input ): Content {
|
||||
return self::format_content( $input, Content_Role::SYSTEM );
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the various supported formats of content into a consistent Content instance.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|Parts|Content $input The content to format.
|
||||
* @param string $role The role for the content.
|
||||
* @return Content The formatted content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if the value is not a string, a Parts instance, or a Content instance.
|
||||
*/
|
||||
public static function format_content( $input, string $role ): Content {
|
||||
if ( is_string( $input ) ) {
|
||||
$parts = new Parts();
|
||||
$parts->add_text_part( $input );
|
||||
|
||||
return new Content( $role, $parts );
|
||||
}
|
||||
|
||||
if ( $input instanceof Parts ) {
|
||||
return new Content( $role, $input );
|
||||
}
|
||||
|
||||
if ( ! $input instanceof Content ) {
|
||||
throw new InvalidArgumentException(
|
||||
'The value must be a string, a Parts instance, or a Content instance.'
|
||||
);
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Strings
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
/**
|
||||
* Class providing static methods for string operations.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Strings {
|
||||
|
||||
/**
|
||||
* Converts a snake_case string to a camelCase string.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param string $input The snake_case string.
|
||||
* @return string The camelCase string.
|
||||
*/
|
||||
public static function snake_case_to_camel_case( string $input ): string {
|
||||
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ATFPP\AI_Translate\Services\Util\Transformer
|
||||
*
|
||||
* @since 0.2.0
|
||||
* @package ai-services
|
||||
*/
|
||||
|
||||
namespace ATFPP\AI_Translate\Services\Util;
|
||||
|
||||
use ATFPP\AI_Translate\Services\API\Types\Content;
|
||||
use ATFPP\AI_Translate\Services\Contracts\Generation_Config;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class providing static methods for transforming data.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*/
|
||||
final class Transformer {
|
||||
|
||||
/**
|
||||
* Transforms the given content using the provided transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param Content $content The content to transform.
|
||||
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
|
||||
* the content as its only parameter and return the transformed value
|
||||
* for its key.
|
||||
* @return array<string, mixed> The transformed content.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
|
||||
*/
|
||||
public static function transform_content( Content $content, array $transformers ): array {
|
||||
$data = array();
|
||||
|
||||
foreach ( $transformers as $key => $transformer ) {
|
||||
if ( ! is_callable( $transformer ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The transformer for key %s is invalid.',
|
||||
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Transform the value and set it if truthy.
|
||||
$value = $transformer( $content );
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
$data[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the given Generation_Config instance into the given parameters using the provided transformers.
|
||||
*
|
||||
* @since 0.2.0
|
||||
*
|
||||
* @param array<string, mixed> $params The parameters to merge the generation config into.
|
||||
* @param Generation_Config $config The generation config to use for the transformation.
|
||||
* @param array<string, callable> $transformers The transformers to use. Each transformer callback should accept
|
||||
* the generation config as its only parameter and return the
|
||||
* transformed value for its key.
|
||||
* @return array<string, mixed> The transformed parameters.
|
||||
*
|
||||
* @throws InvalidArgumentException Thrown if a provided transformer is not callable.
|
||||
*/
|
||||
public static function transform_generation_config_params( array $params, Generation_Config $config, array $transformers ): array {
|
||||
foreach ( $transformers as $key => $transformer ) {
|
||||
if ( ! is_callable( $transformer ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'The transformer for key %s is invalid.',
|
||||
htmlspecialchars( $key ) // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Already set parameters take precedence.
|
||||
if ( isset( $params[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transform the value and set it if truthy.
|
||||
$value = $transformer( $config );
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user