Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5604d1d
feat: first draft of PromptBuilder
JasonTheAdams Aug 14, 2025
3e436ee
feat: adds Prompts utility class
JasonTheAdams Aug 21, 2025
73c2a09
feat: expands builder message handling
JasonTheAdams Aug 21, 2025
c91e520
feat: improves model requirement system
JasonTheAdams Aug 21, 2025
7654608
feat: adds DTO::isArrayShape
JasonTheAdams Aug 21, 2025
c996c45
test: adds isArrayShape tests
JasonTheAdams Aug 21, 2025
8f390d6
test: adds some isArrayShape tests
JasonTheAdams Aug 21, 2025
e8ac1c2
refactor: cleans up file type checking
JasonTheAdams Aug 21, 2025
9638c41
feat: adds type checking methods to File DTO
JasonTheAdams Aug 21, 2025
0da377e
refactor: uses new file type checking
JasonTheAdams Aug 21, 2025
eb9f197
refactor: removes inferred property in favor of late checking
JasonTheAdams Aug 21, 2025
88242f6
feat: adds means of converting ModelConfig to requirements
JasonTheAdams Aug 21, 2025
2a65902
refactor: cleans up how system instructions work
JasonTheAdams Aug 21, 2025
f480ce1
refactor: switches validating to getting a model
JasonTheAdams Aug 21, 2025
59cc9df
fix: always sets the model config
JasonTheAdams Aug 22, 2025
01f294b
feat: adds generate methods
JasonTheAdams Aug 22, 2025
3fd1fc2
refactor: tweaks enum comparison
JasonTheAdams Aug 22, 2025
c56bf85
test: adds includeOutputModality tests
JasonTheAdams Aug 22, 2025
539a278
refactor: uses system instructions instead of message
JasonTheAdams Aug 22, 2025
e86190e
test: adds a lot of Prompt Builder tests
JasonTheAdams Aug 22, 2025
29b59fe
test: fixes failing builder tests
JasonTheAdams Aug 22, 2025
0fd756e
refactor: removes the need for the Prompts utility class
JasonTheAdams Aug 22, 2025
78fa2aa
feat: improves methods for checking if prompt is supported
JasonTheAdams Aug 22, 2025
4d91dc4
test: fixes a bajillion linting errors
JasonTheAdams Aug 22, 2025
d976ccf
fix: uses output schema as require option value
JasonTheAdams Aug 23, 2025
bba08dd
refactor: moves includeOutputModalities to PromptBuilder
JasonTheAdams Aug 23, 2025
9281c54
refactor: improves handling of message arrays
JasonTheAdams Aug 23, 2025
d7320ce
refactor: consolidates builder file methods
JasonTheAdams Aug 23, 2025
cb54e0b
feat: adds file isInline and isRemote methods
JasonTheAdams Aug 23, 2025
f324b96
refactor: removes usingRegistry method for now
JasonTheAdams Aug 25, 2025
a07c242
feat: validates message parts
JasonTheAdams Aug 25, 2025
0d39e74
fix: prepends message in withHistory
JasonTheAdams Aug 25, 2025
7d25a03
feat: turns OptionEnum into a superset of ModelConfig
JasonTheAdams Aug 25, 2025
3c6d641
refactor: switches required and supports options to use enum as name
JasonTheAdams Aug 26, 2025
3396da6
refactor: tightens AbstractEnum cache scope
JasonTheAdams Aug 26, 2025
5d081b4
fix: corrects isSupported to use capabilities
JasonTheAdams Aug 26, 2025
cb7fb19
refactor: corrects input modality collection
JasonTheAdams Aug 26, 2025
2c7b443
feat: adds function call checking to requirements
JasonTheAdams Aug 26, 2025
b1a3f44
refactor: goes back to checking if model meets requirements in suppor…
JasonTheAdams Aug 26, 2025
f366cdd
refactor: adjusts generateResult to use capability
JasonTheAdams Aug 26, 2025
4f4779c
refactor: improves variable naming
JasonTheAdams Aug 26, 2025
5d9a2a9
refactor: cleans up generate methods big time
JasonTheAdams Aug 26, 2025
e7ffc3c
refactor: uses consistent to- naming convention
JasonTheAdams Aug 26, 2025
98dd0a2
chore: corrects array types as lists
JasonTheAdams Aug 26, 2025
d5d782e
refactor: adjusts isSupported to work more like generateResults
JasonTheAdams Aug 26, 2025
b8dcc95
chore: provides Prompt type
JasonTheAdams Aug 26, 2025
32e1fb8
refactor: renames output modalities method to use as- prefix
JasonTheAdams Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,327 changes: 1,327 additions & 0 deletions src/Builders/PromptBuilder.php

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion src/Common/AbstractDataTransferObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ abstract class AbstractDataTransferObject implements
*
* @since n.e.x.t
*
* @param TArrayShape $data The array data to validate.
* @param array<mixed> $data The array data to validate.
* @param string[] $requiredKeys The keys that must be present.
* @throws InvalidArgumentException If any required key is missing.
*/
Expand All @@ -62,6 +62,22 @@ protected static function validateFromArrayData(array $data, array $requiredKeys
}
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
public static function isArrayShape(array $array): bool
{
try {
/** @var TArrayShape $array */
static::fromArray($array);
return true;
} catch (InvalidArgumentException $e) {
return false;
}
}

