Skip to content
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
d44cdad
feat: first draft of AiClient
Aug 15, 2025
634a18a
fix: reduce comment line length in test file
Aug 15, 2025
5fda968
fix: shorten final comment line for code style
Aug 15, 2025
5a964d9
Merge branch 'trunk' into feature/ai-client
Aug 17, 2025
282bcbe
Implement isConfigured() method in AiClient
Aug 17, 2025
8188228
Implement generateResult() unified generation method
Aug 17, 2025
b4cc84b
Implement streamGenerateTextResult() streaming support
Aug 17, 2025
f659d6f
Implement generateTextOperation() & generateImageOperation()
Aug 17, 2025
c52357d
Fix code style: Remove trailing whitespace
Aug 17, 2025
ca0d419
Implement text-to-speech conversion capabilities in AiClient
Aug 17, 2025
5b1d829
Implement speech generation for complete PromptBuilder readiness
Aug 17, 2025
68b4f17
Implement complete embedding generation infrastructure
Aug 17, 2025
69168ae
Build comprehensive embedding test infrastructure
Aug 17, 2025
a5d85b2
Resolve PHPStan type errors in embedding methods
Aug 17, 2025
9d5893d
Fix code style violations in embedding infrastructure
Aug 17, 2025
4ce660b
Add message() method placeholder for complete architecture compliance
Aug 17, 2025
948b3d5
Refactor AiClient utilities into separate classes for better maintain…
Aug 17, 2025
bccc41d
Fix PHPStan list type errors and remove redundant tests
Aug 17, 2025
23f287b
Implement comprehensive AiClient refactoring with utility classes
Aug 17, 2025
8cb7875
Fix PHPStan errors and PHP 7.4 compatibility issues
Aug 18, 2025
e5cf9c8
Refactor AiClient codebase with template method patterns and consolid…
Aug 18, 2025
b8db9ea
Refactor: organize the functionality
Aug 18, 2025
2c8c79d
Fix code style violations
Aug 18, 2025
b6a8722
Remove embeddings functionality from AiClient PR
Aug 19, 2025
99213f2
Fix test files after embeddings removal
Aug 19, 2025
e42d94d
Fix PSR-2 code style violations
Aug 19, 2025
d7e1203
Remove embeddings reference from comment in PromptNormalizer
Aug 19, 2025
b0b77d6
Remove unnecessary AiClientInterface
Aug 20, 2025
3593af1
Replace GenerationStrategyResolver with simple type checking
Aug 20, 2025
f3baee5
Fix trailing whitespace in AiClient
Aug 20, 2025
c75f01a
Add phpstan-assert annotations to eliminate ignore comments
Aug 20, 2025
79f4eeb
Consolidate InterfaceValidator and ModelDiscovery into Models utility
Aug 20, 2025
8ee4a1f
Fix whitespace and remove unused imports in Models utility
Aug 20, 2025
b9c4aa6
Simplify PromptNormalizer with cleaner loop-based approach
Aug 20, 2025
ce4c14a
Fix nullable type annotations and prepare for PromptBuilder integration
Aug 20, 2025
3612e5f
Build simplified PromptNormalizer using existing fromArray() methods
Aug 20, 2025
06509fd
Add @covers annotation to ModelsTest class
Aug 20, 2025
b8e689b
Fix PHPStan type errors in PromptNormalizer
Aug 20, 2025
3b00c26
Add phpstan-assert-if-true annotations for type safety
Aug 20, 2025
5a3bde7
Follow MessageUtil pattern for PHPStan type handling
Aug 20, 2025
34a682b
Fix code style issues in test files
Aug 20, 2025
569f6f9
Add missing @covers annotations to ModelsTest methods
Aug 20, 2025
c044204
Fix remaining long lines in test files
Aug 20, 2025
29097f2
Add not implemented exception for streamGenerateTextResult method
Aug 20, 2025
aecd838
Update streaming tests to expect not implemented exception
Aug 20, 2025
fa10649
refactor: prevents operation calls at this time
JasonTheAdams Aug 20, 2025
ab67b8a
refactor: cleans up normalizer typing
JasonTheAdams Aug 20, 2025
abd171b
refactor: simplifies noramlization array checking
JasonTheAdams Aug 20, 2025
bdb86eb
feat: broadens prompt shape and simplifies normalizing
JasonTheAdams Aug 20, 2025
94c0e4e
test: fixes tests broken by changing normalizing to single Message
JasonTheAdams Aug 20, 2025
76d39f5
Merge branch 'trunk' into feature/ai-client
felixarntz Aug 26, 2025
4ed97f3
Use PromptBuilder and fully set up default registry.
felixarntz Aug 26, 2025
31e8fb2
Merge branch 'trunk' into feature/ai-client
felixarntz Aug 26, 2025
2e19324
Consistently make ModelInterface optional.
felixarntz Aug 27, 2025
9bdc681
Fix some bugs.
felixarntz Aug 27, 2025
109d0f3
refactor: traditional API methods to delegate to PromptBuilder
Aug 27, 2025
077cc8d
fix: style improvements
Aug 27, 2025
587489c
Add utility classes documentation to AiClient
Aug 27, 2025
c7ee758
fix: resolve AiClient critical issues and improve MVP compliance
Ref34t Aug 27, 2025
bdc644d
fix: resolve test failures and static analysis issues
Ref34t Aug 27, 2025
0d4c6de
fix: resolve test failures and code style violations
Aug 27, 2025
38db3f9
refactor: remove premature methods and consolidate AiClient logic
Aug 27, 2025
b950e57
refactor: adopt repository testing patterns for AiClient tests
Aug 27, 2025
f638664
Merge branch 'trunk' into feature/ai-client
felixarntz Aug 27, 2025
70d0bb2
Merge branch 'feature/ai-client' of github.com:Ref34t/php-ai-client i…
felixarntz Aug 27, 2025
1ed0ca0
implement comprehensive AiClient improvements and ModelConfig paramet…
Aug 28, 2025
f4b3174
docs: update outdated PromptBuilder integration comment
Aug 28, 2025
ad23c59
implement dependency injection pattern for registry management
Aug 28, 2025
9f7424f
fix code style issues: line length and whitespace
Aug 28, 2025
0130294
fix function declaration brace formatting
Aug 28, 2025
4e86daa
refactor: renames method to getConfiguredPromptBuilder
JasonTheAdams Aug 28, 2025
7d007d1
test: removes getConfiguredPromptBuilder tests
JasonTheAdams Aug 28, 2025
403e3f3
feat: infers prompt capability from model if provided
JasonTheAdams Aug 28, 2025
09aa789
refactor: simplifies generateResult to rely on PromptBuilder
JasonTheAdams Aug 28, 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
373 changes: 373 additions & 0 deletions src/AiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient;

