[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Messages/DTO/ -> Message.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\Messages\Enums\MessageRoleEnum;
   9  /**
  10   * Represents a message in an AI conversation.
  11   *
  12   * Messages are the fundamental unit of communication with AI models,
  13   * containing a role and one or more parts with different content types.
  14   *
  15   * @since 0.1.0
  16   *
  17   * @phpstan-import-type MessagePartArrayShape from MessagePart
  18   *
  19   * @phpstan-type MessageArrayShape array{
  20   *     role: string,
  21   *     parts: array<MessagePartArrayShape>
  22   * }
  23   *
  24   * @extends AbstractDataTransferObject<MessageArrayShape>
  25   */
  26  class Message extends AbstractDataTransferObject
  27  {
  28      public const KEY_ROLE = 'role';
  29      public const KEY_PARTS = 'parts';
  30      /**
  31       * @var MessageRoleEnum The role of the message sender.
  32       */
  33      protected MessageRoleEnum $role;
  34      /**
  35       * @var MessagePart[] The parts that make up this message.
  36       */
  37      protected array $parts;
  38      /**
  39       * Constructor.
  40       *
  41       * @since 0.1.0
  42       *
  43       * @param MessageRoleEnum $role The role of the message sender.
  44       * @param MessagePart[] $parts The parts that make up this message.
  45       * @throws InvalidArgumentException If parts contain invalid content for the role.
  46       */
  47      public function __construct(MessageRoleEnum $role, array $parts)
  48      {
  49          $this->role = $role;
  50          $this->parts = $parts;
  51          $this->validateParts();
  52      }
  53      /**
  54       * Gets the role of the message sender.
  55       *
  56       * @since 0.1.0
  57       *
  58       * @return MessageRoleEnum The role.
  59       */
  60      public function getRole(): MessageRoleEnum
  61      {
  62          return $this->role;
  63      }
  64      /**
  65       * Gets the message parts.
  66       *
  67       * @since 0.1.0
  68       *
  69       * @return MessagePart[] The message parts.
  70       */
  71      public function getParts(): array
  72      {
  73          return $this->parts;
  74      }
  75      /**
  76       * Returns a new instance with the given part appended.
  77       *
  78       * @since 0.1.0
  79       *
  80       * @param MessagePart $part The part to append.
  81       * @return Message A new instance with the part appended.
  82       * @throws InvalidArgumentException If the part is invalid for the role.
  83       */
  84      public function withPart(\WordPress\AiClient\Messages\DTO\MessagePart $part): \WordPress\AiClient\Messages\DTO\Message
  85      {
  86          $newParts = $this->parts;
  87          $newParts[] = $part;
  88          return new \WordPress\AiClient\Messages\DTO\Message($this->role, $newParts);
  89      }
  90      /**
  91       * Validates that the message parts are appropriate for the message role.
  92       *
  93       * @since 0.1.0
  94       *
  95       * @return void
  96       * @throws InvalidArgumentException If validation fails.
  97       */
  98      private function validateParts(): void
  99      {
 100          foreach ($this->parts as $part) {
 101              $type = $part->getType();
 102              if ($this->role->isUser() && $type->isFunctionCall()) {
 103                  throw new InvalidArgumentException('User messages cannot contain function calls.');
 104              }
 105              if ($this->role->isModel() && $type->isFunctionResponse()) {
 106                  throw new InvalidArgumentException('Model messages cannot contain function responses.');
 107              }
 108          }
 109      }
 110      /**
 111       * {@inheritDoc}
 112       *
 113       * @since 0.1.0
 114       */
 115      public static function getJsonSchema(): array
 116      {
 117          return ['type' => 'object', 'properties' => [self::KEY_ROLE => ['type' => 'string', 'enum' => MessageRoleEnum::getValues(), 'description' => 'The role of the message sender.'], self::KEY_PARTS => ['type' => 'array', 'items' => \WordPress\AiClient\Messages\DTO\MessagePart::getJsonSchema(), 'minItems' => 1, 'description' => 'The parts that make up this message.']], 'required' => [self::KEY_ROLE, self::KEY_PARTS]];
 118      }
 119      /**
 120       * {@inheritDoc}
 121       *
 122       * @since 0.1.0
 123       *
 124       * @return MessageArrayShape
 125       */
 126      public function toArray(): array
 127      {
 128          return [self::KEY_ROLE => $this->role->value, self::KEY_PARTS => array_map(function (\WordPress\AiClient\Messages\DTO\MessagePart $part) {
 129              return $part->toArray();
 130          }, $this->parts)];
 131      }
 132      /**
 133       * {@inheritDoc}
 134       *
 135       * This check is purely structural: it does not construct the message or its parts, so it has no
 136       * side effects (in particular, no filesystem access for file parts).
 137       *
 138       * @since 1.5.1
 139       */
 140      public static function isArrayShape(array $array): bool
 141      {
 142          if (!isset($array[self::KEY_ROLE]) || !is_string($array[self::KEY_ROLE]) || !MessageRoleEnum::isValidValue($array[self::KEY_ROLE])) {
 143              return \false;
 144          }
 145          if (!isset($array[self::KEY_PARTS]) || !is_array($array[self::KEY_PARTS])) {
 146              return \false;
 147          }
 148          $role = MessageRoleEnum::from($array[self::KEY_ROLE]);
 149          if (!$role->isUser() && !$role->isModel()) {
 150              return \false;
 151          }
 152          // Keys are intentionally not required to be consecutive, matching fromArray() (e.g. after array_filter()).
 153          foreach ($array[self::KEY_PARTS] as $partData) {
 154              if (!is_array($partData) || !\WordPress\AiClient\Messages\DTO\MessagePart::isArrayShape($partData)) {
 155                  return \false;
 156              }
 157              // Mirror the role/part rules enforced by the constructor (validateParts()).
 158              if ($role->isUser() && isset($partData[\WordPress\AiClient\Messages\DTO\MessagePart::KEY_FUNCTION_CALL])) {
 159                  return \false;
 160              }
 161              if ($role->isModel() && isset($partData[\WordPress\AiClient\Messages\DTO\MessagePart::KEY_FUNCTION_RESPONSE])) {
 162                  return \false;
 163              }
 164          }
 165          return \true;
 166      }
 167      /**
 168       * {@inheritDoc}
 169       *
 170       * @since 0.1.0
 171       *
 172       * @return self The specific message class based on the role.
 173       */
 174      final public static function fromArray(array $array): self
 175      {
 176          static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]);
 177          $role = MessageRoleEnum::from($array[self::KEY_ROLE]);
 178          $partsData = $array[self::KEY_PARTS];
 179          $parts = array_map(function (array $partData) {
 180              return \WordPress\AiClient\Messages\DTO\MessagePart::fromArray($partData);
 181          }, $partsData);
 182          // Determine which concrete class to instantiate based on role
 183          if ($role->isUser()) {
 184              return new \WordPress\AiClient\Messages\DTO\UserMessage($parts);
 185          } elseif ($role->isModel()) {
 186              return new \WordPress\AiClient\Messages\DTO\ModelMessage($parts);
 187          } else {
 188              // Only USER and MODEL roles are supported
 189              throw new InvalidArgumentException('Invalid message role: ' . $role->value);
 190          }
 191      }
 192      /**
 193       * Performs a deep clone of the message.
 194       *
 195       * This method ensures that message part objects are cloned to prevent
 196       * modifications to the cloned message from affecting the original.
 197       *
 198       * @since 0.4.2
 199       */
 200      public function __clone()
 201      {
 202          $clonedParts = [];
 203          foreach ($this->parts as $part) {
 204              $clonedParts[] = clone $part;
 205          }
 206          $this->parts = $clonedParts;
 207      }
 208  }


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