Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,25 @@ $embeddings = AiClient::generateEmbeddings(
);
```

### Function declaration annotations

`FunctionDeclaration` accepts optional annotations for consumer-specific hints that do not belong to the function's parameter schema:

```php
$function = new FunctionDeclaration(
'get_weather',
'Gets the weather',
null,
['deferredLoading' => true]
);

$annotations = $function->getAnnotations();
```

Annotations are an `array<string, mixed>` whose values should be JSON-serializable. The core SDK preserves annotations without interpreting their names or values. Providers and other consumers define the annotations they recognize and ignore unknown annotations. Provider-specific annotations should use namespaced keys or a nested provider-specific map to avoid collisions, and must not be blindly merged into provider requests.

Empty annotations are omitted from serialized declarations, preserving the existing shape for callers that do not use them. Missing annotations are restored as an empty array. Annotations are not authorization; applications must still validate tool execution.

## Class diagrams

This section shows comprehensive class diagrams for the proposed architecture. For explanation on specific terms, see the [glossary](./GLOSSARY.md).
Expand Down Expand Up @@ -806,6 +825,7 @@ direction LR
+getName() string
+getDescription() string
+getParameters() mixed
+getAnnotations() array< string, mixed >
+getJsonSchema() array< string, mixed >$
}
class FunctionResponse {
Expand Down Expand Up @@ -1229,6 +1249,7 @@ direction LR
+getName() string
+getDescription() string
+getParameters() mixed
+getAnnotations() array< string, mixed >
+getJsonSchema() array< string, mixed >$
}
class Tool {
Expand Down
46 changes: 41 additions & 5 deletions src/Tools/DTO/FunctionDeclaration.php
Comment thread
superdav42 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
* Represents a function declaration for AI models.
*
* This DTO describes a function that can be called by the AI model,
* including its name, description, and parameter schema.
* including its name, description, parameter schema, and optional annotations.
*
* @since 0.1.0
*
* @phpstan-type FunctionDeclarationArrayShape array{
* name: string,
* description: string,
* parameters?: array<string, mixed>
* parameters?: array<string, mixed>,
* annotations?: array<string, mixed>
* }
*
* @extends AbstractDataTransferObject<FunctionDeclarationArrayShape>
Expand All @@ -27,6 +28,7 @@ class FunctionDeclaration extends AbstractDataTransferObject
public const KEY_NAME = 'name';
public const KEY_DESCRIPTION = 'description';
public const KEY_PARAMETERS = 'parameters';
public const KEY_ANNOTATIONS = 'annotations';
/**
* @var string The name of the function.
*/
Expand All @@ -42,20 +44,32 @@ class FunctionDeclaration extends AbstractDataTransferObject
*/
private ?array $parameters;

/**
* @var array<string, mixed> Open-ended annotations which may be consumed by Providers.
*/
private array $annotations;

/**
* Constructor.
*
* @since 0.1.0
* @since n.e.x.t Adds the optional $annotations parameter.
*
* @param string $name The name of the function.
* @param string $description A description of what the function does.
* @param array<string, mixed>|null $parameters The JSON schema for the function parameters.
* @param array<string, mixed> $annotations Optional annotations with JSON-serializable values.
*/
public function __construct(string $name, string $description, ?array $parameters = null)
{
public function __construct(
string $name,
string $description,
?array $parameters = null,
array $annotations = []
) {
$this->name = $name;
$this->description = $description;
$this->parameters = $parameters;
$this->annotations = $annotations;
}

/**
Expand Down Expand Up @@ -94,6 +108,18 @@ public function getParameters(): ?array
return $this->parameters;
}

/**
* Gets the function annotations.
*
* @since n.e.x.t
*
* @return array<string, mixed> The annotations, or an empty array if none were provided.
*/
public function getAnnotations(): array
{
return $this->annotations;
}

/**
* {@inheritDoc}
*
Expand All @@ -117,6 +143,11 @@ public static function getJsonSchema(): array
'description' => 'The JSON schema for the function parameters.',
'additionalProperties' => true,
],
self::KEY_ANNOTATIONS => [
'type' => 'object',
'description' => 'Optional annotations interpreted by consumers.',
'additionalProperties' => true,
],
],
'required' => [self::KEY_NAME, self::KEY_DESCRIPTION],
];
Expand All @@ -140,6 +171,10 @@ public function toArray(): array
$data[self::KEY_PARAMETERS] = $this->parameters;
}

if ($this->annotations !== []) {
$data[self::KEY_ANNOTATIONS] = $this->annotations;
}

return $data;
}

Expand All @@ -155,7 +190,8 @@ public static function fromArray(array $array): self
return new self(
$array[self::KEY_NAME],
$array[self::KEY_DESCRIPTION],
$array[self::KEY_PARAMETERS] ?? null
$array[self::KEY_PARAMETERS] ?? null,
$array[self::KEY_ANNOTATIONS] ?? []
);
}
}
79 changes: 79 additions & 0 deletions tests/unit/Tools/DTO/FunctionDeclarationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace WordPress\AiClient\Tests\unit\Tools\DTO;

use PHPUnit\Framework\TestCase;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Tests\traits\ArrayTransformationTestTrait;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;

Expand Down Expand Up @@ -320,4 +321,82 @@ public function testImplementsWithArrayTransformationInterface(): void
$declaration = new FunctionDeclaration('test', 'test function');
$this->assertImplementsArrayTransformation($declaration);
}

/**
* Tests legacy declarations keep their serialized shape without empty annotations.
*
* @return void
*/
public function testAnnotationsDefaultsPreserveCompatibility(): void
{
$legacy = ['name' => 'get_weather', 'description' => 'Gets the weather'];
$declarations = [
new FunctionDeclaration('get_weather', 'Gets the weather'),
new FunctionDeclaration('get_weather', 'Gets the weather', null, []),
FunctionDeclaration::fromArray($legacy),
];
foreach ($declarations as $declaration) {
$this->assertSame([], $declaration->getAnnotations());
$this->assertSame($legacy, $declaration->toArray());
$this->assertSame($legacy, json_decode((string) json_encode($declaration), true));
}
}

/**
* Tests arbitrary annotations survive array and JSON serialization unchanged.
*
* @return void
*/
public function testAnnotationsRoundTrip(): void
{
$annotations = [
'deferredLoading' => true,
'readOnlyHint' => false,
'vendor' => ['labels' => ['weather', 'public'], 'priority' => 0, 'optional' => null],
];
$declaration = new FunctionDeclaration('get_weather', 'Gets the weather', null, $annotations);
$this->assertSame($annotations, $declaration->getAnnotations());
$this->assertNull($declaration->getParameters());
$this->assertSame($annotations, $declaration->toArray()['annotations']);
$this->assertSame($annotations, FunctionDeclaration::fromArray($declaration->toArray())->getAnnotations());

$json = json_decode((string) json_encode($declaration), true);
$this->assertSame($annotations, FunctionDeclaration::fromArray($json)->getAnnotations());
}

/**
* Tests annotations are optional and unconstrained in the declaration schema.
*
* @return void
*/
public function testAnnotationsSchema(): void
{
$schema = FunctionDeclaration::getJsonSchema();
$this->assertSame('object', $schema['properties']['annotations']['type']);
$this->assertTrue($schema['properties']['annotations']['additionalProperties']);
$this->assertNotContains('annotations', $schema['required']);
}

/**
* Tests model configuration preserves annotations and clones declarations independently.
*
* @return void
*/
public function testAnnotationsSurviveModelConfigRoundTripAndClone(): void
{
$annotations = ['vendor' => ['enabled' => false]];
$declaration = new FunctionDeclaration('lookup', 'Looks up a record', ['type' => 'object'], $annotations);
$config = new ModelConfig();
$config->setFunctionDeclarations([$declaration]);
$restored = ModelConfig::fromArray($config->toArray());
$cloned = clone $config;
$this->assertSame($annotations, $restored->getFunctionDeclarations()[0]->getAnnotations());
$this->assertSame($annotations, $cloned->getFunctionDeclarations()[0]->getAnnotations());
$this->assertNotSame($declaration, $cloned->getFunctionDeclarations()[0]);

$copy = $cloned->getFunctionDeclarations()[0]->getAnnotations();
$copy['vendor']['enabled'] = true;
$this->assertSame($annotations, $declaration->getAnnotations());
$this->assertSame($annotations, $cloned->getFunctionDeclarations()[0]->getAnnotations());
}
}
Loading