Skip to content

Commit 2c8bfa2

Browse files
authored
Merge pull request #55 from Ref34t/feature/ai-client
Add the AiClient with testing suite
2 parents 7367980 + 09aa789 commit 2c8bfa2

File tree

4 files changed

+1225
-4
lines changed

4 files changed

+1225
-4
lines changed

src/AiClient.php

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WordPress\AiClient;
6+
7+
use WordPress\AiClient\Builders\PromptBuilder;
8+
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
9+
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
10+
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
11+
use WordPress\AiClient\Providers\ProviderRegistry;
12+
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
13+
14+
/**
15+
* Main AI Client class providing both fluent and traditional APIs for AI operations.
16+
*
17+
* This class serves as the primary entry point for AI operations, offering:
18+
* - Fluent API for easy-to-read chained method calls
19+
* - Traditional API for array-based configuration (WordPress style)
20+
* - Integration with provider registry for model discovery
21+
* - Support for three model specification approaches
22+
*
23+
* All model requirements analysis and capability matching is handled
24+
* automatically by the PromptBuilder, which provides intelligent model
25+
* discovery based on prompt content and configuration.
26+
*
27+
* ## Model Specification Approaches
28+
*
29+
* ### 1. Specific Model Instance
30+
* Use a specific ModelInterface instance when you know exactly which model to use:
31+
* ```php
32+
* $model = $registry->getProvider('openai')->getModel('gpt-4');
33+
* $result = AiClient::generateTextResult('What is PHP?', $model);
34+
* ```
35+
*
36+
* ### 2. ModelConfig for Auto-Discovery
37+
* Use ModelConfig to specify requirements and let the system discover the best model:
38+
* ```php
39+
* $config = new ModelConfig();
40+
* $config->setTemperature(0.7);
41+
* $config->setMaxTokens(150);
42+
*
43+
* $result = AiClient::generateTextResult('What is PHP?', $config);
44+
* ```
45+
*
46+
* ### 3. Automatic Discovery (Default)
47+
* Pass null or omit the parameter for intelligent model discovery based on prompt content:
48+
* ```php
49+
* // System analyzes prompt and selects appropriate model automatically
50+
* $result = AiClient::generateTextResult('What is PHP?');
51+
* $imageResult = AiClient::generateImageResult('A sunset over mountains');
52+
* ```
53+
*
54+
* ## Fluent API Examples
55+
* ```php
56+
* // Fluent API with automatic model discovery
57+
* $result = AiClient::prompt('Generate an image of a sunset')
58+
* ->usingTemperature(0.7)
59+
* ->generateImageResult();
60+
*
61+
* // Fluent API with specific model
62+
* $result = AiClient::prompt('What is PHP?')
63+
* ->usingModel($specificModel)
64+
* ->usingTemperature(0.5)
65+
* ->generateTextResult();
66+
*
67+
* // Fluent API with model configuration
68+
* $result = AiClient::prompt('Explain quantum physics')
69+
* ->usingModelConfig($config)
70+
* ->generateTextResult();
71+
* ```
72+
*
73+
* @since n.e.x.t
74+
*
75+
* @phpstan-import-type Prompt from PromptBuilder
76+
*
77+
* phpcs:ignore Generic.Files.LineLength.TooLong
78+
*/
79+
class AiClient
80+
{
81+
/**
82+
* @var ProviderRegistry|null The default provider registry instance.
83+
*/
84+
private static ?ProviderRegistry $defaultRegistry = null;
85+
86+
/**
87+
* Gets the default provider registry instance.
88+
*
89+
* @since n.e.x.t
90+
*
91+
* @return ProviderRegistry The default provider registry.
92+
*/
93+
public static function defaultRegistry(): ProviderRegistry
94+
{
95+
if (self::$defaultRegistry === null) {
96+
$registry = new ProviderRegistry();
97+
98+
// Provider registration will be enabled once concrete provider implementations are available.
99+
// This follows the pattern established in the provider registry architecture.
100+
//$registry->setHttpTransporter(HttpTransporterFactory::createTransporter());
101+
//$registry->registerProvider(AnthropicProvider::class);
102+
//$registry->registerProvider(GoogleProvider::class);
103+
//$registry->registerProvider(OpenAiProvider::class);
104+
105+
self::$defaultRegistry = $registry;
106+
}
107+
108+
return self::$defaultRegistry;
109+
}
110+
111+
/**
112+
* Checks if a provider is configured and available for use.
113+
*
114+
* @since n.e.x.t
115+
*
116+
* @param ProviderAvailabilityInterface $availability The provider availability instance to check.
117+
* @return bool True if the provider is configured and available, false otherwise.
118+
*/
119+
public static function isConfigured(ProviderAvailabilityInterface $availability): bool
120+
{
121+
return $availability->isConfigured();
122+
}
123+
124+
/**
125+
* Creates a new prompt builder for fluent API usage.
126+
*
127+
* Returns a PromptBuilder instance configured with the specified or default registry.
128+
* The traditional API methods in this class delegate to PromptBuilder
129+
* for all generation logic.
130+
*
131+
* @since n.e.x.t
132+
*
133+
* @param Prompt $prompt Optional initial prompt content.
134+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
135+
* @return PromptBuilder The prompt builder instance.
136+
*/
137+
public static function prompt($prompt = null, ?ProviderRegistry $registry = null): PromptBuilder
138+
{
139+
return new PromptBuilder($registry ?? self::defaultRegistry(), $prompt);
140+
}
141+
142+
/**
143+
* Generates content using a unified API that automatically detects model capabilities.
144+
*
145+
* When no model is provided, this method delegates to PromptBuilder for intelligent
146+
* model discovery based on prompt content and configuration. When a model is provided,
147+
* it infers the capability from the model's interfaces and delegates to the capability-based method.
148+
*
149+
* @since n.e.x.t
150+
*
151+
* @param Prompt $prompt The prompt content.
152+
* @param ModelInterface|ModelConfig $modelOrConfig Specific model to use, or model configuration
153+
* for auto-discovery.
154+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
155+
* @return GenerativeAiResult The generation result.
156+
*
157+
* @throws \InvalidArgumentException If the provided model doesn't support any known generation type.
158+
* @throws \RuntimeException If no suitable model can be found for the prompt.
159+
*/
160+
public static function generateResult(
161+
$prompt,
162+
$modelOrConfig,
163+
?ProviderRegistry $registry = null
164+
): GenerativeAiResult {
165+
self::validateModelOrConfigParameter($modelOrConfig);
166+
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateResult();
167+
}
168+
169+
/**
170+
* Generates text using the traditional API approach.
171+
*
172+
* @since n.e.x.t
173+
*
174+
* @param Prompt $prompt The prompt content.
175+
* @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
176+
* or model configuration for auto-discovery,
177+
* or null for defaults.
178+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
179+
* @return GenerativeAiResult The generation result.
180+
*
181+
* @throws \InvalidArgumentException If the prompt format is invalid.
182+
* @throws \RuntimeException If no suitable model is found.
183+
*/
184+
public static function generateTextResult(
185+
$prompt,
186+
$modelOrConfig = null,
187+
?ProviderRegistry $registry = null
188+
): GenerativeAiResult {
189+
self::validateModelOrConfigParameter($modelOrConfig);
190+
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateTextResult();
191+
}
192+
193+
194+
/**
195+
* Generates an image using the traditional API approach.
196+
*
197+
* @since n.e.x.t
198+
*
199+
* @param Prompt $prompt The prompt content.
200+
* @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
201+
* or model configuration for auto-discovery,
202+
* or null for defaults.
203+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
204+
* @return GenerativeAiResult The generation result.
205+
*
206+
* @throws \InvalidArgumentException If the prompt format is invalid.
207+
* @throws \RuntimeException If no suitable model is found.
208+
*/
209+
public static function generateImageResult(
210+
$prompt,
211+
$modelOrConfig = null,
212+
?ProviderRegistry $registry = null
213+
): GenerativeAiResult {
214+
self::validateModelOrConfigParameter($modelOrConfig);
215+
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateImageResult();
216+
}
217+
218+
/**
219+
* Converts text to speech using the traditional API approach.
220+
*
221+
* @since n.e.x.t
222+
*
223+
* @param Prompt $prompt The prompt content.
224+
* @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
225+
* or model configuration for auto-discovery,
226+
* or null for defaults.
227+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
228+
* @return GenerativeAiResult The generation result.
229+
*
230+
* @throws \InvalidArgumentException If the prompt format is invalid.
231+
* @throws \RuntimeException If no suitable model is found.
232+
*/
233+
public static function convertTextToSpeechResult(
234+
$prompt,
235+
$modelOrConfig = null,
236+
?ProviderRegistry $registry = null
237+
): GenerativeAiResult {
238+
self::validateModelOrConfigParameter($modelOrConfig);
239+
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->convertTextToSpeechResult();
240+
}
241+
242+
/**
243+
* Generates speech using the traditional API approach.
244+
*
245+
* @since n.e.x.t
246+
*
247+
* @param Prompt $prompt The prompt content.
248+
* @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
249+
* or model configuration for auto-discovery,
250+
* or null for defaults.
251+
* @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
252+
* @return GenerativeAiResult The generation result.
253+
*
254+
* @throws \InvalidArgumentException If the prompt format is invalid.
255+
* @throws \RuntimeException If no suitable model is found.
256+
*/
257+
public static function generateSpeechResult(
258+
$prompt,
259+
$modelOrConfig = null,
260+
?ProviderRegistry $registry = null
261+
): GenerativeAiResult {
262+
self::validateModelOrConfigParameter($modelOrConfig);
263+
return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateSpeechResult();
264+
}
265+
266+
/**
267+
* Creates a new message builder for fluent API usage.
268+
*
269+
* This method will be implemented once MessageBuilder is available.
270+
* MessageBuilder will provide a fluent interface for constructing complex
271+
* messages with multiple parts, attachments, and metadata.
272+
*
273+
* @since n.e.x.t
274+
*
275+
* @param string|null $text Optional initial message text.
276+
* @return object MessageBuilder instance (type will be updated when MessageBuilder is available).
277+
*
278+
* @throws \RuntimeException When MessageBuilder is not yet available.
279+
*/
280+
public static function message(?string $text = null)
281+
{
282+
throw new \RuntimeException(
283+
'MessageBuilder is not yet available. This method depends on builder infrastructure. ' .
284+
'Use direct generation methods (generateTextResult, generateImageResult, etc.) for now.'
285+
);
286+
}
287+
288+
/**
289+
* Validates that parameter is ModelInterface, ModelConfig, or null.
290+
*
291+
* @param mixed $modelOrConfig The parameter to validate.
292+
* @return void
293+
* @throws \InvalidArgumentException If parameter is invalid type.
294+
*/
295+
private static function validateModelOrConfigParameter($modelOrConfig): void
296+
{
297+
if (
298+
$modelOrConfig !== null
299+
&& !$modelOrConfig instanceof ModelInterface
300+
&& !$modelOrConfig instanceof ModelConfig
301+
) {
302+
throw new \InvalidArgumentException(
303+
'Parameter must be a ModelInterface instance (specific model), ' .
304+
'ModelConfig instance (for auto-discovery), or null (default auto-discovery). ' .
305+
sprintf('Received: %s', is_object($modelOrConfig) ? get_class($modelOrConfig) : gettype($modelOrConfig))
306+
);
307+
}
308+
}
309+
310+
/**
311+
* Configures PromptBuilder based on model/config parameter type.
312+
*
313+
* @param Prompt $prompt The prompt content.
314+
* @param ModelInterface|ModelConfig|null $modelOrConfig The model or config parameter.
315+
* @param ProviderRegistry|null $registry Optional custom registry to use.
316+
* @return PromptBuilder Configured prompt builder.
317+
*/
318+
private static function getConfiguredPromptBuilder(
319+
$prompt,
320+
$modelOrConfig,
321+
?ProviderRegistry $registry = null
322+
): PromptBuilder {
323+
$builder = self::prompt($prompt, $registry);
324+
325+
if ($modelOrConfig instanceof ModelInterface) {
326+
$builder->usingModel($modelOrConfig);
327+
} elseif ($modelOrConfig instanceof ModelConfig) {
328+
$builder->usingModelConfig($modelOrConfig);
329+
}
330+
// null case: use default model discovery
331+
332+
return $builder;
333+
}
334+
}

0 commit comments

Comments
 (0)