/**
* Converts the object to a JSON-serializable format.
*
Expand Down
93 changes: 55 additions & 38 deletions src/Common/AbstractEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ final public static function from(string $value): self
*/
final public static function tryFrom(string $value): ?self
{
$constants = self::getConstants();
$constants = static::getConstants();
foreach ($constants as $name => $constantValue) {
if ($constantValue === $value) {
return self::getInstance($constantValue, $name);
Expand All @@ -157,7 +157,7 @@ final public static function tryFrom(string $value): ?self
final public static function cases(): array
{
$cases = [];
$constants = self::getConstants();
$constants = static::getConstants();
foreach ($constants as $name => $value) {
$cases[] = self::getInstance($value, $name);
}
Expand Down Expand Up @@ -203,7 +203,7 @@ final public function is(self $other): bool
*/
final public static function getValues(): array
{
return array_values(self::getConstants());
return array_values(static::getConstants());
}

/**
Expand Down Expand Up @@ -258,43 +258,60 @@ final protected static function getConstants(): array
$className = static::class;

if (!isset(self::$cache[$className])) {
$reflection = new ReflectionClass($className);
$constants = $reflection->getConstants();

// Validate all constants
$enumConstants = [];
foreach ($constants as $name => $value) {
// Check if constant name follows uppercase snake_case pattern
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
throw new RuntimeException(
sprintf(
'Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.',
$name,
$className
)
);
}

// Check if value is valid type
if (!is_string($value)) {
throw new RuntimeException(
sprintf(
'Invalid enum value type for constant %s::%s. ' .
'Only string values are allowed, %s given.',
$className,
$name,
gettype($value)
)
);
}

$enumConstants[$name] = $value;
self::$cache[$className] = static::determineClassEnumerations($className);
}

return self::$cache[$className];
}

/**
* Determines the class enumerations by reflecting on class constants.
*
* This method can be overridden by subclasses to customize how
* enumerations are determined (e.g., to add dynamic constants).
*
* @since n.e.x.t
*
* @param class-string $className The fully qualified class name.
* @return array<string, string> Map of constant names to values.
* @throws RuntimeException If invalid constant found.
*/
protected static function determineClassEnumerations(string $className): array
{
$reflection = new ReflectionClass($className);
$constants = $reflection->getConstants();

// Validate all constants
$enumConstants = [];
foreach ($constants as $name => $value) {
// Check if constant name follows uppercase snake_case pattern
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
throw new RuntimeException(
sprintf(
'Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.',
$name,
$className
)
);
}

// Check if value is valid type
if (!is_string($value)) {
throw new RuntimeException(
sprintf(
'Invalid enum value type for constant %s::%s. ' .
'Only string values are allowed, %s given.',
$className,
$name,
gettype($value)
)
);
}

self::$cache[$className] = $enumConstants;
$enumConstants[$name] = $value;
}

return self::$cache[$className];
return $enumConstants;
}

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

if (isset($constants[$constantName])) {
return $this->value === $constants[$constantName];
Expand All @@ -337,7 +354,7 @@ final public function __call(string $name, array $arguments): bool
final public static function __callStatic(string $name, array $arguments): self
{
$constantName = self::camelCaseToConstant($name);
$constants = self::getConstants();
$constants = static::getConstants();

if (isset($constants[$constantName])) {
return self::getInstance($constants[$constantName], $constantName);
Expand Down
11 changes: 11 additions & 0 deletions src/Common/Contracts/WithArrayTransformationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,15 @@ public function toArray(): array;
* @return self<TArrayShape> The created instance.
*/
public static function fromArray(array $array): self;

/**
* Checks if the array is a valid shape for this object.
*
* @since n.e.x.t
*
* @param array<mixed> $array The array to check.
* @return bool True if the array is a valid shape.
* @phpstan-assert-if-true TArrayShape $array
*/
public static function isArrayShape(array $array): bool;
}
36 changes: 36 additions & 0 deletions src/Files/DTO/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,30 @@ public function getFileType(): FileTypeEnum
return $this->fileType;
}

/**
* Checks if the file is an inline file.
*
* @since n.e.x.t
*
* @return bool True if the file is inline (base64/data URI).
*/
public function isInline(): bool
{
return $this->fileType->isInline();
}

/**
* Checks if the file is a remote file.
*
* @since n.e.x.t
*
* @return bool True if the file is remote (URL).
*/
public function isRemote(): bool
{
return $this->fileType->isRemote();
}

/**
* Gets the URL for remote files.
*
Expand Down Expand Up @@ -286,6 +310,18 @@ public function isText(): bool
return $this->mimeType->isText();
}

/**
* Checks if the file is a document.
*
* @since n.e.x.t
*
* @return bool True if the file is a document.
*/
public function isDocument(): bool
{
return $this->mimeType->isDocument();
}

/**
* Checks if the file is a specific MIME type.
*
Expand Down
45 changes: 45 additions & 0 deletions src/Messages/DTO/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace WordPress\AiClient\Messages\DTO;

use InvalidArgumentException;
use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;

Expand Down Expand Up @@ -45,11 +46,13 @@ class Message extends AbstractDataTransferObject
*
* @param MessageRoleEnum $role The role of the message sender.
* @param MessagePart[] $parts The parts that make up this message.
* @throws InvalidArgumentException If parts contain invalid content for the role.
*/
public function __construct(MessageRoleEnum $role, array $parts)
{
$this->role = $role;
$this->parts = $parts;
$this->validateParts();
}

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

/**
* Returns a new instance with the given part appended.
*
* @since n.e.x.t
*
* @param MessagePart $part The part to append.
* @return Message A new instance with the part appended.
* @throws InvalidArgumentException If the part is invalid for the role.
*/
public function withPart(MessagePart $part): Message
{
$newParts = $this->parts;
$newParts[] = $part;

return new Message($this->role, $newParts);
}

/**
* Validates that the message parts are appropriate for the message role.
*
* @since n.e.x.t
*
* @return void
* @throws InvalidArgumentException If validation fails.
*/
private function validateParts(): void
{
foreach ($this->parts as $part) {
if ($this->role->isUser() && $part->getType()->isFunctionCall()) {
throw new InvalidArgumentException(
'User messages cannot contain function calls.'
);
}

if ($this->role->isModel() && $part->getType()->isFunctionResponse()) {
throw new InvalidArgumentException(
'Model messages cannot contain function responses.'
);
}
}
}

/**
* {@inheritDoc}
*
Expand Down
4 changes: 4 additions & 0 deletions src/Messages/DTO/ModelMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
* This is a convenience class that automatically sets the role to MODEL.
* Model messages contain the AI's responses.
*
* Important: Do not rely on `instanceof ModelMessage` to determine the message role.
* This is merely a helper class for construction. Always use `$message->getRole()`
* to check the role of a message.
*
* @since n.e.x.t
*/
class ModelMessage extends Message
Expand Down
4 changes: 4 additions & 0 deletions src/Messages/DTO/UserMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
*
* This is a convenience class that automatically sets the role to USER.
*
* Important: Do not rely on `instanceof UserMessage` to determine the message role.
* This is merely a helper class for construction. Always use `$message->getRole()`
* to check the role of a message.
*
* @since n.e.x.t
*/
class UserMessage extends Message
Expand Down
Loading