diff --git a/src/Client.php b/src/Client.php index fb019c2e..6a50eb35 100644 --- a/src/Client.php +++ b/src/Client.php @@ -14,6 +14,7 @@ use OpenAI\Resources\Batches; use OpenAI\Resources\Chat; use OpenAI\Resources\Completions; +use OpenAI\Resources\Containers; use OpenAI\Resources\Edits; use OpenAI\Resources\Embeddings; use OpenAI\Resources\Files; @@ -68,6 +69,16 @@ public function chat(): Chat return new Chat($this->transporter); } + /** + * Create and manage containers for use with the Code Interpreter tool. + * + * @see https://platform.openai.com/docs/api-reference/containers + */ + public function containers(): Containers + { + return new Containers($this->transporter); + } + /** * Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms. * diff --git a/src/Contracts/Resources/ContainersContract.php b/src/Contracts/Resources/ContainersContract.php new file mode 100644 index 00000000..3066a58d --- /dev/null +++ b/src/Contracts/Resources/ContainersContract.php @@ -0,0 +1,45 @@ + $parameters + */ + public function create(array $parameters): CreateContainer; + + /** + * Retrieves a container with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/containers/retrieveContainer + */ + public function retrieve(string $id): RetrieveContainer; + + /** + * Delete a container with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/containers/deleteContainer + */ + public function delete(string $id): DeleteContainer; + + /** + * List containers + * + * @see https://platform.openai.com/docs/api-reference/containers/listContainers + * + * @param array $parameters + */ + public function list(array $parameters = []): ListContainers; +} diff --git a/src/Resources/Containers.php b/src/Resources/Containers.php new file mode 100644 index 00000000..65470c3b --- /dev/null +++ b/src/Resources/Containers.php @@ -0,0 +1,88 @@ + $parameters + */ + public function create(array $parameters): CreateContainer + { + $payload = Payload::create('containers', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return CreateContainer::from($response->data(), $response->meta()); + } + + /** + * Retrieves a container with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/containers/retrieveContainer + */ + public function retrieve(string $id): RetrieveContainer + { + $payload = Payload::retrieve('containers', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return RetrieveContainer::from($response->data(), $response->meta()); + } + + /** + * Delete a container with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/containers/deleteContainer + */ + public function delete(string $id): DeleteContainer + { + $payload = Payload::delete('containers', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return DeleteContainer::from($response->data(), $response->meta()); + } + + /** + * List containers + * + * @see https://platform.openai.com/docs/api-reference/containers/listContainers + * + * @param array $parameters + */ + public function list(array $parameters = []): ListContainers + { + $payload = Payload::list('containers', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return ListContainers::from($response->data(), $response->meta()); + } +} diff --git a/src/Responses/Containers/CreateContainer.php b/src/Responses/Containers/CreateContainer.php new file mode 100644 index 00000000..8a224c5f --- /dev/null +++ b/src/Responses/Containers/CreateContainer.php @@ -0,0 +1,78 @@ + + */ +final class CreateContainer implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'container' $object + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ExpiresAfter $expiresAfter, + public readonly int $lastActiveAt, + public readonly string $name, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CreateContainerType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + expiresAfter: ExpiresAfter::from($attributes['expires_after']), + lastActiveAt: $attributes['last_active_at'], + name: $attributes['name'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'expires_after' => $this->expiresAfter->toArray(), + 'last_active_at' => $this->lastActiveAt, + 'name' => $this->name, + ]; + } +} diff --git a/src/Responses/Containers/DeleteContainer.php b/src/Responses/Containers/DeleteContainer.php new file mode 100644 index 00000000..c9bb5819 --- /dev/null +++ b/src/Responses/Containers/DeleteContainer.php @@ -0,0 +1,60 @@ + + */ +final class DeleteContainer implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly bool $deleted, + private readonly MetaInformation $meta, + ) {} + + /** + * @param DeleteContainerType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + id: $attributes['id'], + object: $attributes['object'], + deleted: $attributes['deleted'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'object' => $this->object, + 'deleted' => $this->deleted, + ]; + } +} diff --git a/src/Responses/Containers/ListContainers.php b/src/Responses/Containers/ListContainers.php new file mode 100644 index 00000000..76d51a87 --- /dev/null +++ b/src/Responses/Containers/ListContainers.php @@ -0,0 +1,78 @@ + + */ +final class ListContainers implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'list' $object + * @param RetrieveContainer[] $data + */ + private function __construct( + public readonly string $object, + public readonly array $data, + public readonly ?string $firstId, + public readonly ?string $lastId, + public readonly bool $hasMore, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ListContainersType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + object: $attributes['object'], + data: array_map( + fn (array $container): RetrieveContainer => RetrieveContainer::from($container, $meta), + $attributes['data'] + ), + firstId: $attributes['first_id'] ?? null, + lastId: $attributes['last_id'] ?? null, + hasMore: $attributes['has_more'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'object' => $this->object, + 'data' => array_map( + fn (RetrieveContainer $container): array => $container->toArray(), + $this->data + ), + 'first_id' => $this->firstId, + 'last_id' => $this->lastId, + 'has_more' => $this->hasMore, + ]; + } +} diff --git a/src/Responses/Containers/Objects/ExpiresAfter.php b/src/Responses/Containers/Objects/ExpiresAfter.php new file mode 100644 index 00000000..06679b09 --- /dev/null +++ b/src/Responses/Containers/Objects/ExpiresAfter.php @@ -0,0 +1,54 @@ + + */ +final class ExpiresAfter implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'last_active_at' $anchor + */ + private function __construct( + public readonly string $anchor, + public readonly int $minutes, + ) {} + + /** + * @param ExpiresAfterType $attributes + */ + public static function from(array $attributes): self + { + return new self( + anchor: $attributes['anchor'], + minutes: $attributes['minutes'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'anchor' => $this->anchor, + 'minutes' => $this->minutes, + ]; + } +} diff --git a/src/Responses/Containers/RetrieveContainer.php b/src/Responses/Containers/RetrieveContainer.php new file mode 100644 index 00000000..f086eae7 --- /dev/null +++ b/src/Responses/Containers/RetrieveContainer.php @@ -0,0 +1,78 @@ + + */ +final class RetrieveContainer implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'container' $object + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ExpiresAfter $expiresAfter, + public readonly int $lastActiveAt, + public readonly string $name, + private readonly MetaInformation $meta, + ) {} + + /** + * @param RetrieveContainerType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + expiresAfter: ExpiresAfter::from($attributes['expires_after']), + lastActiveAt: $attributes['last_active_at'], + name: $attributes['name'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'expires_after' => $this->expiresAfter->toArray(), + 'last_active_at' => $this->lastActiveAt, + 'name' => $this->name, + ]; + } +} diff --git a/src/Testing/Resources/ContainersTestResource.php b/src/Testing/Resources/ContainersTestResource.php new file mode 100644 index 00000000..2c4d2ebd --- /dev/null +++ b/src/Testing/Resources/ContainersTestResource.php @@ -0,0 +1,41 @@ +record(__FUNCTION__, func_get_args()); + } + + public function retrieve(string $id): RetrieveContainer + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function list(array $parameters = []): ListContainers + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function delete(string $id): DeleteContainer + { + return $this->record(__FUNCTION__, func_get_args()); + } +} diff --git a/tests/Fixtures/Container.php b/tests/Fixtures/Container.php new file mode 100644 index 00000000..8f2cc68d --- /dev/null +++ b/tests/Fixtures/Container.php @@ -0,0 +1,76 @@ + + */ +function containerResource(): array +{ + return [ + 'id' => 'container_abc123', + 'object' => 'container', + 'created_at' => 1690000000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ], + 'last_active_at' => 1690001000, + 'name' => 'Test Container', + ]; +} + +/** + * @return array + */ +function createContainerResource(): array +{ + return containerResource(); +} + +/** + * @return array + */ +function retrieveContainerResource(): array +{ + return containerResource(); +} + +/** + * @return array + */ +function listContainersResource(): array +{ + return [ + 'object' => 'list', + 'data' => [ + containerResource(), + [ + 'id' => 'container_def456', + 'object' => 'container', + 'created_at' => 1690010000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 120, + ], + 'last_active_at' => 1690011000, + 'name' => 'Another Test Container', + ], + ], + 'first_id' => 'container_abc123', + 'last_id' => 'container_def456', + 'has_more' => false, + ]; +} + +/** + * @return array + */ +function deleteContainerResource(): array +{ + return [ + 'id' => 'container_abc123', + 'object' => 'container', + 'deleted' => true, + ]; +} diff --git a/tests/Resources/Containers.php b/tests/Resources/Containers.php new file mode 100644 index 00000000..c456cd29 --- /dev/null +++ b/tests/Resources/Containers.php @@ -0,0 +1,117 @@ + 'Test Container', + ], \OpenAI\ValueObjects\Transporter\Response::from(createContainerResource(), metaHeaders())); + + $result = $client->containers()->create([ + 'name' => 'Test Container', + ]); + + expect($result) + ->toBeInstanceOf(CreateContainer::class) + ->id->toBe('container_abc123') + ->object->toBe('container') + ->createdAt->toBe(1690000000) + ->status->toBe('active') + ->expiresAfter->toBeInstanceOf(ExpiresAfter::class) + ->lastActiveAt->toBe(1690001000) + ->name->toBe('Test Container'); + + expect($result->expiresAfter) + ->anchor->toBe('last_active_at') + ->minutes->toBe(60); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('retrieve', function () { + $client = mockClient('GET', 'containers/container_abc123', [], + \OpenAI\ValueObjects\Transporter\Response::from(retrieveContainerResource(), metaHeaders())); + + $result = $client->containers()->retrieve('container_abc123'); + + expect($result) + ->toBeInstanceOf(RetrieveContainer::class) + ->id->toBe('container_abc123') + ->object->toBe('container') + ->createdAt->toBe(1690000000) + ->status->toBe('active') + ->expiresAfter->toBeInstanceOf(ExpiresAfter::class) + ->lastActiveAt->toBe(1690001000) + ->name->toBe('Test Container'); + + expect($result->expiresAfter) + ->anchor->toBe('last_active_at') + ->minutes->toBe(60); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('delete', function () { + $client = mockClient('DELETE', 'containers/container_abc123', [], + \OpenAI\ValueObjects\Transporter\Response::from(deleteContainerResource(), metaHeaders())); + + $result = $client->containers()->delete('container_abc123'); + + expect($result) + ->toBeInstanceOf(DeleteContainer::class) + ->id->toBe('container_abc123') + ->object->toBe('container') + ->deleted->toBe(true); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('list', function () { + $client = mockClient('GET', 'containers', [], + \OpenAI\ValueObjects\Transporter\Response::from(listContainersResource(), metaHeaders())); + + $result = $client->containers()->list(); + + expect($result) + ->toBeInstanceOf(ListContainers::class) + ->object->toBe('list') + ->data->toBeArray()->toHaveCount(2) + ->firstId->toBe('container_abc123') + ->lastId->toBe('container_def456') + ->hasMore->toBe(false); + + expect($result->data[0]) + ->toBeInstanceOf(RetrieveContainer::class) + ->id->toBe('container_abc123') + ->name->toBe('Test Container'); + + expect($result->data[1]) + ->toBeInstanceOf(RetrieveContainer::class) + ->id->toBe('container_def456') + ->name->toBe('Another Test Container'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('list with parameters', function () { + $client = mockClient('GET', 'containers', ['limit' => 2], + \OpenAI\ValueObjects\Transporter\Response::from(listContainersResource(), metaHeaders())); + + $result = $client->containers()->list([ + 'limit' => 2, + ]); + + expect($result) + ->toBeInstanceOf(ListContainers::class) + ->object->toBe('list') + ->data->toBeArray()->toHaveCount(2); +}); diff --git a/tests/Responses/Containers/CreateContainer.php b/tests/Responses/Containers/CreateContainer.php new file mode 100644 index 00000000..7728a4a1 --- /dev/null +++ b/tests/Responses/Containers/CreateContainer.php @@ -0,0 +1,39 @@ +id->toBe('container_abc123') + ->object->toBe('container') + ->createdAt->toBe(1690000000) + ->status->toBe('active') + ->expiresAfter->toBeInstanceOf(ExpiresAfter::class) + ->lastActiveAt->toBe(1690001000) + ->name->toBe('Test Container'); + + expect($result->expiresAfter) + ->anchor->toBe('last_active_at') + ->minutes->toBe(60); +}); + +test('to array', function () { + $result = CreateContainer::from(createContainerResource(), meta()); + + expect($result->toArray()) + ->toBe([ + 'id' => 'container_abc123', + 'object' => 'container', + 'created_at' => 1690000000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ], + 'last_active_at' => 1690001000, + 'name' => 'Test Container', + ]); +}); diff --git a/tests/Responses/Containers/DeleteContainer.php b/tests/Responses/Containers/DeleteContainer.php new file mode 100644 index 00000000..3cee846e --- /dev/null +++ b/tests/Responses/Containers/DeleteContainer.php @@ -0,0 +1,23 @@ +id->toBe('container_abc123') + ->object->toBe('container') + ->deleted->toBe(true); +}); + +test('to array', function () { + $result = DeleteContainer::from(deleteContainerResource(), meta()); + + expect($result->toArray()) + ->toBe([ + 'id' => 'container_abc123', + 'object' => 'container', + 'deleted' => true, + ]); +}); diff --git a/tests/Responses/Containers/ListContainers.php b/tests/Responses/Containers/ListContainers.php new file mode 100644 index 00000000..fe2d8580 --- /dev/null +++ b/tests/Responses/Containers/ListContainers.php @@ -0,0 +1,63 @@ +object->toBe('list') + ->data->toBeArray()->toHaveCount(2) + ->firstId->toBe('container_abc123') + ->lastId->toBe('container_def456') + ->hasMore->toBe(false); + + expect($result->data[0]) + ->toBeInstanceOf(RetrieveContainer::class) + ->id->toBe('container_abc123') + ->name->toBe('Test Container'); + + expect($result->data[1]) + ->toBeInstanceOf(RetrieveContainer::class) + ->id->toBe('container_def456') + ->name->toBe('Another Test Container'); +}); + +test('to array', function () { + $result = ListContainers::from(listContainersResource(), meta()); + + expect($result->toArray()) + ->toBe([ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'container_abc123', + 'object' => 'container', + 'created_at' => 1690000000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ], + 'last_active_at' => 1690001000, + 'name' => 'Test Container', + ], + [ + 'id' => 'container_def456', + 'object' => 'container', + 'created_at' => 1690010000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 120, + ], + 'last_active_at' => 1690011000, + 'name' => 'Another Test Container', + ], + ], + 'first_id' => 'container_abc123', + 'last_id' => 'container_def456', + 'has_more' => false, + ]); +}); diff --git a/tests/Responses/Containers/Objects/ExpiresAfter.php b/tests/Responses/Containers/Objects/ExpiresAfter.php new file mode 100644 index 00000000..524d2bf0 --- /dev/null +++ b/tests/Responses/Containers/Objects/ExpiresAfter.php @@ -0,0 +1,27 @@ + 'last_active_at', + 'minutes' => 60, + ]); + + expect($result) + ->anchor->toBe('last_active_at') + ->minutes->toBe(60); +}); + +test('to array', function () { + $result = ExpiresAfter::from([ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ]); + + expect($result->toArray()) + ->toBe([ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ]); +}); diff --git a/tests/Responses/Containers/RetrieveContainer.php b/tests/Responses/Containers/RetrieveContainer.php new file mode 100644 index 00000000..0b35fa7f --- /dev/null +++ b/tests/Responses/Containers/RetrieveContainer.php @@ -0,0 +1,39 @@ +id->toBe('container_abc123') + ->object->toBe('container') + ->createdAt->toBe(1690000000) + ->status->toBe('active') + ->expiresAfter->toBeInstanceOf(ExpiresAfter::class) + ->lastActiveAt->toBe(1690001000) + ->name->toBe('Test Container'); + + expect($result->expiresAfter) + ->anchor->toBe('last_active_at') + ->minutes->toBe(60); +}); + +test('to array', function () { + $result = RetrieveContainer::from(retrieveContainerResource(), meta()); + + expect($result->toArray()) + ->toBe([ + 'id' => 'container_abc123', + 'object' => 'container', + 'created_at' => 1690000000, + 'status' => 'active', + 'expires_after' => [ + 'anchor' => 'last_active_at', + 'minutes' => 60, + ], + 'last_active_at' => 1690001000, + 'name' => 'Test Container', + ]); +});