Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 97 additions & 0 deletions Tests/GeminiProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Joomla\AI\Provider\GeminiProvider;
use Joomla\AI\Response\Response;
use Joomla\Http\HttpFactory;
use Joomla\Http\Http;
use Joomla\Http\Response as HttpResponse;

echo "=== GeminiProvider Test Suite ===\n\n";

// 1. Try to load real config
$configFile = __DIR__ . '/../config.json';
$config = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : [];
$apiKey = $config['gemini_api_key'] ?? getenv('GEMINI_API_KEY');

if ($apiKey) {
echo "[Integration Mode] Real API Key found. Testing against Google Gemini API...\n";
testRealApi($apiKey);
} else {
echo "[Mock Mode] No API Key found. Testing logic using Mock HTTP Client...\n";
testMockApi();
}

function testRealApi($apiKey) {
try {
$provider = new GeminiProvider(['api_key' => $apiKey]);
echo "Provider initialized.\n";

$response = $provider->chat("Explain what Joomla is in one sentence.");

echo "Response received:\n";
echo "Model: " . $response->getModel() . "\n";
echo "Content: " . $response->getContent() . "\n";

if (empty($response->getContent())) {
throw new Exception("Empty content received!");
}

echo "\n[PASS] Real API Test Passed.\n";

} catch (Exception $e) {
echo "[FAIL] Real API Test Failed: " . $e->getMessage() . "\n";
}
}

function testMockApi() {
// anonymous class for HttpFactory mock
$mockHttpFactory = new class extends HttpFactory {
public function getHttp(array $options = [], $adapters = null): Http {
// anonymous class for Http mock
return new class($options) extends Http {
public function post($url, $data, array $headers = [], $timeout = null) {
echo " -> Mock POST request to: " . substr($url, 0, 50) . "...\n";

// Simulate Gemini Success Response
$body = json_encode([
'candidates' => [
[
'content' => [
'parts' => [
['text' => 'Joomla is a powerful, open-source content management system used for building websites and applications.']
]
]
]
]
]);

return new HttpResponse(200, [], $body);
}
};
}
};

try {
echo "Initializing Provider with Mock Factory...\n";
// Pass mock factory to constructor
$provider = new GeminiProvider(['api_key' => 'mock_key'], $mockHttpFactory);

echo "Sending chat request...\n";
$response = $provider->chat("Test Message");

echo "Response processed.\n";
echo "Content: " . $response->getContent() . "\n";

if ($response->getContent() === 'Joomla is a powerful, open-source content management system used for building websites and applications.') {
echo "\n[PASS] Mock Logic Test Passed.\n";
} else {
throw new Exception("Unexpected content in mock response.");
}

} catch (Exception $e) {
echo "[FAIL] Mock Logic Test Failed: " . $e->getMessage() . "\n";
echo $e->getTraceAsString();
}
}
1 change: 1 addition & 0 deletions src/AIFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AIFactory
'openai' => OpenAIProvider::class,
'anthropic' => AnthropicProvider::class,
'ollama' => OllamaProvider::class,
'gemini' => \Joomla\AI\Provider\GeminiProvider::class,
];

/**
Expand Down
118 changes: 118 additions & 0 deletions src/Provider/GeminiProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* Part of the Joomla Framework AI Package
*
* @copyright
* Copyright (C) 2026 Open Source Matters, Inc.
*
* @license
* GNU General Public License version 2 or later; see LICENSE
*/

namespace Joomla\AI\Provider;

use Joomla\AI\AbstractProvider;
use Joomla\AI\Exception\AuthenticationException;
use Joomla\AI\Exception\InvalidArgumentException;
use Joomla\AI\Exception\ProviderException;
use Joomla\AI\Interface\ChatInterface;
use Joomla\AI\Interface\ModelInterface;
use Joomla\AI\Interface\ProviderInterface;
use Joomla\AI\Response\Response;
use Joomla\Http\HttpFactory;

/**
* Google Gemini provider implementation.
*
* @since __DEPLOY_VERSION__
*/
class GeminiProvider extends AbstractProvider implements ProviderInterface, ChatInterface, ModelInterface
{
private string $baseUrl = 'https://generativelanguage.googleapis.com/v1beta';

private const CHAT_MODELS = [
'gemini-pro',
'gemini-1.5-pro',
'gemini-1.5-flash',
];

public function chat(string $message, array $options = []): Response
{
$this->validateApiKey();

$model = $options['model'] ?? $this->defaultModel ?? 'gemini-pro';

if (!in_array($model, self::CHAT_MODELS, true)) {
throw new InvalidArgumentException('Unsupported Gemini model: ' . $model);
}

$payload = [
'contents' => [
[
'role' => 'user',
'parts' => [
['text' => $message],
],
]
],
];

// Merge additional options if needed, but for now just message
// $payload = array_merge($payload, $options);

$url = $this->baseUrl . '/models/' . $model . ':generateContent?key=' . $this->getApiKey();

$response = $this->makePostRequest(
$url,
json_encode($payload),
['Content-Type' => 'application/json']
);

$data = json_decode($response->body, true);

if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
throw new ProviderException('Invalid response received from Gemini API.', $data);
}

return new Response([
'content' => $data['candidates'][0]['content']['parts'][0]['text'],
'model' => $model,
'raw' => $data,
]);
}

public function vision(string $message, string $image, array $options = []): Response
{
$this->validateApiKey();

$model = $options['model'] ?? $this->defaultModel ?? 'gemini-1.5-flash';

if (!in_array($model, self::CHAT_MODELS, true)) {
throw new InvalidArgumentException('Unsupported Gemini model: ' . $model);
}

// Detect if input is a URL or base64
// For simplicity assuming base64 or handled by caller for now as per other providers
// But Gemini expects inlineData or fileData.

// This is a placeholder for full vision support, implementing basic structure
throw new ProviderException("Vision not fully implemented for Gemini yet");
}

public function models(): array
{
return self::CHAT_MODELS;
}

private function validateApiKey(): void
{
if (!$this->getApiKey()) {
throw new AuthenticationException('Gemini API key is missing.');
}
}

private function getApiKey(): ?string
{
return $this->getOption('api_key');
}
}