Skip to content

Commit b0cb59d

Browse files
committed
Add AIFactory and AI class. Update doc accordingly
1 parent d4503de commit b0cb59d

File tree

9 files changed

+310
-27
lines changed

9 files changed

+310
-27
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ Each provider is instantiated with its configuration (API key, defaults such as
4343

4444
### OpenAI:
4545
```php
46-
use Joomla\AI\Provider\OpenAIProvider;
46+
use Joomla\AI\AIFactory;
4747

48-
$openai = new OpenAIProvider([
48+
$openai = AIFactory::getAI('openai', [
4949
'api_key' => getenv('OPENAI_API_KEY'),
5050
// Optional defaults:
5151
// 'model' => 'gpt-4o',
@@ -55,19 +55,19 @@ $openai = new OpenAIProvider([
5555

5656
### Anthropic:
5757
```php
58-
use Joomla\AI\Provider\AnthropicProvider;
58+
use Joomla\AI\AIFactory;
5959

60-
$anthropic = new AnthropicProvider([
60+
$anthropic = AIFactory::getAI('anthropic', [
6161
'api_key' => getenv('ANTHROPIC_API_KEY'),
6262
// 'model' => 'claude-3-5-sonnet',
6363
]);
6464
```
6565

6666
### Ollama (local):
6767
```php
68-
use Joomla\AI\Provider\OllamaProvider;
68+
use Joomla\AI\AIFactory;
6969

70-
$ollama = new OllamaProvider([
70+
$ollama = AIFactory::getAI('ollama', [
7171
// 'base_url' => 'http://localhost:11434',
7272
// 'model' => 'llama3',
7373
]);

Tests/FactoryTest.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
require_once '../vendor/autoload.php';
4+
5+
use Joomla\AI\AIFactory;
6+
use Joomla\AI\Exception\ProviderException;
7+
8+
$configFile = __DIR__ . '/../config.json';
9+
$config = json_decode(file_get_contents($configFile), true);
10+
$api_key = $config['openai_api_key'] ?? null;
11+
$anthropic_api_key = $config['anthropic_api_key'] ?? null;
12+
13+
echo "=== AI Factory Test Suite ===\n\n";
14+
15+
// Test Case 1: Invalid Provider
16+
echo "1. Testing invalid provider 'abcd':\n";
17+
try {
18+
$options = [
19+
'api_key' => $anthropic_api_key
20+
];
21+
22+
$ai = AIFactory::getAI('abcd', $options);
23+
$response = $ai->chat("Hey");
24+
echo $response->getContent();
25+
26+
} catch (ProviderException $e) {
27+
echo "Caught expected exception: " . $e->getMessage() . "\n";
28+
}
29+
echo "\n";
30+
31+
// Test Case 2: Valid Provider Creation
32+
echo "2. Testing valid provider creation (anthropic):\n";
33+
try {
34+
$options = [
35+
'api_key' => $anthropic_api_key
36+
];
37+
38+
$ai = AIFactory::getAI('anthropic', $options);
39+
echo "Provider name: " . $ai->getProvider()->getName() . "\n";
40+
$response = $ai->chat("Hey");
41+
echo $response->getContent();
42+
} catch (Exception $e) {
43+
echo "Failed to create Anthropic provider: " . $e->getMessage() . "\n";
44+
}
45+
echo "\n";
46+
47+
// Test Case 3: Non-existent Method Call
48+
echo "3. Testing non-existent method call:\n";
49+
try {
50+
$options = [
51+
'api_key' => $anthropic_api_key
52+
];
53+
54+
$ai = AIFactory::getAI('anthropic', $options);
55+
$response = $ai->nonExistentMethod("test");
56+
echo $response->getContent();
57+
} catch (ProviderException $e) {
58+
echo "Caught expected Exception for non-existent method: " . $e->getMessage() . "\n";
59+
}
60+
echo "\n";
61+
62+
// Test Case 4: Available Providers
63+
echo "4. Testing available providers:\n";
64+
try {
65+
$availableProviders = AIFactory::getAvailableProviders();
66+
echo "Available providers: " . implode(', ', $availableProviders) . "\n";
67+
68+
// Test each provider availability
69+
foreach ($availableProviders as $provider) {
70+
$isAvailable = AIFactory::isProviderAvailable($provider);
71+
echo "Provider '$provider' is available: " . ($isAvailable ? 'Yes' : 'No') . "\n";
72+
}
73+
74+
// Test non-existent provider
75+
$isAvailable = AIFactory::isProviderAvailable('non-existent');
76+
echo "Provider 'non-existent' is available: " . ($isAvailable ? 'Yes' : 'No') . "\n";
77+
} catch (Exception $e) {
78+
echo "Failed to get available providers: " . $e->getMessage() . "\n";
79+
}
80+
echo "\n";
81+
82+
// Test Case 5: Valid Method Call
83+
echo "5. Testing valid method calls:\n";
84+
try {
85+
$options = [
86+
'api_key' => $anthropic_api_key
87+
];
88+
89+
$ai = AIFactory::getAI('anthropic', $options);
90+
$response = $ai->chat("Hey");
91+
echo $response->getContent();
92+
} catch (Exception $e) {
93+
echo "Test Failed: " . $e->getMessage() . "\n";
94+
}
95+
96+
echo "\n=== Test Suite Complete ===\n";

docs/getting-started.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ You should not hardcode your API keys into your program code. Make them configur
5959
<?php
6060
require_once 'vendor/autoload.php';
6161

62-
use Joomla\AI\Provider\OpenAIProvider;
62+
use Joomla\AI\AIFactory;
6363

6464
// Create provider instance
65-
$openai = new OpenAIProvider([
65+
$openai = AIFactory::getAI('openai', [
6666
'api_key' => getenv('OPENAI_API_KEY')
6767
]);
6868

@@ -81,15 +81,13 @@ echo "Provider: " . $response->getProvider() . "\n";
8181
<?php
8282
require_once 'vendor/autoload.php';
8383

84-
use Joomla\AI\Provider\OpenAIProvider;
85-
use Joomla\AI\Provider\AnthropicProvider;
86-
use Joomla\AI\Provider\OllamaProvider;
84+
use Joomla\AI\AIFactory;
8785

8886
// Configure multiple providers
8987
$providers = [
90-
'openai' => new OpenAIProvider(['api_key' => getenv('OPENAI_API_KEY')]),
91-
'anthropic' => new AnthropicProvider(['api_key' => getenv('ANTHROPIC_API_KEY')]),
92-
'ollama' => new OllamaProvider() // Local server, no API key needed
88+
'openai' => $openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]),
89+
'anthropic' => nAIFactory::getAI('anthropic', ['api_key' => getenv('ANTHROPIC_API_KEY')]),
90+
'ollama' => AIFactory::getAI('ollama', []) // Local server, no API key needed
9391
];
9492

9593
$question = "What is Joomla?";
@@ -110,9 +108,9 @@ foreach ($providers as $name => $provider) {
110108
### Image Generation
111109

112110
```php
113-
use Joomla\AI\Provider\OpenAIProvider;
111+
use Joomla\AI\AIFactory;
114112

115-
$openai = new OpenAIProvider(['api_key' => getenv('OPENAI_API_KEY')]);
113+
$openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]);
116114

117115
// Generate an image
118116
$image = $openai->generateImage("A beautiful sunset over mountains", [
@@ -155,7 +153,7 @@ echo "Vision analysis: " . $vision->getContent() . "\n";
155153
Avoid repeating model names by setting defaults:
156154

157155
```php
158-
$openai = new OpenAIProvider(['api_key' => getenv('OPENAI_API_KEY')]);
156+
$openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]);
159157

160158
// Set a default model for all chat requests
161159
$openai->setDefaultModel('gpt-4o-mini');
@@ -212,9 +210,9 @@ For local AI without API costs:
212210
3. **Use in your code**:
213211

214212
```php
215-
use Joomla\AI\Provider\OllamaProvider;
213+
use Joomla\AI\AIFactory;
216214

217-
$ollama = new OllamaProvider();
215+
$ollama = AIFactory::getAI('ollama');
218216

219217
$response = $ollama->chat("Hello!", ['model' => 'llama3']);
220218
echo $response->getContent();

docs/overview.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ Centralizes common functionality:
3535
- Error mapping (401→Auth, 429→RateLimit/Quota, etc.)
3636
- JSON response parsing
3737

38+
**AI Factory** (`/src/AIFactory.php`)
39+
Centralized provider instantiation and management:
40+
- `getAI($provider, $options)` - Creates provider instances by name
41+
- Provider registry with supported providers ('openai', 'anthropic', 'ollama')
42+
- Simplifies provider switching and configuration management
43+
44+
**AI Class** (`/src/AI.php`)
45+
- Wrapper providing access to all AI capabilities.
46+
3847
**Response Object** (`/src/Response/Response.php`)
3948
Unified response wrapper that extends Joomla's HttpResponse:
4049
- `getContent()` - Primary result (text, base64 image, binary audio)
@@ -62,8 +71,8 @@ All exceptions inherit from [`AIException`](../src/Exception/AIException.php):
6271
### Provider Abstraction
6372
```php
6473
// Same interface, different providers
65-
$openai = new OpenAIProvider(['api_key' => $key]);
66-
$anthropic = new AnthropicProvider(['api_key' => $key]);
74+
$openai = AIFactory::getAI('openai', ['api_key' => $key]);
75+
$anthropic = AIFactory::getAI('anthropic', ['api_key' => $anthropic_key]);
6776

6877
// Identical usage
6978
$response1 = $openai->chat("Hello!");

docs/providers/anthropic.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ The Anthropic Provider ([`AnthropicProvider`](../../src/Provider/AnthropicProvid
1313
## Configuration
1414

1515
```php
16-
use Joomla\AI\Provider\AnthropicProvider;
16+
use Joomla\AI\AIFactory;
1717

18-
$anthropic = new AnthropicProvider([
18+
$anthropic = AIFactory::getAI('anthropic', [
1919
'api_key' => getenv('ANTHROPIC_API_KEY'),
2020
'model' => 'claude-3-5-sonnet' // Optional: default model
2121
]);

docs/providers/ollama.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ The Ollama Provider ([`OllamaProvider`](../../src/Provider/OllamaProvider.php))
1414
## Configuration
1515

1616
```php
17-
use Joomla\AI\Provider\OllamaProvider;
17+
use Joomla\AI\AIFactory;
1818

19-
$ollama = new OllamaProvider([
19+
$ollama = AIFactory::getAI('ollama', [
2020
'base_url' => 'http://localhost:11434', // Default Ollama server
2121
'model' => 'llama3' // Optional: default model
2222
]);

docs/providers/openai.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ The OpenAI Provider ([`OpenAIProvider`](../../src/Provider/OpenAIProvider.php))
2121
## Configuration
2222

2323
```php
24-
use Joomla\AI\Provider\OpenAIProvider;
24+
use Joomla\AI\AIFactory;
2525

26-
$openai = new OpenAIProvider([
26+
$openai = AIFactory::getAI('openai', [
2727
'api_key' => getenv('OPENAI_API_KEY'),
2828
'base_url' => 'https://api.openai.com/v1', // Optional: custom endpoint
2929
'model' => 'gpt-4o-mini' // Optional: default model

src/AI.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/**
4+
* Part of the Joomla Framework AI Package
5+
*
6+
* @copyright Copyright (C) 2025 Open Source Matters, Inc. All rights reserved.
7+
* @license GNU General Public License version 2 or later; see LICENSE
8+
*/
9+
10+
namespace Joomla\AI;
11+
12+
use Joomla\AI\Exception\ProviderException;
13+
14+
/**
15+
* AI client class.
16+
*
17+
* @since __DEPLOY_VERSION__
18+
*/
19+
class AI
20+
{
21+
/**
22+
* The AI provider object to use for AI requests.
23+
*
24+
* @var AbstractProvider
25+
* @since __DEPLOY_VERSION__
26+
*/
27+
protected AbstractProvider $provider;
28+
29+
/**
30+
* Constructor.
31+
*
32+
* @param AbstractProvider $provider The AI provider object.
33+
*
34+
* @since __DEPLOY_VERSION__
35+
*/
36+
public function __construct(AbstractProvider $provider)
37+
{
38+
$this->provider = $provider;
39+
}
40+
41+
/**
42+
* Magic method to delegate all calls to the provider.
43+
*
44+
* @param string $method The method name being called
45+
* @param array $arguments The arguments passed to the method
46+
*
47+
* @return mixed
48+
* @throws \ProviderException If the provider doesn't have the method
49+
* @since __DEPLOY_VERSION__
50+
*/
51+
public function __call(string $method, array $arguments)
52+
{
53+
if (method_exists($this->provider, $method)) {
54+
return $this->provider->$method(...$arguments);
55+
}
56+
57+
throw new ProviderException(
58+
$this->provider->getName(),
59+
['message' => 'Method ' . $method . ' is not supported by ' . $this->provider->getName()],
60+
);
61+
}
62+
63+
/**
64+
* Get the underlying provider instance.
65+
*
66+
* @return AbstractProvider
67+
* @since __DEPLOY_VERSION__
68+
*/
69+
public function getProvider(): AbstractProvider
70+
{
71+
return $this->provider;
72+
}
73+
}

0 commit comments

Comments
 (0)