Add PSR HTTP Message Interfaces and Dependencies

- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards.
- Added getallheaders function to retrieve HTTP headers in a compatible manner.
- Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts.
- Introduced function for triggering deprecation notices in Symfony.
This commit is contained in:
2025-12-28 12:44:00 +01:00
parent cf600ae727
commit cd264483f8
410 changed files with 60841 additions and 16 deletions

View File

@@ -0,0 +1,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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 tokens logprobs if the token has already been seen in the response.', 'ai-services' ),
'type' => 'number',
),
'frequencyPenalty' => array(
'description' => __( 'Frequency penalty applied to the next tokens 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,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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