-
Notifications
You must be signed in to change notification settings - Fork 24
Implement ProviderRegistry with comprehensive test suite #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 38 commits
3c56f68
4b09a07
a84a4ac
40b926b
7bdcb9c
877fc55
cacb2dc
63fe203
b1e63d5
37d1124
83927be
d77b021
06411b9
b44f26c
cbf1aef
7956769
4ac777e
57dfe25
8f96b26
766fcf8
8a64e8a
23a2a01
14e650b
2aeac0e
212bab6
4258145
da98ac1
8de52d2
2f1d300
db59e19
2c9ff36
03310ba
489a4ed
cafc630
df20ba4
c52f1d9
3abe579
45de1fa
6e25240
f868e7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace WordPress\AiClient\Providers; | ||
|
||
use InvalidArgumentException; | ||
use WordPress\AiClient\Providers\Contracts\ProviderInterface; | ||
use WordPress\AiClient\Providers\DTO\ProviderMetadata; | ||
use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata; | ||
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelConfig; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; | ||
|
||
/** | ||
* Registry for managing AI providers and their models. | ||
* | ||
* This class provides a centralized way to register AI providers, discover | ||
* their capabilities, and find suitable models based on requirements. | ||
* | ||
* @since n.e.x.t | ||
*/ | ||
class ProviderRegistry | ||
{ | ||
/** | ||
* @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names. | ||
*/ | ||
private array $providerClassNames = []; | ||
|
||
|
||
/** | ||
* Registers a provider class with the registry. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the | ||
* ProviderInterface | ||
* @throws InvalidArgumentException If the class doesn't exist or implement the required interface. | ||
*/ | ||
public function registerProvider(string $className): void | ||
{ | ||
if (!class_exists($className)) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider class does not exist: %s', $className) | ||
); | ||
} | ||
|
||
// Validate that class implements ProviderInterface | ||
if (!is_subclass_of($className, ProviderInterface::class)) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className) | ||
); | ||
} | ||
|
||
$metadata = $className::metadata(); | ||
|
||
if (!$metadata instanceof ProviderMetadata) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className) | ||
); | ||
} | ||
|
||
$this->providerClassNames[$metadata->getId()] = $className; | ||
} | ||
|
||
/** | ||
* Checks if a provider is registered. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check. | ||
* @return bool True if the provider is registered. | ||
*/ | ||
public function hasProvider(string $idOrClassName): bool | ||
{ | ||
return isset($this->providerClassNames[$idOrClassName]) || | ||
in_array($idOrClassName, $this->providerClassNames, true); | ||
} | ||
|
||
/** | ||
* Gets the class name for a registered provider. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param string $id The provider ID. | ||
* @return string The provider class name. | ||
* @throws InvalidArgumentException If the provider is not registered. | ||
*/ | ||
public function getProviderClassName(string $id): string | ||
{ | ||
if (!isset($this->providerClassNames[$id])) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider not registered: %s', $id) | ||
); | ||
} | ||
|
||
return $this->providerClassNames[$id]; | ||
} | ||
|
||
/** | ||
* Checks if a provider is properly configured. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. | ||
* @return bool True if the provider is configured and ready to use. | ||
*/ | ||
public function isProviderConfigured(string $idOrClassName): bool | ||
{ | ||
try { | ||
$className = $this->resolveProviderClassName($idOrClassName); | ||
|
||
// Use static method from ProviderInterface | ||
/** @var class-string<ProviderInterface> $className */ | ||
$availability = $className::availability(); | ||
|
||
return $availability->isConfigured(); | ||
} catch (InvalidArgumentException $e) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Finds models across all providers that support the given requirements. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param ModelRequirements $modelRequirements The requirements to match against. | ||
* @return list<ProviderModelsMetadata> List of provider models metadata that match requirements. | ||
*/ | ||
public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array | ||
{ | ||
$results = []; | ||
|
||
foreach ($this->providerClassNames as $providerId => $className) { | ||
$providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements); | ||
if (!empty($providerResults)) { | ||
// Use static method from ProviderInterface | ||
/** @var class-string<ProviderInterface> $className */ | ||
$providerMetadata = $className::metadata(); | ||
|
||
$results[] = new ProviderModelsMetadata( | ||
$providerMetadata, | ||
$providerResults | ||
); | ||
} | ||
} | ||
|
||
return $results; | ||
} | ||
|
||
/** | ||
* Finds models within a specific provider that support the given requirements. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param string $idOrClassName The provider ID or class name. | ||
* @param ModelRequirements $modelRequirements The requirements to match against. | ||
* @return list<ModelMetadata> List of model metadata that match requirements. | ||
*/ | ||
public function findProviderModelsMetadataForSupport( | ||
string $idOrClassName, | ||
ModelRequirements $modelRequirements | ||
): array { | ||
$className = $this->resolveProviderClassName($idOrClassName); | ||
|
||
$modelMetadataDirectory = $className::modelMetadataDirectory(); | ||
|
||
// Filter models that meet requirements | ||
$matchingModels = []; | ||
foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) { | ||
if ($modelMetadata->meetsRequirements($modelRequirements)) { | ||
$matchingModels[] = $modelMetadata; | ||
} | ||
} | ||
|
||
return $matchingModels; | ||
} | ||
|
||
/** | ||
* Gets a configured model instance from a provider. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. | ||
* @param string $modelId The model identifier. | ||
* @param ModelConfig|null $modelConfig The model configuration. | ||
* @return ModelInterface The configured model instance. | ||
* @throws InvalidArgumentException If provider or model is not found. | ||
*/ | ||
public function getProviderModel( | ||
string $idOrClassName, | ||
string $modelId, | ||
?ModelConfig $modelConfig = null | ||
): ModelInterface { | ||
$className = $this->resolveProviderClassName($idOrClassName); | ||
|
||
// Use static method from ProviderInterface | ||
/** @var class-string<ProviderInterface> $className */ | ||
return $className::model($modelId, $modelConfig); | ||
} | ||
|
||
/** | ||
* Gets the class name for a registered provider (handles both ID and class name input). | ||
* | ||
* @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name. | ||
* @return class-string<ProviderInterface> The provider class name. | ||
* @throws InvalidArgumentException If provider is not registered. | ||
*/ | ||
private function resolveProviderClassName(string $idOrClassName): string | ||
{ | ||
// Handle both ID and class name | ||
$className = $this->providerClassNames[$idOrClassName] ?? $idOrClassName; | ||
|
||
if (!$this->hasProvider($idOrClassName)) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider not registered: %s', $idOrClassName) | ||
); | ||
} | ||
|
||
// Validate that class implements ProviderInterface (for PHPStan type safety) | ||
if (!is_subclass_of($className, ProviderInterface::class)) { | ||
throw new InvalidArgumentException( | ||
sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className) | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unnecessary. It's impossible for If PHPStan complains about this, we should rather ignore that because it's an invalid complaint I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @felixarntz resolved here f868e7b |
||
|
||
return $className; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace WordPress\AiClient\Tests\mocks; | ||
|
||
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelConfig; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; | ||
|
||
/** | ||
* Mock model for testing. | ||
* | ||
* @since n.e.x.t | ||
*/ | ||
class MockModel implements ModelInterface | ||
{ | ||
/** | ||
* @var ModelMetadata The model metadata. | ||
*/ | ||
private ModelMetadata $metadata; | ||
|
||
/** | ||
* @var ModelConfig The model configuration. | ||
*/ | ||
private ModelConfig $config; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
* @param ModelMetadata $metadata The model metadata. | ||
* @param ModelConfig $config The model configuration. | ||
*/ | ||
public function __construct(ModelMetadata $metadata, ModelConfig $config) | ||
{ | ||
$this->metadata = $metadata; | ||
$this->config = $config; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function metadata(): ModelMetadata | ||
{ | ||
return $this->metadata; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function getConfig(): ModelConfig | ||
{ | ||
return $this->config; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function setConfig(ModelConfig $config): void | ||
{ | ||
$this->config = $config; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace WordPress\AiClient\Tests\mocks; | ||
|
||
use InvalidArgumentException; | ||
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface; | ||
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; | ||
|
||
/** | ||
* Mock model metadata directory for testing. | ||
* | ||
* @since n.e.x.t | ||
*/ | ||
class MockModelMetadataDirectory implements ModelMetadataDirectoryInterface | ||
{ | ||
/** | ||
* @var array<string, ModelMetadata> Available models. | ||
*/ | ||
private array $models = []; | ||
|
||
/** | ||
* Constructor. | ||
* | ||
* @param array<string, ModelMetadata> $models Available models. | ||
*/ | ||
public function __construct(array $models = []) | ||
{ | ||
$this->models = $models; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function listModelMetadata(): array | ||
{ | ||
return array_values($this->models); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function hasModelMetadata(string $modelId): bool | ||
{ | ||
return isset($this->models[$modelId]); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function getModelMetadata(string $modelId): ModelMetadata | ||
{ | ||
if (!isset($this->models[$modelId])) { | ||
throw new InvalidArgumentException( | ||
sprintf('Model not found: %s', $modelId) | ||
); | ||
} | ||
|
||
return $this->models[$modelId]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in_array
is not as efficient as usingisset
. Since this method may be commonly called, I think it would be best to add another class property keyed by the provider class names. This way you can useisset
for the second check here too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved here 6e25240