use WordPress\AiClient\Builders\PromptBuilder;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;

/**
* Main AI Client class providing both fluent and traditional APIs for AI operations.
*
* This class serves as the primary entry point for AI operations, offering:
* - Fluent API for easy-to-read chained method calls
* - Traditional API for array-based configuration (WordPress style)
* - Integration with provider registry for model discovery
*
* All model requirements analysis and capability matching is handled
* automatically by the PromptBuilder, which provides intelligent model
* discovery based on prompt content and configuration.
*
* Example usage:
* ```php
* // Fluent API with automatic model discovery
* $result = AiClient::prompt('Generate an image of a sunset')
* ->usingTemperature(0.7)
* ->generateImageResult();
*
* // Traditional API
* $result = AiClient::generateTextResult('What is PHP?');
* ```
*
* @since n.e.x.t
*
* @phpstan-import-type Prompt from PromptBuilder
*
* phpcs:ignore Generic.Files.LineLength.TooLong
*/
class AiClient
{
/**
* @var ProviderRegistry|null The default provider registry instance.
*/
private static ?ProviderRegistry $defaultRegistry = null;

/**
* Gets the default provider registry instance.
*
* @since n.e.x.t
*
* @return ProviderRegistry The default provider registry.
*/
public static function defaultRegistry(): ProviderRegistry
{
if (self::$defaultRegistry === null) {
$registry = new ProviderRegistry();

// TODO: Uncomment this once provider implementation PR #39 is merged.
//$registry->setHttpTransporter(HttpTransporterFactory::createTransporter());
//$registry->registerProvider(AnthropicProvider::class);
//$registry->registerProvider(GoogleProvider::class);
//$registry->registerProvider(OpenAiProvider::class);

self::$defaultRegistry = $registry;
}

return self::$defaultRegistry;
}

/**
* Sets the default provider registry instance.
*
* @since n.e.x.t
*
* @param ProviderRegistry $registry The provider registry to set as default.
*/
public static function setDefaultRegistry(ProviderRegistry $registry): void
{
self::$defaultRegistry = $registry;
}

/**
* Checks if a provider is configured and available for use.
*
* @since n.e.x.t
*
* @param ProviderAvailabilityInterface $availability The provider availability instance to check.
* @return bool True if the provider is configured and available, false otherwise.
*/
public static function isConfigured(ProviderAvailabilityInterface $availability): bool
{
return $availability->isConfigured();
}

/**
* Creates a new prompt builder for fluent API usage.
*
* This method will return an actual PromptBuilder instance once PR #49 is merged.
* The traditional API methods in this class will then delegate to PromptBuilder
* rather than implementing their own generation logic.
*
* @since n.e.x.t
*
* @param Prompt $prompt Optional initial prompt content.
* @return PromptBuilder The prompt builder instance.
*/
public static function prompt($prompt = null): PromptBuilder
{
return new PromptBuilder(self::defaultRegistry(), $prompt);
}

/**
* Generates content using a unified API that automatically detects model capabilities.
*
* When no model is provided, this method delegates to PromptBuilder for intelligent
* model discovery based on prompt content and configuration. When a model is provided,
* it infers the capability from the model's interfaces and delegates to the capability-based method.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the provided model doesn't support any known generation type.
* @throws \RuntimeException If no suitable model can be found for the prompt.
*/
public static function generateResult($prompt, ?ModelInterface $model = null): GenerativeAiResult
{
// If no model provided, use PromptBuilder's intelligent model discovery
if ($model === null) {
return self::prompt($prompt)->generateResult();
}

// Infer capability from model interface (priority order matters)
if ($model instanceof TextGenerationModelInterface) {
return self::generateResultWithCapability($prompt, CapabilityEnum::textGeneration(), $model);
}

if ($model instanceof ImageGenerationModelInterface) {
return self::generateResultWithCapability($prompt, CapabilityEnum::imageGeneration(), $model);
}

if ($model instanceof TextToSpeechConversionModelInterface) {
return self::generateResultWithCapability($prompt, CapabilityEnum::textToSpeechConversion(), $model);
}

if ($model instanceof SpeechGenerationModelInterface) {
return self::generateResultWithCapability($prompt, CapabilityEnum::speechGeneration(), $model);
}

throw new \InvalidArgumentException(
sprintf(
'Model "%s" must implement at least one supported generation interface ' .
'(TextGeneration, ImageGeneration, TextToSpeechConversion, SpeechGeneration)',
$model->metadata()->getId()
)
);
Copy link
Member

@felixarntz felixarntz Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should always rely on PromptBuilder here, even if a ModelInterface is provided:

  • If the second parameter is a ModelConfig, we need to pass it to PromptBuilder::usingModelConfig.
  • If a ModelInterface is provided, we need to call PromptBuilder::usingModel before calling PromptBuilder::generateResult.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this to work like the other methods where it simply relies on the PromptBuilder to figure things out. Happily, it did point out a bug in the builder where it didn't infer the capability from the model if provided, which was resolved in 403e3f3.

}

/**
* Generates content using a unified API with explicit capability selection.
*
* This method allows explicit capability selection for models that implement
* multiple generation interfaces. If the model doesn't support the specified
* capability, an exception is thrown.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param CapabilityEnum $capability The desired generation capability.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the model doesn't support the specified capability.
* @throws \RuntimeException If no suitable model can be found for the prompt and capability.
*/
public static function generateResultWithCapability(
$prompt,
CapabilityEnum $capability,
?ModelInterface $model = null
): GenerativeAiResult {
// If no model provided, use PromptBuilder with explicit capability
if ($model === null) {
return self::prompt($prompt)->generateResult($capability);
}

// Validate that the model supports the requested capability
$supportedCapabilities = $model->metadata()->getSupportedCapabilities();
$supportsCapability = false;
foreach ($supportedCapabilities as $supportedCapability) {
if ($supportedCapability->equals($capability)) {
$supportsCapability = true;
break;
}
}

if (!$supportsCapability) {
throw new \InvalidArgumentException(
sprintf(
'Model "%s" does not support the "%s" capability',
$model->metadata()->getId(),
$capability->value
)
);
}

// Route to the appropriate method based on capability
if ($capability->isTextGeneration()) {
return self::generateTextResult($prompt, $model);
}

if ($capability->isImageGeneration()) {
return self::generateImageResult($prompt, $model);
}

if ($capability->isTextToSpeechConversion()) {
return self::convertTextToSpeechResult($prompt, $model);
}

if ($capability->isSpeechGeneration()) {
return self::generateSpeechResult($prompt, $model);
}

throw new \InvalidArgumentException(
sprintf('Capability "%s" is not yet supported for generation', $capability->value)
);
}

/**
* Creates a new message builder for fluent API usage.
*
* This method will be implemented once MessageBuilder is available.
* MessageBuilder will provide a fluent interface for constructing complex
* messages with multiple parts, attachments, and metadata.
*
* @since n.e.x.t
*
* @param string|null $text Optional initial message text.
* @return object MessageBuilder instance (type will be updated when MessageBuilder is available).
*
* @throws \RuntimeException When MessageBuilder is not yet available.
*/
public static function message(?string $text = null)
{
throw new \RuntimeException(
'MessageBuilder is not yet available. This method depends on builder infrastructure. ' .
'Use direct generation methods (generateTextResult, generateImageResult, etc.) for now.'
);
}

/**
* Generates text using the traditional API approach.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the prompt format is invalid.
* @throws \RuntimeException If no suitable model is found.
*/
public static function generateTextResult($prompt, ?ModelInterface $model = null): GenerativeAiResult
{
$builder = self::prompt($prompt);
if ($model !== null) {
$builder->usingModel($model);
}
return $builder->generateTextResult();
}


/**
* Generates an image using the traditional API approach.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the prompt format is invalid.
* @throws \RuntimeException If no suitable model is found.
*/
public static function generateImageResult($prompt, ?ModelInterface $model = null): GenerativeAiResult
{
$builder = self::prompt($prompt);
if ($model !== null) {
$builder->usingModel($model);
}
return $builder->generateImageResult();
}

/**
* Converts text to speech using the traditional API approach.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the prompt format is invalid.
* @throws \RuntimeException If no suitable model is found.
*/
public static function convertTextToSpeechResult($prompt, ?ModelInterface $model = null): GenerativeAiResult
{
$builder = self::prompt($prompt);
if ($model !== null) {
$builder->usingModel($model);
}
return $builder->convertTextToSpeechResult();
}

/**
* Generates speech using the traditional API approach.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return GenerativeAiResult The generation result.
*
* @throws \InvalidArgumentException If the prompt format is invalid.
* @throws \RuntimeException If no suitable model is found.
*/
public static function generateSpeechResult($prompt, ?ModelInterface $model = null): GenerativeAiResult
{
$builder = self::prompt($prompt);
if ($model !== null) {
$builder->usingModel($model);
}
return $builder->generateSpeechResult();
}


/**
* Convenience method for text generation.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return string The generated text.
*/
public static function generateText($prompt, ?ModelInterface $model = null): string
{
return self::generateTextResult($prompt, $model)->toText();
}

/**
* Convenience method for image generation.
*
* @since n.e.x.t
*
* @param Prompt $prompt The prompt content.
* @param ModelInterface|null $model Optional specific model to use.
* @return \WordPress\AiClient\Files\DTO\File The generated image file.
*/
public static function generateImage($prompt, ?ModelInterface $model = null)
{
return self::generateImageResult($prompt, $model)->toFile();
}
}
3 changes: 1 addition & 2 deletions src/Builders/PromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ class PromptBuilder
* @since n.e.x.t
*
* @param ProviderRegistry $registry The provider registry for finding suitable models.
* @param Prompt $prompt
* Optional initial prompt content.
* @param Prompt $prompt Optional initial prompt content.
*/
// phpcs:enable Generic.Files.LineLength.TooLong
public function __construct(ProviderRegistry $registry, $prompt = null)
Expand Down
Loading