[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Messages/DTO/ -> MessagePart.php (source)

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Messages\DTO;
   5  
   6  use WordPress\AiClient\Common\AbstractDataTransferObject;
   7  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   8  use WordPress\AiClient\Common\Exception\RuntimeException;
   9  use WordPress\AiClient\Files\DTO\File;
  10  use WordPress\AiClient\Messages\Enums\MessagePartChannelEnum;
  11  use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum;
  12  use WordPress\AiClient\Tools\DTO\FunctionCall;
  13  use WordPress\AiClient\Tools\DTO\FunctionResponse;
  14  /**
  15   * Represents a part of a message.
  16   *
  17   * Messages can contain multiple parts of different types, such as text, files,
  18   * function calls, etc. This DTO encapsulates one such part.
  19   *
  20   * @since 0.1.0
  21   *
  22   * @phpstan-import-type FileArrayShape from File
  23   * @phpstan-import-type FunctionCallArrayShape from FunctionCall
  24   * @phpstan-import-type FunctionResponseArrayShape from FunctionResponse
  25   *
  26   * @phpstan-type MessagePartArrayShape array{
  27   *     channel: string,
  28   *     type: string,
  29   *     thoughtSignature?: string,
  30   *     text?: string,
  31   *     file?: FileArrayShape,
  32   *     functionCall?: FunctionCallArrayShape,
  33   *     functionResponse?: FunctionResponseArrayShape
  34   * }
  35   *
  36   * @extends AbstractDataTransferObject<MessagePartArrayShape>
  37   */
  38  class MessagePart extends AbstractDataTransferObject
  39  {
  40      public const KEY_CHANNEL = 'channel';
  41      public const KEY_TYPE = 'type';
  42      public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature';
  43      public const KEY_TEXT = 'text';
  44      public const KEY_FILE = 'file';
  45      public const KEY_FUNCTION_CALL = 'functionCall';
  46      public const KEY_FUNCTION_RESPONSE = 'functionResponse';
  47      /**
  48       * @var MessagePartChannelEnum The channel this message part belongs to.
  49       */
  50      private MessagePartChannelEnum $channel;
  51      /**
  52       * @var MessagePartTypeEnum The type of this message part.
  53       */
  54      private MessagePartTypeEnum $type;
  55      /**
  56       * @var string|null Thought signature for extended thinking.
  57       */
  58      private ?string $thoughtSignature = null;
  59      /**
  60       * @var string|null Text content (when type is TEXT).
  61       */
  62      private ?string $text = null;
  63      /**
  64       * @var File|null File data (when type is FILE).
  65       */
  66      private ?File $file = null;
  67      /**
  68       * @var FunctionCall|null Function call request (when type is FUNCTION_CALL).
  69       */
  70      private ?FunctionCall $functionCall = null;
  71      /**
  72       * @var FunctionResponse|null Function response (when type is FUNCTION_RESPONSE).
  73       */
  74      private ?FunctionResponse $functionResponse = null;
  75      /**
  76       * Constructor that accepts various content types and infers the message part type.
  77       *
  78       * @since 0.1.0
  79       *
  80       * @param mixed $content The content of this message part.
  81       * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT.
  82       * @param string|null $thoughtSignature Optional thought signature for extended thinking.
  83       * @throws InvalidArgumentException If an unsupported content type is provided.
  84       */
  85      public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null)
  86      {
  87          $this->channel = $channel ?? MessagePartChannelEnum::content();
  88          $this->thoughtSignature = $thoughtSignature;
  89          if (is_string($content)) {
  90              $this->type = MessagePartTypeEnum::text();
  91              $this->text = $content;
  92          } elseif ($content instanceof File) {
  93              $this->type = MessagePartTypeEnum::file();
  94              $this->file = $content;
  95          } elseif ($content instanceof FunctionCall) {
  96              $this->type = MessagePartTypeEnum::functionCall();
  97              $this->functionCall = $content;
  98          } elseif ($content instanceof FunctionResponse) {
  99              $this->type = MessagePartTypeEnum::functionResponse();
 100              $this->functionResponse = $content;
 101          } else {
 102              $type = is_object($content) ? get_class($content) : gettype($content);
 103              throw new InvalidArgumentException(sprintf('Unsupported content type %s. Expected string, File, ' . 'FunctionCall, or FunctionResponse.', $type));
 104          }
 105      }
 106      /**
 107       * Gets the channel this message part belongs to.
 108       *
 109       * @since 0.1.0
 110       *
 111       * @return MessagePartChannelEnum The channel.
 112       */
 113      public function getChannel(): MessagePartChannelEnum
 114      {
 115          return $this->channel;
 116      }
 117      /**
 118       * Gets the type of this message part.
 119       *
 120       * @since 0.1.0
 121       *
 122       * @return MessagePartTypeEnum The type.
 123       */
 124      public function getType(): MessagePartTypeEnum
 125      {
 126          return $this->type;
 127      }
 128      /**
 129       * Gets the thought signature.
 130       *
 131       * @since 1.3.0
 132       *
 133       * @return string|null The thought signature or null if not set.
 134       */
 135      public function getThoughtSignature(): ?string
 136      {
 137          return $this->thoughtSignature;
 138      }
 139      /**
 140       * Gets the text content.
 141       *
 142       * @since 0.1.0
 143       *
 144       * @return string|null The text content or null if not a text part.
 145       */
 146      public function getText(): ?string
 147      {
 148          return $this->text;
 149      }
 150      /**
 151       * Gets the file.
 152       *
 153       * @since 0.1.0
 154       *
 155       * @return File|null The file or null if not a file part.
 156       */
 157      public function getFile(): ?File
 158      {
 159          return $this->file;
 160      }
 161      /**
 162       * Gets the function call.
 163       *
 164       * @since 0.1.0
 165       *
 166       * @return FunctionCall|null The function call or null if not a function call part.
 167       */
 168      public function getFunctionCall(): ?FunctionCall
 169      {
 170          return $this->functionCall;
 171      }
 172      /**
 173       * Gets the function response.
 174       *
 175       * @since 0.1.0
 176       *
 177       * @return FunctionResponse|null The function response or null if not a function response part.
 178       */
 179      public function getFunctionResponse(): ?FunctionResponse
 180      {
 181          return $this->functionResponse;
 182      }
 183      /**
 184       * {@inheritDoc}
 185       *
 186       * @since 0.1.0
 187       */
 188      public static function getJsonSchema(): array
 189      {
 190          $channelSchema = ['type' => 'string', 'enum' => MessagePartChannelEnum::getValues(), 'description' => 'The channel this message part belongs to.'];
 191          $thoughtSignatureSchema = ['type' => 'string', 'description' => 'Thought signature for extended thinking.'];
 192          return ['oneOf' => [['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::text()->value], self::KEY_TEXT => ['type' => 'string', 'description' => 'Text content.'], self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_TEXT], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::file()->value], self::KEY_FILE => File::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FILE], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionCall()->value], self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionResponse()->value], self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => \false]]];
 193      }
 194      /**
 195       * {@inheritDoc}
 196       *
 197       * @since 0.1.0
 198       *
 199       * @return MessagePartArrayShape
 200       */
 201      public function toArray(): array
 202      {
 203          $data = [self::KEY_CHANNEL => $this->channel->value, self::KEY_TYPE => $this->type->value];
 204          if ($this->text !== null) {
 205              $data[self::KEY_TEXT] = $this->text;
 206          } elseif ($this->file !== null) {
 207              $data[self::KEY_FILE] = $this->file->toArray();
 208          } elseif ($this->functionCall !== null) {
 209              $data[self::KEY_FUNCTION_CALL] = $this->functionCall->toArray();
 210          } elseif ($this->functionResponse !== null) {
 211              $data[self::KEY_FUNCTION_RESPONSE] = $this->functionResponse->toArray();
 212          } else {
 213              throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.');
 214          }
 215          if ($this->thoughtSignature !== null) {
 216              $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature;
 217          }
 218          return $data;
 219      }
 220      /**
 221       * {@inheritDoc}
 222       *
 223       * This check is purely structural: it does not construct the part or any nested object, so it has no
 224       * side effects (in particular, no filesystem access for file parts).
 225       *
 226       * @since 1.5.1
 227       */
 228      public static function isArrayShape(array $array): bool
 229      {
 230          if (isset($array[self::KEY_CHANNEL])) {
 231              if (!is_string($array[self::KEY_CHANNEL]) || !MessagePartChannelEnum::isValidValue($array[self::KEY_CHANNEL])) {
 232                  return \false;
 233              }
 234          }
 235          if (isset($array[self::KEY_TYPE])) {
 236              if (!is_string($array[self::KEY_TYPE]) || !MessagePartTypeEnum::isValidValue($array[self::KEY_TYPE])) {
 237                  return \false;
 238              }
 239          }
 240          if (isset($array[self::KEY_THOUGHT_SIGNATURE]) && !is_string($array[self::KEY_THOUGHT_SIGNATURE])) {
 241              return \false;
 242          }
 243          $contentKeys = [self::KEY_TEXT, self::KEY_FILE, self::KEY_FUNCTION_CALL, self::KEY_FUNCTION_RESPONSE];
 244          $presentKeys = array_values(array_filter($contentKeys, static function (string $key) use ($array): bool {
 245              return isset($array[$key]);
 246          }));
 247          if (count($presentKeys) !== 1) {
 248              return \false;
 249          }
 250          switch ($presentKeys[0]) {
 251              case self::KEY_TEXT:
 252                  return is_string($array[self::KEY_TEXT]);
 253              case self::KEY_FILE:
 254                  return is_array($array[self::KEY_FILE]) && File::isArrayShape($array[self::KEY_FILE]);
 255              case self::KEY_FUNCTION_CALL:
 256                  return is_array($array[self::KEY_FUNCTION_CALL]) && FunctionCall::isArrayShape($array[self::KEY_FUNCTION_CALL]);
 257              default:
 258                  return is_array($array[self::KEY_FUNCTION_RESPONSE]) && FunctionResponse::isArrayShape($array[self::KEY_FUNCTION_RESPONSE]);
 259          }
 260      }
 261      /**
 262       * {@inheritDoc}
 263       *
 264       * @since 0.1.0
 265       * @since 1.5.1 Values of the wrong type are rejected with an InvalidArgumentException.
 266       */
 267      public static function fromArray(array $array): self
 268      {
 269          if (isset($array[self::KEY_CHANNEL])) {
 270              if (!is_string($array[self::KEY_CHANNEL])) {
 271                  throw new InvalidArgumentException('MessagePart channel must be a string.');
 272              }
 273              $channel = MessagePartChannelEnum::from($array[self::KEY_CHANNEL]);
 274          } else {
 275              $channel = null;
 276          }
 277          $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null;
 278          if ($thoughtSignature !== null && !is_string($thoughtSignature)) {
 279              throw new InvalidArgumentException('MessagePart thoughtSignature must be a string.');
 280          }
 281          // Check which properties are set to determine how to construct the MessagePart
 282          if (isset($array[self::KEY_TEXT])) {
 283              if (!is_string($array[self::KEY_TEXT])) {
 284                  throw new InvalidArgumentException('MessagePart text must be a string.');
 285              }
 286              return new self($array[self::KEY_TEXT], $channel, $thoughtSignature);
 287          } elseif (isset($array[self::KEY_FILE])) {
 288              if (!is_array($array[self::KEY_FILE])) {
 289                  throw new InvalidArgumentException('MessagePart file must be an array.');
 290              }
 291              return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature);
 292          } elseif (isset($array[self::KEY_FUNCTION_CALL])) {
 293              if (!is_array($array[self::KEY_FUNCTION_CALL])) {
 294                  throw new InvalidArgumentException('MessagePart functionCall must be an array.');
 295              }
 296              return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature);
 297          } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) {
 298              if (!is_array($array[self::KEY_FUNCTION_RESPONSE])) {
 299                  throw new InvalidArgumentException('MessagePart functionResponse must be an array.');
 300              }
 301              return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel, $thoughtSignature);
 302          } else {
 303              throw new InvalidArgumentException('MessagePart requires one of: text, file, functionCall, or functionResponse.');
 304          }
 305      }
 306      /**
 307       * Performs a deep clone of the message part.
 308       *
 309       * This method ensures that nested objects (file, function call, function response)
 310       * are cloned to prevent modifications to the cloned part from affecting the original.
 311       *
 312       * @since 0.4.2
 313       */
 314      public function __clone()
 315      {
 316          if ($this->file !== null) {
 317              $this->file = clone $this->file;
 318          }
 319          if ($this->functionCall !== null) {
 320              $this->functionCall = clone $this->functionCall;
 321          }
 322          if ($this->functionResponse !== null) {
 323              $this->functionResponse = clone $this->functionResponse;
 324          }
 325      }
 326  }


Generated : Sun Nov 1 08:20:35 2026 Cross-referenced by PHPXref