Skip to content

Commit e5e1f18

Browse files
Merge pull request #49 from WordPress/prompt-builder
2 parents 0b36ef8 + 32e1fb8 commit e5e1f18

26 files changed

+4730
-302
lines changed

src/Builders/PromptBuilder.php

Lines changed: 1171 additions & 0 deletions
Large diffs are not rendered by default.

src/Common/AbstractDataTransferObject.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ abstract class AbstractDataTransferObject implements
3737
*
3838
* @since n.e.x.t
3939
*
40-
* @param TArrayShape $data The array data to validate.
40+
* @param array<mixed> $data The array data to validate.
4141
* @param string[] $requiredKeys The keys that must be present.
4242
* @throws InvalidArgumentException If any required key is missing.
4343
*/
@@ -62,6 +62,22 @@ protected static function validateFromArrayData(array $data, array $requiredKeys
6262
}
6363
}
6464

65+
/**
66+
* {@inheritDoc}
67+
*
68+
* @since n.e.x.t
69+
*/
70+
public static function isArrayShape(array $array): bool
71+
{
72+
try {
73+
/** @var TArrayShape $array */
74+
static::fromArray($array);
75+
return true;
76+
} catch (InvalidArgumentException $e) {
77+
return false;
78+
}
79+
}
80+
6581
/**
6682
* Converts the object to a JSON-serializable format.
6783
*

src/Common/AbstractEnum.php

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ final public static function from(string $value): self
138138
*/
139139
final public static function tryFrom(string $value): ?self
140140
{
141-
$constants = self::getConstants();
141+
$constants = static::getConstants();
142142
foreach ($constants as $name => $constantValue) {
143143
if ($constantValue === $value) {
144144
return self::getInstance($constantValue, $name);
@@ -157,7 +157,7 @@ final public static function tryFrom(string $value): ?self
157157
final public static function cases(): array
158158
{
159159
$cases = [];
160-
$constants = self::getConstants();
160+
$constants = static::getConstants();
161161
foreach ($constants as $name => $value) {
162162
$cases[] = self::getInstance($value, $name);
163163
}
@@ -203,7 +203,7 @@ final public function is(self $other): bool
203203
*/
204204
final public static function getValues(): array
205205
{
206-
return array_values(self::getConstants());
206+
return array_values(static::getConstants());
207207
}
208208

209209
/**
@@ -258,43 +258,60 @@ final protected static function getConstants(): array
258258
$className = static::class;
259259

260260
if (!isset(self::$cache[$className])) {
261-
$reflection = new ReflectionClass($className);
262-
$constants = $reflection->getConstants();
263-
264-
// Validate all constants
265-
$enumConstants = [];
266-
foreach ($constants as $name => $value) {
267-
// Check if constant name follows uppercase snake_case pattern
268-
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
269-
throw new RuntimeException(
270-
sprintf(
271-
'Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.',
272-
$name,
273-
$className
274-
)
275-
);
276-
}
277-
278-
// Check if value is valid type
279-
if (!is_string($value)) {
280-
throw new RuntimeException(
281-
sprintf(
282-
'Invalid enum value type for constant %s::%s. ' .
283-
'Only string values are allowed, %s given.',
284-
$className,
285-
$name,
286-
gettype($value)
287-
)
288-
);
289-
}
290-
291-
$enumConstants[$name] = $value;
261+
self::$cache[$className] = static::determineClassEnumerations($className);
262+
}
263+
264+
return self::$cache[$className];
265+
}
266+
267+
/**
268+
* Determines the class enumerations by reflecting on class constants.
269+
*
270+
* This method can be overridden by subclasses to customize how
271+
* enumerations are determined (e.g., to add dynamic constants).
272+
*
273+
* @since n.e.x.t
274+
*
275+
* @param class-string $className The fully qualified class name.
276+
* @return array<string, string> Map of constant names to values.
277+
* @throws RuntimeException If invalid constant found.
278+
*/
279+
protected static function determineClassEnumerations(string $className): array
280+
{
281+
$reflection = new ReflectionClass($className);
282+
$constants = $reflection->getConstants();
283+
284+
// Validate all constants
285+
$enumConstants = [];
286+
foreach ($constants as $name => $value) {
287+
// Check if constant name follows uppercase snake_case pattern
288+
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
289+
throw new RuntimeException(
290+
sprintf(
291+
'Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.',
292+
$name,
293+
$className
294+
)
295+
);
296+
}
297+
298+
// Check if value is valid type
299+
if (!is_string($value)) {
300+
throw new RuntimeException(
301+
sprintf(
302+
'Invalid enum value type for constant %s::%s. ' .
303+
'Only string values are allowed, %s given.',
304+
$className,
305+
$name,
306+
gettype($value)
307+
)
308+
);
292309
}
293310

294-
self::$cache[$className] = $enumConstants;
311+
$enumConstants[$name] = $value;
295312
}
296313

297-
return self::$cache[$className];
314+
return $enumConstants;
298315
}
299316

300317
/**
@@ -312,7 +329,7 @@ final public function __call(string $name, array $arguments): bool
312329
// Handle is* methods
313330
if (strpos($name, 'is') === 0) {
314331
$constantName = self::camelCaseToConstant(substr($name, 2));
315-
$constants = self::getConstants();
332+
$constants = static::getConstants();
316333

317334
if (isset($constants[$constantName])) {
318335
return $this->value === $constants[$constantName];
@@ -337,7 +354,7 @@ final public function __call(string $name, array $arguments): bool
337354
final public static function __callStatic(string $name, array $arguments): self
338355
{
339356
$constantName = self::camelCaseToConstant($name);
340-
$constants = self::getConstants();
357+
$constants = static::getConstants();
341358

342359
if (isset($constants[$constantName])) {
343360
return self::getInstance($constants[$constantName], $constantName);

src/Common/Contracts/WithArrayTransformationInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,15 @@ public function toArray(): array;
3131
* @return self<TArrayShape> The created instance.
3232
*/
3333
public static function fromArray(array $array): self;
34+
35+
/**
36+
* Checks if the array is a valid shape for this object.
37+
*
38+
* @since n.e.x.t
39+
*
40+
* @param array<mixed> $array The array to check.
41+
* @return bool True if the array is a valid shape.
42+
* @phpstan-assert-if-true TArrayShape $array
43+
*/
44+
public static function isArrayShape(array $array): bool;
3445
}

src/Files/DTO/File.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ public function getFileType(): FileTypeEnum
174174
return $this->fileType;
175175
}
176176

177+
/**
178+
* Checks if the file is an inline file.
179+
*
180+
* @since n.e.x.t
181+
*
182+
* @return bool True if the file is inline (base64/data URI).
183+
*/
184+
public function isInline(): bool
185+
{
186+
return $this->fileType->isInline();
187+
}
188+
189+
/**
190+
* Checks if the file is a remote file.
191+
*
192+
* @since n.e.x.t
193+
*
194+
* @return bool True if the file is remote (URL).
195+
*/
196+
public function isRemote(): bool
197+
{
198+
return $this->fileType->isRemote();
199+
}
200+
177201
/**
178202
* Gets the URL for remote files.
179203
*
@@ -286,6 +310,18 @@ public function isText(): bool
286310
return $this->mimeType->isText();
287311
}
288312

313+
/**
314+
* Checks if the file is a document.
315+
*
316+
* @since n.e.x.t
317+
*
318+
* @return bool True if the file is a document.
319+
*/
320+
public function isDocument(): bool
321+
{
322+
return $this->mimeType->isDocument();
323+
}
324+
289325
/**
290326
* Checks if the file is a specific MIME type.
291327
*

src/Messages/DTO/Message.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace WordPress\AiClient\Messages\DTO;
66

7+
use InvalidArgumentException;
78
use WordPress\AiClient\Common\AbstractDataTransferObject;
89
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
910

@@ -45,11 +46,13 @@ class Message extends AbstractDataTransferObject
4546
*
4647
* @param MessageRoleEnum $role The role of the message sender.
4748
* @param MessagePart[] $parts The parts that make up this message.
49+
* @throws InvalidArgumentException If parts contain invalid content for the role.
4850
*/
4951
public function __construct(MessageRoleEnum $role, array $parts)
5052
{
5153
$this->role = $role;
5254
$this->parts = $parts;
55+
$this->validateParts();
5356
}
5457

5558
/**
@@ -76,6 +79,48 @@ public function getParts(): array
7679
return $this->parts;
7780
}
7881

82+
/**
83+
* Returns a new instance with the given part appended.
84+
*
85+
* @since n.e.x.t
86+
*
87+
* @param MessagePart $part The part to append.
88+
* @return Message A new instance with the part appended.
89+
* @throws InvalidArgumentException If the part is invalid for the role.
90+
*/
91+
public function withPart(MessagePart $part): Message
92+
{
93+
$newParts = $this->parts;
94+
$newParts[] = $part;
95+
96+
return new Message($this->role, $newParts);
97+
}
98+
99+
/**
100+
* Validates that the message parts are appropriate for the message role.
101+
*
102+
* @since n.e.x.t
103+
*
104+
* @return void
105+
* @throws InvalidArgumentException If validation fails.
106+
*/
107+
private function validateParts(): void
108+
{
109+
foreach ($this->parts as $part) {
110+
if ($this->role->isUser() && $part->getType()->isFunctionCall()) {
111+
throw new InvalidArgumentException(
112+
'User messages cannot contain function calls.'
113+
);
114+
}
115+
116+
if ($this->role->isModel() && $part->getType()->isFunctionResponse()) {
117+
throw new InvalidArgumentException(
118+
'Model messages cannot contain function responses.'
119+
);
120+
}
121+
}
122+
}
123+
79124
/**
80125
* {@inheritDoc}
81126
*

src/Messages/DTO/ModelMessage.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* This is a convenience class that automatically sets the role to MODEL.
1313
* Model messages contain the AI's responses.
1414
*
15+
* Important: Do not rely on `instanceof ModelMessage` to determine the message role.
16+
* This is merely a helper class for construction. Always use `$message->getRole()`
17+
* to check the role of a message.
18+
*
1519
* @since n.e.x.t
1620
*/
1721
class ModelMessage extends Message

src/Messages/DTO/UserMessage.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
*
1212
* This is a convenience class that automatically sets the role to USER.
1313
*
14+
* Important: Do not rely on `instanceof UserMessage` to determine the message role.
15+
* This is merely a helper class for construction. Always use `$message->getRole()`
16+
* to check the role of a message.
17+
*
1418
* @since n.e.x.t
1519
*/
1620
class UserMessage extends Message

0 commit comments

Comments
 (0)