diff --git a/README.md b/README.md index e69de29..a7f047e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,118 @@ +# The Joomla! AI Package + +## Installation via Composer + +Add `"joomla/ai": "~4.0"` to the require block in your composer.json and then run `composer install`. + +```json +{ + "require": { + "joomla/ai": "~4.0" + } +} +``` + +Alternatively, you can simply run the following from the command line: + +```sh +composer require joomla/ai "~4.0" +``` + +If you want to include the test sources and docs, use + +```sh +composer require --prefer-source joomla/ai "~4.0" +``` + +## Using the AI Framework +The AI Framework provides a straightforward, provider-agnostic interface for working with three main AI providers: OpenAI, Anthropic and Ollama. Instead of writing separate code for each provider’s SDK, developers write once and simply switch providers by changing configuration. + +You can find the official provider APIs documentation here: +- OpenAI API: https://platform.openai.com/docs/api-reference +- Anthropic API: https://docs.anthropic.com/claude/docs +- Ollama API: https://github.com/ollama/ollama/blob/main/docs/api.md + +The AI framework is built upon the Http package which provides an easy way to consume URLs and web services in a transport independent way. Joomla\Http currently supports streams, sockets and cURL. The framework centralizes HTTP handling in the Abstract Provider. Providers encapsulate: +- Base URLs, headers, and auth (API keys/tokens) +- Request building (JSON/multipart) +- Response normalization and error mapping into framework exceptions + +## Instantiating a Provider + +Each provider is instantiated with its configuration (API key, defaults such as model or base URL). You can override these defaults per call when needed. + +### OpenAI: +```php +use Joomla\AI\AIFactory; + +$openai = AIFactory::getAI('openai', [ + 'api_key' => getenv('OPENAI_API_KEY'), + // Optional defaults: + // 'model' => 'gpt-4o', + // 'base_url' => 'https://api.openai.com/v1', +]); +``` + +### Anthropic: +```php +use Joomla\AI\AIFactory; + +$anthropic = AIFactory::getAI('anthropic', [ + 'api_key' => getenv('ANTHROPIC_API_KEY'), + // 'model' => 'claude-3-5-sonnet', +]); +``` + +### Ollama (local): +```php +use Joomla\AI\AIFactory; + +$ollama = AIFactory::getAI('ollama', [ + // 'base_url' => 'http://localhost:11434', + // 'model' => 'llama3', +]); +``` + +## Supported Methods + +| Provider | Methods | +| --- | --- | +| OpenAI | `chat`, `vision`, `generateImage`, `createImageVariation`, `editImage`, `speech`, `transcribe`, `translate`, `createEmbeddings`, `moderate`, `isContentFlagged`| +| Anthropic | `chat`, `vision`, `getModel`| +| Ollama | `chat`, `generate`, `pullModel`, `copyModel`, `deleteModel`, `checkModelExists`, `getRunningModels`| + +Not all providers implement every capability. The framework exposes capabilities via interfaces (e.g. ChatInterface, ImageInterface). Developers can use what each provider supports. + +## Making your first request +All providers implement a shared set of capability interfaces (e.g., Chat, Images, Audio). Invoke these methods directly, passing per-call options to override defaults. + +```php +// Chat example (OpenAI) +$response = $openai->chat("Write a haiku about Joomla.", [ + 'model' => 'gpt-4o-mini', // overrides constructor default if set +]); +echo $response->getContent(); // primary content (e.g. text) +$meta = $response->getMetadata(); // metadata content (e.g. model, usage) +``` + +## Error handling +Provider HTTP errors are mapped to framework exceptions (e.g. auth, rate limit, invalid arguments). Catch and handle them as needed. +```php +try { + $response = $openai->chat("Hello!"); +} catch (\Throwable $e) { + // Log and surface a friendly message +} +``` + +## Documentation + +- **[Overview](docs/overview.md)** + Architecture & goals + +- **[Getting Started](docs/getting-started.md)** + Install, configure, first provider + +- **[Guides](providers/)** + Provider-specific guides with code examples +--- diff --git a/Tests/FactoryTest.php b/Tests/FactoryTest.php new file mode 100644 index 0000000..d6eaedb --- /dev/null +++ b/Tests/FactoryTest.php @@ -0,0 +1,96 @@ + $anthropic_api_key + ]; + + $ai = AIFactory::getAI('abcd', $options); + $response = $ai->chat("Hey"); + echo $response->getContent(); + +} catch (ProviderException $e) { + echo "Caught expected exception: " . $e->getMessage() . "\n"; +} +echo "\n"; + +// Test Case 2: Valid Provider Creation +echo "2. Testing valid provider creation (anthropic):\n"; +try { + $options = [ + 'api_key' => $anthropic_api_key + ]; + + $ai = AIFactory::getAI('anthropic', $options); + echo "Provider name: " . $ai->getProvider()->getName() . "\n"; + $response = $ai->chat("Hey"); + echo $response->getContent(); +} catch (Exception $e) { + echo "Failed to create Anthropic provider: " . $e->getMessage() . "\n"; +} +echo "\n"; + +// Test Case 3: Non-existent Method Call +echo "3. Testing non-existent method call:\n"; +try { + $options = [ + 'api_key' => $anthropic_api_key + ]; + + $ai = AIFactory::getAI('anthropic', $options); + $response = $ai->nonExistentMethod("test"); + echo $response->getContent(); +} catch (ProviderException $e) { + echo "Caught expected Exception for non-existent method: " . $e->getMessage() . "\n"; +} +echo "\n"; + +// Test Case 4: Available Providers +echo "4. Testing available providers:\n"; +try { + $availableProviders = AIFactory::getAvailableProviders(); + echo "Available providers: " . implode(', ', $availableProviders) . "\n"; + + // Test each provider availability + foreach ($availableProviders as $provider) { + $isAvailable = AIFactory::isProviderAvailable($provider); + echo "Provider '$provider' is available: " . ($isAvailable ? 'Yes' : 'No') . "\n"; + } + + // Test non-existent provider + $isAvailable = AIFactory::isProviderAvailable('non-existent'); + echo "Provider 'non-existent' is available: " . ($isAvailable ? 'Yes' : 'No') . "\n"; +} catch (Exception $e) { + echo "Failed to get available providers: " . $e->getMessage() . "\n"; +} +echo "\n"; + +// Test Case 5: Valid Method Call +echo "5. Testing valid method calls:\n"; +try { + $options = [ + 'api_key' => $anthropic_api_key + ]; + + $ai = AIFactory::getAI('anthropic', $options); + $response = $ai->chat("Hey"); + echo $response->getContent(); +} catch (Exception $e) { + echo "Test Failed: " . $e->getMessage() . "\n"; +} + +echo "\n=== Test Suite Complete ===\n"; \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..2ddd2b7 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,257 @@ +# Getting Started + +This guide will help you install the AI Framework and make your first request in just a few minutes. + +## Requirements + +- PHP 8.3.0 or higher +- Composer +- API keys for the providers you want to use + +## Installation + +### Via Composer + +Add the AI package to your project: + +```json +{ + "require": { + "joomla/ai": "~4.0" + } +} +``` + +Then run: +```bash +composer install +``` + +Alternatively, install directly: +```bash +composer require joomla/ai "~4.0" +``` + +For development with test sources: +```bash +composer require --prefer-source joomla/ai "~4.0" +``` + +## Configuration + +### API Keys + +You will need API keys from the providers you want to use: + +- **OpenAI**: Get your key at https://platform.openai.com/api-keys +- **Anthropic**: Get your key at https://console.anthropic.com/ +- **Ollama**: Runs locally, no API key needed + +### Storing API Keys + +You should not hardcode your API keys into your program code. Make them configurable via for example a registry object or by storing them in environment variables. **Never commit API keys to version control.** + +## Your First Request + +### Basic Chat Example + +```php + getenv('OPENAI_API_KEY') +]); + +// Make your first chat request +$response = $openai->chat("Hello! Tell me about Joomla in one sentence."); + +// Display the result +echo "Response: " . $response->getContent() . "\n"; +echo "Model used: " . $response->getMetadata()['model'] . "\n"; +echo "Provider: " . $response->getProvider() . "\n"; +``` + +### Multi-Provider Example + +```php + $openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]), + 'anthropic' => nAIFactory::getAI('anthropic', ['api_key' => getenv('ANTHROPIC_API_KEY')]), + 'ollama' => AIFactory::getAI('ollama', []) // Local server, no API key needed +]; + +$question = "What is Joomla?"; + +foreach ($providers as $name => $provider) { + try { + $response = $provider->chat($question); + echo "\n=== {$name} ===\n"; + echo $response->getContent() . "\n"; + } catch (Exception $e) { + echo "{$name}: Error - " . $e->getMessage() . "\n"; + } +} +``` + +## Working with Different Capabilities + +### Image Generation + +```php +use Joomla\AI\AIFactory; + +$openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]); + +// Generate an image +$image = $openai->generateImage("A beautiful sunset over mountains", [ + 'model' => 'dall-e-3', + 'size' => '1024x1024', + 'response_format' => 'b64_json' +]); + +// Save the image +$image->saveFile('my_sunset.png'); +echo "Image saved as my_sunset.png\n"; +``` + +### Text-to-Speech + +```php +$audio = $openai->speech("Welcome to Joomla! This is a text-to-speech demo.", [ + 'model' => 'tts-1', + 'voice' => 'alloy', + 'response_format' => 'mp3' +]); + +$audio->saveFile('welcome.mp3'); +echo "Audio saved as welcome.mp3\n"; +``` + +### Vision (Image Analysis) + +```php +$vision = $openai->vision( + "What do you see in this image?", + "https://example.com/your-image.jpg" +); + +echo "Vision analysis: " . $vision->getContent() . "\n"; +``` + +## Setting Default Models + +Avoid repeating model names by setting defaults: + +```php +$openai = AIFactory::getAI('openai', ['api_key' => getenv('OPENAI_API_KEY')]); + +// Set a default model for all chat requests +$openai->setDefaultModel('gpt-4o-mini'); + +// These will use gpt-4o-mini +$response1 = $openai->chat("Hello!"); +$response2 = $openai->chat("How are you?"); + +// Override the default for one request +$response3 = $openai->chat("Complex task", ['model' => 'gpt-4o']); + +// Back to default +$response4 = $openai->chat("Simple task"); + +// Clear the default +$openai->unsetDefaultModel(); +``` + +## Error Handling + +The framework provides specific exceptions that are inherited from [`AIException`](../src/Exception/AIException.php): + +```php +use Joomla\AI\Exception\AuthenticationException; +use Joomla\AI\Exception\RateLimitException; +use Joomla\AI\Exception\QuotaExceededException; +use Joomla\AI\Exception\InvalidArgumentException; +use Joomla\AI\Exception\ProviderException; + +try { + $response = $openai->chat("Hello!"); + echo $response->getContent(); +} catch (AuthenticationException $e) { + echo "Authentication failed: Check your API key\n"; +} catch (RateLimitException $e) { + echo "Rate limited: Please wait before trying again\n"; +} catch (QuotaExceededException $e) { + echo "Quota exceeded: Check your billing\n"; +} catch (InvalidArgumentException $e) { + echo "Invalid input: " . $e->getMessage() . "\n"; +} catch (ProviderException $e) { + echo "Provider error: " . $e->getMessage() . "\n"; +} catch (Exception $e) { + echo "Unexpected error: " . $e->getMessage() . "\n"; +} +``` + +## Local Development with Ollama + +For local AI without API costs: + +1. **Install Ollama**: Download from https://ollama.ai/ +2. **Pull a model**: `ollama pull llama3` +3. **Use in your code**: + +```php +use Joomla\AI\AIFactory; + +$ollama = AIFactory::getAI('ollama'); + +$response = $ollama->chat("Hello!", ['model' => 'llama3']); +echo $response->getContent(); +``` + +## Response Object + +All methods return a unified [`Response`](../src/Response/Response.php) object: + +```php +$response = $provider->chat("Hello!"); + +// Primary content +echo $response->getContent(); + +// Provider metadata (model, usage, etc.) +$metadata = $response->getMetadata(); +print_r($metadata); + +// Provider name +echo $response->getProvider(); // "OpenAI", "Anthropic", or "Ollama" + +// Save files (for images, audio) +$response->saveFile('output.png'); +``` + +## Next Steps + +Now that you have the basics working: + +- **[Provider Guides](providers/)** - Provider-specific features and limitations + +## Troubleshooting + +**API Key Issues:** +- Ensure environment variables are set correctly +- Restart your terminal/web server after setting environment variables +- Check that your API keys have sufficient credits/permissions + +**Model Issues:** +- Verify the model name is correct and available +- Some models may require special access or billing setup diff --git a/docs/index.md b/docs/index.md index c87f1b4..f795495 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,5 @@ * [Overview](overview.md) +* [Getting Started](getting-started.md) +* [Open AI Provider](providers/openai.md) +* [Anthropic Provider](providers/anthropic.md) +* [Ollama Provider](providers/ollama.md) diff --git a/docs/overview.md b/docs/overview.md index 063f86f..5589ef2 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,3 +1,115 @@ ## Overview -The AI package provides an abstraction layer to access the services of several AI providers. +The AI package provides an abstraction layer to access the services of several AI providers. It provides a unified, provider-agnostic interface for integrating multiple AI services into your applications. Instead of learning different SDKs and handling varying response formats, you write code once and switch providers by changing configuration. + +Official provider API references: +- OpenAI: https://platform.openai.com/docs/api-reference +- Anthropic: https://docs.anthropic.com/claude/docs +- Ollama: https://github.com/ollama/ollama/blob/main/docs/api.md + +## Supported Providers + +| Provider | Chat | Vision | Images | Audio | Embeddings | Moderation | Models | +|----------|------|--------|---------|-------|------------|------------|---------| +| **OpenAI** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| **Anthropic** | ✓ | ✓ | - | - | - | - | ✓ | +| **Ollama** | ✓ | - | - | - | - | - | ✓ | + +## Architecture + +### Core Components + +**Interfaces** (`/src/Interface/`) +Define the interfaces that providers implement: +- [`ProviderInterface`](../src/Interface/ProviderInterface.php) - Base provider contract +- [`ChatInterface`](../src/Interface/ChatInterface.php) - Text conversation +- [`ImageInterface`](../src/Interface/ImageInterface.php) - Image generation, editing, variations +- [`AudioInterface`](../src/Interface/AudioInterface.php) - Speech synthesis, transcription, translation +- [`EmbeddingInterface`](../src/Interface/EmbeddingInterface.php) - Vector embeddings +- [`ModelInterface`](../src/Interface/ModelInterface.php) - Model management and capabilities +- [`ModerationInterface`](../src/Interface/ModerationInterface.php) - Content safety + +**Abstract Provider** (`/src/AbstractProvider.php`) +Centralizes common functionality: +- HTTP client management (GET, POST, DELETE, multipart) +- Error mapping (401→Auth, 429→RateLimit/Quota, etc.) +- JSON response parsing + +**AI Factory** (`/src/AIFactory.php`) +Centralized provider instantiation and management: +- `getAI($provider, $options)` - Creates provider instances by name +- Provider registry with supported providers ('openai', 'anthropic', 'ollama') +- Simplifies provider switching and configuration management + +**AI Class** (`/src/AI.php`) +- Wrapper providing access to all AI capabilities. + +**Response Object** (`/src/Response/Response.php`) +Unified response wrapper that extends Joomla's HttpResponse: +- `getContent()` - Primary result (text, base64 image, binary audio) +- `getMetadata()` - Normalized provider details (model, usage, formats) +- `getProvider()` - Provider name ("OpenAI", "Anthropic", "Ollama") +- `saveFile($path)` - Method to save the generated output + +**Providers** (`/src/Provider/`) +- [`OpenAIProvider`](../src/Provider/OpenAIProvider.php) - Implements methods related to OpenAI +- [`AnthropicProvider`](../src/Provider/AnthropicProvider.php) - Implements methods related to Anthropic +- [`OllamaProvider`](../src/Provider/OllamaProvider.php) - Implements methods related to Ollama + +### Exception Hierarchy + +All exceptions inherit from [`AIException`](../src/Exception/AIException.php): +- [`AuthenticationException`](../src/Exception/AuthenticationException.php) +- [`RateLimitException`](../src/Exception/RateLimitException.php) +- [`QuotaExceededException`](../src/Exception/QuotaExceededException.php) +- [`ProviderException`](../src/Exception/ProviderException.php) +- [`InvalidArgumentException`](../src/Exception/InvalidArgumentException.php) +- [`UnserializableResponseException`](../src/Exception/UnserializableResponseException.php) + +## Key Design Principles + +### Provider Abstraction +```php +// Same interface, different providers +$openai = AIFactory::getAI('openai', ['api_key' => $key]); +$anthropic = AIFactory::getAI('anthropic', ['api_key' => $anthropic_key]); + +// Identical usage +$response1 = $openai->chat("Hello!"); +$response2 = $anthropic->chat("Hello!"); +``` + +### Unified Response Format +```php +$response = $provider->chat("Hello!"); +echo $response->getContent(); // "Hello! How can I help you today?" +echo $response->getProvider(); // "OpenAI" +$metadata = $response->getMetadata(); +echo $metadata['model']; // "gpt-4o" + +``` + +### Default Model Management +Flexible precedence system for model selection with four levels of fallback: + +1. **Per-call `options['model']`** - Highest priority +2. **Provider default via `setDefaultModel()`** - Session-level default +3. **Constructor option `'model'`** - Provider-level configuration +4. **Method-specific fallback** - Built-in defaults per capability + +### File Handling +Streamlined output persistence: +```php +// Images +$image = $provider->generateImage("A sunset"); +$image->saveFile('sunset.png'); + +// Audio +$audio = $provider->speech("Hello world"); +$audio->saveFile('greeting.mp3'); +``` + +## Next Steps + +- **[Getting Started](getting-started.md)** - Installation and first requests +- **[Provider Guides](providers/)** - Provider-specific documentation diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md new file mode 100644 index 0000000..eb8bab0 --- /dev/null +++ b/docs/providers/anthropic.md @@ -0,0 +1,143 @@ +# Anthropic Provider + +The Anthropic Provider ([`AnthropicProvider`](../../src/Provider/AnthropicProvider.php)) focuses on conversational AI using Claude models, offering superior reasoning capabilities. + +## Capabilities Overview + +| Feature | Description | +|---------|-------------| +| **Chat** | Advanced text conversation with Claude models | +| **Vision** | Image analysis and description using Claude's vision capabilities | +| **Model Management** | List available models and retrieve model information | + +## Configuration + +```php +use Joomla\AI\AIFactory; + +$anthropic = AIFactory::getAI('anthropic', [ + 'api_key' => getenv('ANTHROPIC_API_KEY'), + 'model' => 'claude-3-5-sonnet' // Optional: default model +]); +``` + +## Chat Completions + +### Basic Chat +```php +$response = $anthropic->chat("Explain the advantages of Joomla over other CMS platforms."); +echo $response->getContent(); +``` + +### Advanced Chat with options +```php +$response = $anthropic->chat("How do I optimize database queries in Joomla?", [ + 'model' => 'claude-3-5-sonnet', + 'max_tokens' => 1000, + 'temperature' => 0.3 +]); + +$metadata = $response->getMetadata(); +echo "Model used: " . $metadata['model'] . "\n"; +echo "Tokens used: " . $metadata['usage']['input_tokens'] + $metadata['usage']['output_tokens'] . "\n"; +``` + +### Message History +```php +$response = $anthropic->chat("", [ + 'messages' => [ + ['role' => 'user', 'content' => 'What is Joomla?'], + ['role' => 'assistant', 'content' => 'Joomla is a free, open-source content management system...'], + ['role' => 'user', 'content' => 'How does it compare to WordPress?'] + ], + 'system' => 'You are a helpful web development consultant.' +]); + +echo $response->getContent(); +``` + +## Vision + +Analyze images with Claude's vision capabilities: + +### URL Image Analysis +```php +$response = $anthropic->vision( + "Analyze this website design and suggest improvements for user experience", + "https://example.com/website-screenshot.jpg" +); + +echo $response->getContent(); +``` + +### Base64 Image Analysis +```php +$imageData = base64_encode(file_get_contents('ui-mockup.png')); +$response = $anthropic->vision( + "Review this UI mockup and provide detailed feedback on the design principles", + "data:image/png;base64," . $imageData, + [ + 'model' => 'claude-3-5-sonnet', + 'max_tokens' => 1500 + ] +); + +echo $response->getContent(); +``` + +## Model Management + +### List Available Models +```php +$models = $anthropic->getAvailableModels(); +echo "Available Models: " . implode(', ', $models) . "\n"; +``` + +## Default Model Management + +Set defaults for consistent model usage: + +```php +// Set a default model +$anthropic->setDefaultModel('claude-3-5-sonnet'); + +// These will use the default +$chat1 = $anthropic->chat("What is web accessibility?"); +$vision1 = $anthropic->vision("Describe this image", $imageUrl); + +// Override for specific calls +$detailed = $anthropic->chat("Provide a comprehensive analysis", [ + 'model' => 'claude-3-opus', + 'max_tokens' => 2000 +]); + +// Clear the default +$anthropic->unsetDefaultModel(); +``` + +## Error Handling + +Anthropic-specific error patterns: + +```php +use Joomla\AI\Exception\AuthenticationException; +use Joomla\AI\Exception\RateLimitException; +use Joomla\AI\Exception\InvalidArgumentException; + +try { + $response = $anthropic->chat("Hello", [ + 'max_tokens' => 500000 // Exceeds model limit + ]); +} catch (InvalidArgumentException $e) { + echo "Parameter error: " . $e->getMessage(); +} catch (AuthenticationException $e) { + echo "Authentication failed: Check your API key"; +} catch (RateLimitException $e) { + echo "Rate limited: " . $e->getMessage(); +} +``` + +## Next Steps + +- **[OpenAI Provider](openai.md)** +- **[Ollama Provider](ollama.md)** diff --git a/docs/providers/index.md b/docs/providers/index.md new file mode 100644 index 0000000..fc689e9 --- /dev/null +++ b/docs/providers/index.md @@ -0,0 +1,3 @@ +* [Open AI Provider](openai.md) +* [Anthropic Provider](anthropic.md) +* [Ollama Provider](ollama.md) diff --git a/docs/providers/ollama.md b/docs/providers/ollama.md new file mode 100644 index 0000000..fdabaa2 --- /dev/null +++ b/docs/providers/ollama.md @@ -0,0 +1,190 @@ +# Ollama Provider + +The Ollama Provider ([`OllamaProvider`](../../src/Provider/OllamaProvider.php)) enables local AI inference without API costs, offering privacy-focused processing and offline capabilities. + +## Capabilities Overview + +| Feature | Description | +|---------|-------------| +| **Chat** | Text conversation with locally-hosted models | +| **Generate** | Text generation and completion tasks | +| **Model Management** | Pull, copy, delete, and list local models | +| **Model Availability** | Automatic model pulling and status checking | + +## Configuration + +```php +use Joomla\AI\AIFactory; + +$ollama = AIFactory::getAI('ollama', [ + 'base_url' => 'http://localhost:11434', // Default Ollama server + 'model' => 'llama3' // Optional: default model +]); +``` + +## Prerequisites + +### Install Ollama +1. Download from https://ollama.ai/ +2. Install and start the Ollama service +3. Pull your first model: +```bash +ollama pull llama3 +``` + +## Chat Completions + +### Basic Chat +```php +$response = $ollama->chat("Explain what Joomla CMS is and its main features."); +echo $response->getContent(); +``` + +### Chat with Model Selection +```php +$response = $ollama->chat("Write a PHP function for Joomla component development", [ + 'model' => 'codellama:7b', + 'temperature' => 0.1 // More focused for code generation +]); + +echo $response->getContent(); +``` + +## Text Generation + +### Basic Generation +```php +$response = $ollama->generate("Complete this Joomla tutorial: To create a custom module, first you need to", [ + 'model' => 'llama3' +]); + +echo $response->getContent(); +``` + +### Generation with Parameters +```php +$response = $ollama->generate("Write a Joomla plugin that", [ + 'model' => 'codellama:13b', + 'max_tokens' => 500, + 'temperature' => 0.3, + 'top_p' => 0.9, +]); + +echo $response->getContent(); +``` + +## Model Management + +### List Available Models (Local) +```php +$models = $ollama->getAvailableModels(); +echo "Available Models: " . implode(', ', $models) . "\n"; +``` + +### List Running Models +```php +$running = $ollama->getRunningModels(); +echo $running; +``` + +### Pull a Model +```php +// Pull model if not already available +$ollama->pullModel('mistral:7b'); +echo "Mistral 7B model is now available\n"; +``` + +### Copy a Model +```php +// Create a copy with a custom name +$ollama->copyModel('llama3', 'llama3-joomla-tuned'); +echo "Model copied for Joomla-specific use\n"; +``` + +### Delete a Model +```php +$ollama->deleteModel('old-model:tag'); +echo "Model removed to free up space\n"; +``` + +### Check Model Availability +```php +if ($ollama->checkModelExists('llama3')) { + echo "Llama3 is available locally\n"; +} else { + echo "Pulling Llama3...\n"; + $ollama->pullModel('llama3'); +} +``` + +## Default Model Management + +```php +// Set a default model for all operations +$ollama->setDefaultModel('llama3'); + +// These will use the default +$chat = $ollama->chat("Hello!"); +$generate = $ollama->generate("Complete this: "); + +// Override for specific calls +$code = $ollama->generate("Write PHP code:", ['model' => 'codellama:7b']); + +// Clear the default +$ollama->unsetDefaultModel(); +``` + +### Custom Parameters +```php +$response = $ollama->generate("Explain Joomla architecture", [ + 'model' => 'llama3', + 'temperature' => 0.7, + 'top_k' => 40, + 'top_p' => 0.9, + 'repeat_penalty' => 1.1, + 'seed' => 42, // For reproducible results + 'num_predict' => 100 +]); +``` + +## Error Handling + +```php +use Joomla\AI\Exception\ProviderException; +use Joomla\AI\Exception\InvalidArgumentException; + +try { + $response = $ollama->chat("Hello", ['model' => 'nonexistent-model']); +} catch (ProviderException $e) { + echo "Model not available. \n"; +} catch (InvalidArgumentException $e) { + echo "Parameter error: " . $e->getMessage(); +} +``` + +## Troubleshooting + +### Connection Issues +```php +// Test Ollama server connectivity +try { + $models = $ollama->getAvailableModels(); + echo "Ollama server is responding\n"; +} catch (Exception $e) { + echo "Ollama server not accessible. Check if it's running on localhost:11434\n"; +} +``` + +### Model Issues +```php +// Verify model availability before use +$modelName = 'llama3'; +if (!$ollama->checkModelExists($modelName)) { + echo "Model $modelName not found. Available models:\n"; +} +``` + +## Next Steps + +- **[OpenAI Provider](openai.md)** +- **[Anthropic Provider](anthropic.md)** diff --git a/docs/providers/openai.md b/docs/providers/openai.md new file mode 100644 index 0000000..b938e63 --- /dev/null +++ b/docs/providers/openai.md @@ -0,0 +1,390 @@ +# OpenAI Provider + +The OpenAI Provider ([`OpenAIProvider`](../../src/Provider/OpenAIProvider.php)) offers the most comprehensive feature set in the framework, supporting chat, vision, image generation, audio processing, embeddings, and content moderation. + +## Capabilities Overview + +| Feature | Description | +|---------|-------------| +| **Chat** | Standard text conversation with GPT models | +| **Vision** | Image analysis and description using vision-enabled models | +| **Audio Chat** | Experimental audio responses with gpt-4o-audio-preview | +| **Image Generation** | Text-to-image generation with DALL-E 2, DALL-E 3, and GPT-Image-1 models | +| **Image Editing** | Edit existing images with masks and prompts | +| **Image Variations** | Create variations of existing images (DALL-E 2 only) | +| **Text-to-Speech** | Convert text to speech with multiple voices and audio formats | +| **Transcription** | Convert audio to text using Whisper and GPT transcription models | +| **Translation** | Translate audio from any language to English | +| **Embeddings** | Generate vector embeddings for text similarity and search | +| **Moderation** | Content safety checking and flagging inappropriate content | + +## Configuration + +```php +use Joomla\AI\AIFactory; + +$openai = AIFactory::getAI('openai', [ + 'api_key' => getenv('OPENAI_API_KEY'), + 'base_url' => 'https://api.openai.com/v1', // Optional: custom endpoint + 'model' => 'gpt-4o-mini' // Optional: default model +]); +``` + +## Chat Completions + +### Basic Chat +```php +$response = $openai->chat("Explain Joomla CMS in one paragraph."); +echo $response->getContent(); +``` + +### Advanced Chat Options +```php +$response = $openai->chat("Write a technical article outline", [ + 'model' => 'gpt-4o', + 'max_tokens' => 1000, + 'temperature' => 0.7, + 'n' => 2 // Generate 2 response choices +]); + +$metadata = $response->getMetadata(); +echo "Used model: " . $metadata['model'] . "\n"; +echo "Tokens used: " . $metadata['usage']['total_tokens'] . "\n"; +``` + +### Message History +```php +$response = $openai->chat("", [ + 'messages' => [ + ['role' => 'system', 'content' => 'You are a Joomla expert.'], + ['role' => 'user', 'content' => 'How do I create a custom component?'], + ['role' => 'assistant', 'content' => 'To create a custom component...'], + ['role' => 'user', 'content' => 'What about adding database tables?'] + ] +]); +``` + +### Audio Chat (Experimental) +```php +$response = $openai->chat("Say hello in a friendly voice", [ + 'model' => 'gpt-4o-audio-preview', + 'modalities' => ['text', 'audio'], + 'audio' => [ + 'voice' => 'alloy', + 'format' => 'wav' + ] +]); + +// Save audio +$metadata = $response->getMetadata(); +if (isset($metadata['choices'][0]['message']['audio']['data'])) { + file_put_contents('hello.wav', base64_decode($metadata['choices'][0]['message']['audio']['data'])); +} +``` + +## Vision + +Analyze images with text prompts: + +```php +// URL image +$response = $openai->vision( + "Describe this website screenshot in detail", + "https://example.com/screenshot.jpg" +); + +// Base64 image +$imageData = base64_encode(file_get_contents('local-image.jpg')); +$response = $openai->vision( + "What coding framework is shown in this image?", + "data:image/jpeg;base64," . $imageData +); + +echo $response->getContent(); +``` + +## Image Generation + +### DALL-E 2 +```php +$image = $openai->generateImage("A futuristic Joomla logo in space", [ + 'model' => 'dall-e-2', + 'size' => '1024x1024', + 'n' => 4, // Generate 4 variations + 'response_format' => 'b64_json' +]); + +$image->saveFile('joomla-space-logo.png'); +``` + +### DALL-E 3 +```php +$image = $openai->generateImage("Minimalist Joomla CMS dashboard design", [ + 'model' => 'dall-e-3', + 'quality' => 'hd', + 'style' => 'vivid', // or 'natural' + 'response_format' => 'url' +]); + +echo "Image URL: " . $image->getContent(); +``` + +### GPT-Image-1 +```php +$image = $openai->generateImage("Professional web development workspace", [ + 'model' => 'gpt-image-1', + 'background' => 'transparent', + 'output_format' => 'png', + 'output_compression' => 80 +]); + +$image->saveFile('workspace.png'); +``` + +## Image Editing + +### Edit with Mask (DALL-E 2) +```php +$edited = $openai->editImage( + 'original-logo.png', + "Change the background to a gradient blue", + [ + 'model' => 'dall-e-2', + 'mask' => 'logo-mask.png', + 'size' => '1024x1024', + 'n' => 2 + ] +); + +$edited->saveFile('logo-blue-bg.png'); +``` + +### Multi-Image Edit (GPT-Image-1) +```php +$edited = $openai->editImage( + ['image1.png', 'image2.png', 'image3.png'], + "Combine these into a cohesive collage", + [ + 'model' => 'gpt-image-1', + 'background' => 'auto', + 'quality' => 'high' + ] +); + +$edited->saveFile('collage.png'); +``` + +## Image Variations + +Create variations of existing images (DALL-E 2 only): + +```php +$variations = $openai->createImageVariation('base-design.png', [ + 'model' => 'dall-e-2', + 'n' => 3, + 'size' => '512x512', + 'response_format' => 'b64_json' +]); + +$variations->saveFile('design-variation.png'); +``` + +## Text-to-Speech + +### Basic TTS +```php +$audio = $openai->speech("Welcome to our Joomla website!", [ + 'model' => 'tts-1', + 'voice' => 'nova', + 'response_format' => 'mp3' +]); + +$audio->saveFile('welcome.mp3'); +``` + +### High-Quality TTS +```php +$audio = $openai->speech("Professional announcement text", [ + 'model' => 'tts-1-hd', + 'response_format' => 'wav', + 'speed' => 1.25 // 0.25 to 4.0 +]); + +$audio->saveFile('announcement.wav'); +``` + +### Advanced TTS with Instructions +```php +$audio = $openai->speech("Say this with enthusiasm!", [ + 'model' => 'gpt-4o-mini-tts', + 'voice' => 'alloy', + 'response_format' => 'mp3', + 'instructions' => 'Speak with high energy and excitement', + 'speed' => 1.1 +]); + +$audio->saveFile('enthusiastic.mp3'); +``` + +## Audio Transcription + +### Basic Transcription +```php +$transcript = $openai->transcribe('meeting-recording.wav', [ + 'model' => 'whisper-1', + 'response_format' => 'json', + 'language' => 'en' +]); + +echo "Transcript: " . $transcript->getContent(); +``` + +### Subtitle Generation +```php +$subtitles = $openai->transcribe('video-audio.wav', [ + 'model' => 'whisper-1', + 'response_format' => 'srt' // or 'vtt' +]); + +$subtitles->saveFile('video-subtitles.srt'); +``` + +## Audio Translation + +Translate audio to English: + +```php +$translation = $openai->translate('spanish-audio.mp3', [ + 'model' => 'whisper-1', + 'response_format' => 'json', +]); + +echo "English translation: " . $translation->getContent(); +``` + +## Embeddings + +### Basic Embeddings +```php +$embeddings = $openai->createEmbeddings( + "Joomla is a content management system", + 'text-embedding-3-small' +); + +$vectors = json_decode($embeddings->getContent()); +echo "Vector dimensions: " . count($vectors) . "\n"; +``` + +## Content Moderation + +### Basic Moderation +```php +$result = $openai->moderate("Some text to check for safety"); + +if ($openai->isContentFlagged($result)) { + echo "Content was flagged\n"; + print_r($result['results'][0]['categories']); +} else { + echo "Content passed moderation\n"; +} +``` + +## Model Management + +### List Available Models +```php +$allModels = $openai->getAvailableModels(); +echo "Available models: " . implode(', ', $allModels) . "\n"; +``` + +## Default Model Management + +Set defaults to avoid repeating model names: + +```php +// Set defaults for different capabilities +$openai->setDefaultModel('gpt-4o-mini'); + +// All these use the default +$chat = $openai->chat("Hello"); +$vision = $openai->vision("Describe this", $imageUrl); + +// Override for specific calls +$premium = $openai->chat("Complex analysis", ['model' => 'gpt-4o']); + +// Clear defaults +$openai->unsetDefaultModel(); +``` + +## Error Handling + +OpenAI-specific error patterns: + +```php +use Joomla\AI\Exception\InvalidArgumentException; +use Joomla\AI\Exception\QuotaExceededException; + +try { + $response = $openai->generateImage("A simple logo", [ + 'model' => 'dall-e-3', + 'style' => 'invalid-style' // This will throw InvalidArgumentException + ]); +} catch (InvalidArgumentException $e) { + echo "Parameter error: " . $e->getMessage(); +} catch (QuotaExceededException $e) { + echo "Quota exceeded - check your billing"; +} +``` + +## Important Constraints + +### Model-Specific Limitations + +**DALL-E 2:** +- Image variations: requires square PNG, max 4MB +- Sizes: 256x256, 512x512, 1024x1024 only +- Max 10 images per request + +**DALL-E 3:** +- Only 1 image per request (`n=1`) +- Sizes: 1024x1024, 1792x1024, 1024x1792 only +- Style parameter: `vivid` or `natural` + +**GPT-Image-1:** +- Always returns base64 (no `response_format` parameter) +- Supports multiple input images for editing +- Max 16 images for editing +- Advanced parameters: background, output_format, compression + +**TTS Models:** +- Text limit: 4096 characters +- `instructions` parameter: only `gpt-4o-mini-tts` +- Speed range: 0.25 to 4.0 + +**Transcription:** +- File size limit: 25MB +- Formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, webm +- `gpt-4o-transcribe`/`gpt-4o-mini-transcribe`: only JSON response format + +**Embeddings:** +- `dimensions` parameter: only text-embedding-3-large (max 3072) and text-embedding-3-small (max 1536) + +### Content Safety + +The framework automatically applies moderation to: +- Chat messages (before sending) +- Image generation prompts +- TTS text input +- Image editing prompts + +Content flagged by moderation will throw an exception before the API call. + +## Best Practices + +1. **Use appropriate models**: Choose the right model for your use case (cost vs capability) +2. **Handle rate limits**: Implement exponential backoff for production use + +## Next Steps + +- **[Anthropic Provider](anthropic.md)** +- **[Ollama Provider](ollama.md)** diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a4fd6ec..03184cc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,8 @@ - - - - - - - - - - - - + - - Tests/Command - Tests/Monitor - Tests/Query - Tests/Service - Tests/AbstractDatabaseDriverTestCase.php - Tests/DatabaseAwareTraitTest.php - Tests/DatabaseExporterTest.php - Tests/DatabaseFactoryTest.php - Tests/DatabaseImporterTest.php - Tests/DatabaseIteratorTest.php - Tests/DatabaseQueryTest.php + + Tests diff --git a/src/AI.php b/src/AI.php new file mode 100644 index 0000000..b3dd06d --- /dev/null +++ b/src/AI.php @@ -0,0 +1,73 @@ +provider = $provider; + } + + /** + * Magic method to delegate all calls to the provider. + * + * @param string $method The method name being called + * @param array $arguments The arguments passed to the method + * + * @return mixed + * @throws ProviderException If the provider doesn't have the method + * @since __DEPLOY_VERSION__ + */ + public function __call(string $method, array $arguments) + { + if (method_exists($this->provider, $method)) { + return $this->provider->$method(...$arguments); + } + + throw new ProviderException( + $this->provider->getName(), + ['message' => 'Method ' . $method . ' is not supported by ' . $this->provider->getName()], + ); + } + + /** + * Get the underlying provider instance. + * + * @return AbstractProvider + * @since __DEPLOY_VERSION__ + */ + public function getProvider(): AbstractProvider + { + return $this->provider; + } +} diff --git a/src/AIFactory.php b/src/AIFactory.php new file mode 100644 index 0000000..3a8d58c --- /dev/null +++ b/src/AIFactory.php @@ -0,0 +1,107 @@ + OpenAIProvider::class, + 'anthropic' => AnthropicProvider::class, + 'ollama' => OllamaProvider::class, + ]; + + /** + * Create an AI instance with the specified provider + * + * @param string $provider The provider name (openai, anthropic, ollama) + * @param array $options Provider configuration options + * + * @return AI + * @throws InvalidArgumentException + * @throws ProviderException + * @since __DEPLOY_VERSION__ + */ + public static function getAI(string $provider, array $options = []): AI + { + $provider = strtolower($provider); + + if (!isset(self::PROVIDERS[$provider])) { + throw new ProviderException( + $provider, + [ + 'message' => 'Unknown AI provider: ' . $provider . '. Available providers: ' . implode(', ', array_keys(self::PROVIDERS)), + 'requested_provider' => $provider, + 'available_providers' => array_keys(self::PROVIDERS) + ], + null, + 'INVALID_PROVIDER' + ); + } + + $providerClass = self::PROVIDERS[$provider]; + + try { + $providerInstance = new $providerClass($options); + return new AI($providerInstance); + } catch (\Exception $e) { + throw new ProviderException( + $provider, + [ + 'message' => 'Failed to create ' . $provider . ' provider: ' . $e->getMessage(), + 'provider' => $provider, + 'original_error' => $e->getMessage() + ], + null, + 'PROVIDER_CREATION_FAILED' + ); + } + } + + /** + * Get available provider names + * + * @return array + * @since __DEPLOY_VERSION__ + */ + public static function getAvailableProviders(): array + { + return array_keys(self::PROVIDERS); + } + + /** + * Check if a provider is available + * + * @param string $provider The provider name + * + * @return bool + * @since __DEPLOY_VERSION__ + */ + public static function isProviderAvailable(string $provider): bool + { + return isset(self::PROVIDERS[strtolower($provider)]); + } +} diff --git a/src/AbstractProvider.php b/src/AbstractProvider.php index 65b35e7..e504411 100644 --- a/src/AbstractProvider.php +++ b/src/AbstractProvider.php @@ -52,7 +52,7 @@ abstract class AbstractProvider implements ProviderInterface /** * Constructor. * - * @param array|\ArrayAccess $options Provider options array. + * @param mixed $options Provider options array. * @param HttpFactory $httpFactory The http factory * * @throws \InvalidArgumentException diff --git a/src/Interface/ImageInterface.php b/src/Interface/ImageInterface.php index 80a5d92..1d494f7 100644 --- a/src/Interface/ImageInterface.php +++ b/src/Interface/ImageInterface.php @@ -39,7 +39,7 @@ public function generateImage(string $prompt, array $options = []): Response; * @return Response * @since __DEPLOY_VERSION__ */ - // public function editImage(string $imagePath, string $prompt, array $options = []): Response; + public function editImage(string $imagePath, string $prompt, array $options = []): Response; /** * Create alternative versions of an image from the text prompt given to the AI provider. @@ -50,5 +50,5 @@ public function generateImage(string $prompt, array $options = []): Response; * @return Response * @since __DEPLOY_VERSION__ */ - // public function createImageVariations(string $imagePath, array $options = []): Response; + public function createImageVariation(string $imagePath, array $options = []): Response; } diff --git a/src/Interface/ModelInterface.php b/src/Interface/ModelInterface.php index d88beb8..b29591b 100644 --- a/src/Interface/ModelInterface.php +++ b/src/Interface/ModelInterface.php @@ -16,38 +16,6 @@ */ interface ModelInterface { - /** - * Get all available models for this provider. - * - * @return array Array of available model names - * @since __DEPLOY_VERSION__ - */ - public function getAvailableModels(): array; - - /** - * Get models that support chat capability. - * - * @return array Array of chat capable model names - * @since __DEPLOY_VERSION__ - */ - public function getChatModels(): array; - - /** - * Get models that support vision capability. - * - * @return array Array of vision capable model names - * @since __DEPLOY_VERSION__ - */ - public function getVisionModels(): array; - - /** - * Get models that support image generation capability. - * - * @return array Array of image capable model names - * @since __DEPLOY_VERSION__ - */ - // public function getImageModels(): array; - /** * Check if a model supports a specific capability. * diff --git a/src/Interface/ProviderInterface.php b/src/Interface/ProviderInterface.php index 03d8ca4..abd825f 100644 --- a/src/Interface/ProviderInterface.php +++ b/src/Interface/ProviderInterface.php @@ -9,8 +9,6 @@ namespace Joomla\AI\Interface; -use Joomla\AI\Response\Response; - /** * AI provider class interface. * @@ -34,15 +32,11 @@ public static function isSupported(): bool; */ public function getName(): string; - // Should be a smart router in future versions. /** - * Send a prompt to the AI provider and return a Response object with the response. - * - * @param string $prompt The prompt to send to the AI provider. - * @param array $options An associative array of options to send with the request. + * Get all available models for this provider. * - * @return Response - * @since __DEPLOY_VERSION__ + * @return array Array of available model names + * @since __DEPLOY_VERSION__ */ - // public function prompt(string $prompt, array $options = []): Response; + public function getAvailableModels(): array; } diff --git a/src/Provider/AnthropicProvider.php b/src/Provider/AnthropicProvider.php index c033ad0..600e8d7 100644 --- a/src/Provider/AnthropicProvider.php +++ b/src/Provider/AnthropicProvider.php @@ -10,7 +10,6 @@ namespace Joomla\AI\Provider; use Joomla\AI\AbstractProvider; -use Joomla\AI\Provider\OpenAIProvider; use Joomla\AI\Exception\AuthenticationException; use Joomla\AI\Exception\InvalidArgumentException; use Joomla\AI\Exception\ProviderException; diff --git a/src/Provider/OllamaProvider.php b/src/Provider/OllamaProvider.php index f0c2e85..433a20b 100644 --- a/src/Provider/OllamaProvider.php +++ b/src/Provider/OllamaProvider.php @@ -6,6 +6,7 @@ use Joomla\AI\Exception\AuthenticationException; use Joomla\AI\Exception\InvalidArgumentException; use Joomla\AI\Exception\ProviderException; +use Joomla\AI\Interface\ProviderInterface; use Joomla\AI\Response\Response; use Joomla\Http\HttpFactory; @@ -14,7 +15,7 @@ * * @since __DEPLOY_VERSION__ */ -class OllamaProvider extends AbstractProvider +class OllamaProvider extends AbstractProvider implements ProviderInterface { /** * Custom base URL for API requests diff --git a/src/Provider/OpenAIProvider.php b/src/Provider/OpenAIProvider.php index 2027908..0b25a44 100644 --- a/src/Provider/OpenAIProvider.php +++ b/src/Provider/OpenAIProvider.php @@ -13,8 +13,10 @@ use Joomla\AI\Exception\AuthenticationException; use Joomla\AI\Exception\InvalidArgumentException; use Joomla\AI\Exception\ProviderException; +use Joomla\AI\Interface\ProviderInterface; use Joomla\AI\Interface\AudioInterface; use Joomla\AI\Interface\ChatInterface; +use Joomla\AI\Interface\EmbeddingInterface; use Joomla\AI\Interface\ImageInterface; use Joomla\AI\Interface\ModelInterface; use Joomla\AI\Interface\ModerationInterface; @@ -26,7 +28,7 @@ * * @since __DEPLOY_VERSION__ */ -class OpenAIProvider extends AbstractProvider implements ChatInterface, ModelInterface, ImageInterface, AudioInterface, ModerationInterface +class OpenAIProvider extends AbstractProvider implements ProviderInterface, ChatInterface, ModelInterface, ImageInterface, EmbeddingInterface, AudioInterface, ModerationInterface { /** * Custom base URL for API requests diff --git a/src/Response/Response.php b/src/Response/Response.php index c2c7030..8767273 100644 --- a/src/Response/Response.php +++ b/src/Response/Response.php @@ -100,13 +100,13 @@ public function saveFile(string $filename) $baseName = pathinfo($filename, PATHINFO_FILENAME); $ext = pathinfo($filename, PATHINFO_EXTENSION) ?: 'png'; - $data = json_decode($content, true); - if ($imageCount === 1 && is_string($content)) { + if ($imageCount === 1) { $decodedContent = base64_decode($content); if (File::write($filename, $decodedContent)) { $savedFiles[] = $filename; } - } elseif (is_array($data)) { + } else { + $data = json_decode($content, true); foreach ($data as $index => $b64) { $file = Path::clean($dir . '/' . $baseName . '_' . ($index + 1) . '.' . $ext); $decodedContent = base64_decode($b64); @@ -124,7 +124,7 @@ public function saveFile(string $filename) $data = json_decode($content, true); $lines = []; - if ($imageCount === 1 && is_string($content)) { + if ($imageCount === 1) { $lines[] = " Image URL: " . $content; } elseif (is_array($data)) { foreach ($data as $index => $url) { @@ -150,7 +150,7 @@ public function saveFile(string $filename) } // For all other content - if ($content !== null) { + if ($content !== '') { if (File::write($filename, $content)) { return [$filename]; }