Skip to content

feat: add Albert API support #366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,9 @@ RUN_EXPENSIVE_EXAMPLES=false
# For using Gemini
GOOGLE_API_KEY=

# For using Albert API (French Sovereign AI)
ALBERT_API_KEY=
ALBERT_API_URL=

# For MariaDB store. Server defined in compose.yaml
MARIADB_URI=pdo-mysql://[email protected]:3309/my_database
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ $embeddings = new Embeddings();
* [DeepSeek's R1](https://www.deepseek.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
* [Amazon's Nova](https://nova.amazon.com) with [AWS](https://aws.amazon.com/bedrock/) as Platform
* [Mistral's Mistral](https://www.mistral.ai/) with [Mistral](https://www.mistral.ai/) as Platform
* [Albert API](https://github.com/etalab-ia/albert-api) models with [Albert](https://github.com/etalab-ia/albert-api) as Platform (French government's sovereign AI gateway)
* Embeddings Models
* [OpenAI's Text Embeddings](https://platform.openai.com/docs/guides/embeddings/embedding-models) with [OpenAI](https://platform.openai.com/docs/overview) and [Azure](https://learn.microsoft.com/azure/ai-services/openai/concepts/models) as Platform
* [Voyage's Embeddings](https://docs.voyageai.com/docs/embeddings) with [Voyage](https://www.voyageai.com/) as Platform
Expand Down Expand Up @@ -166,6 +167,7 @@ $response = $chain->call($messages, [
1. [Google's Gemini with Google](examples/google/chat.php)
1. [Google's Gemini with OpenRouter](examples/openrouter/chat-gemini.php)
1. [Mistral's Mistral with Mistral](examples/mistral/chat-mistral.php)
1. [Albert API (French Sovereign AI)](examples/albert/chat.php)

### Tools

Expand Down
55 changes: 55 additions & 0 deletions examples/albert/chat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

use PhpLlm\LlmChain\Chain\Chain;
use PhpLlm\LlmChain\Platform\Bridge\Albert\PlatformFactory;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Platform\Message\Message;
use PhpLlm\LlmChain\Platform\Message\MessageBag;

require_once dirname(__DIR__).'/../vendor/autoload.php';

if (empty($_ENV['ALBERT_API_KEY'])) {
echo 'Please set the ALBERT_API_KEY environment variable.'.\PHP_EOL;
exit(1);
}

if (empty($_ENV['ALBERT_API_URL'])) {
echo 'Please set the ALBERT_API_URL environment variable (e.g., https://your-albert-instance.com).'.\PHP_EOL;
exit(1);
}

$platform = PlatformFactory::create(
apiKey: $_ENV['ALBERT_API_KEY'],
albertUrl: $_ENV['ALBERT_API_URL'],
);

$model = new GPT('gpt-4o');
$chain = new Chain($platform, $model);

$documentContext = <<<'CONTEXT'
Document: AI Strategy of France

France has launched a comprehensive national AI strategy with the following key objectives:
1. Strengthening the AI ecosystem and attracting talent
2. Developing sovereign AI capabilities
3. Ensuring ethical and responsible AI development
4. Supporting AI adoption in public services
5. Investing €1.5 billion in AI research and development

The Albert project is part of this strategy, providing a sovereign AI solution for French public administration.
CONTEXT;

$messages = new MessageBag(
Message::forSystem(
'You are an AI assistant with access to documents about French AI initiatives. '.
'Use the provided context to answer questions accurately.'
),
Message::ofUser($documentContext),
Message::ofUser('What are the main objectives of France\'s AI strategy?'),
);

$response = $chain->call($messages);

echo $response->getContent().\PHP_EOL;
38 changes: 38 additions & 0 deletions src/Platform/Bridge/Albert/EmbeddingsModelClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Platform\Bridge\Albert;

use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings;
use PhpLlm\LlmChain\Platform\Model;
use PhpLlm\LlmChain\Platform\ModelClientInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Webmozart\Assert\Assert;

final readonly class EmbeddingsModelClient implements ModelClientInterface
{
public function __construct(
private HttpClientInterface $httpClient,
#[\SensitiveParameter]
private string $apiKey,
private string $baseUrl,
) {
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
Assert::stringNotEmpty($baseUrl, 'The base URL must not be empty.');
}

public function supports(Model $model): bool
{
return $model instanceof Embeddings;
}

public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
{
return $this->httpClient->request('POST', $this->baseUrl.'embeddings', [
'auth_bearer' => $this->apiKey,
'json' => array_merge($options, $payload),
]);
}
}
42 changes: 42 additions & 0 deletions src/Platform/Bridge/Albert/GPTModelClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Platform\Bridge\Albert;

use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Platform\Model;
use PhpLlm\LlmChain\Platform\ModelClientInterface;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Webmozart\Assert\Assert;

final readonly class GPTModelClient implements ModelClientInterface
{
private EventSourceHttpClient $httpClient;

public function __construct(
HttpClientInterface $httpClient,
#[\SensitiveParameter]
private string $apiKey,
private string $baseUrl,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
Assert::stringNotEmpty($baseUrl, 'The base URL must not be empty.');
}

public function supports(Model $model): bool
{
return $model instanceof GPT;
}

public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
{
return $this->httpClient->request('POST', $this->baseUrl.'chat/completions', [
'auth_bearer' => $this->apiKey,
'json' => array_merge($options, $payload),
]);
}
}
50 changes: 50 additions & 0 deletions src/Platform/Bridge/Albert/PlatformFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Platform\Bridge\Albert;

use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT\ResponseConverter as GPTResponseConverter;
use PhpLlm\LlmChain\Platform\Contract;
use PhpLlm\LlmChain\Platform\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Webmozart\Assert\Assert;

final class PlatformFactory
{
public static function create(
string $apiKey,
string $albertUrl,
?HttpClientInterface $httpClient = null,
): Platform {
Assert::startsWith($albertUrl, 'https://', 'The Albert URL must start with "https://".');

$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);

// The base URL should include the full path to the API endpoint
// Albert API expects the URL to end with /v1/
$baseUrl = rtrim($albertUrl, '/');
if (!str_ends_with($baseUrl, '/v1')) {
$baseUrl .= '/v1';
}
$baseUrl .= '/';

// Create Albert-specific model clients with custom base URL
$gptClient = new GPTModelClient($httpClient, $apiKey, $baseUrl);
$embeddingsClient = new EmbeddingsModelClient($httpClient, $apiKey, $baseUrl);

return new Platform(
[
$gptClient,
$embeddingsClient,
],
[
new GPTResponseConverter(),
new EmbeddingsResponseConverter(),
],
Contract::create(),
);
}
}
55 changes: 55 additions & 0 deletions tests/Platform/Bridge/Albert/PlatformFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChain\Tests\Platform\Bridge\Albert;

use PhpLlm\LlmChain\Platform\Bridge\Albert\PlatformFactory;
use PhpLlm\LlmChain\Platform\Platform;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Webmozart\Assert\InvalidArgumentException;

#[CoversClass(PlatformFactory::class)]
#[Small]
final class PlatformFactoryTest extends TestCase
{
#[Test]
public function createsPlatformWithCorrectBaseUrl(): void
{
$platform = PlatformFactory::create('test-key', 'https://albert.example.com');

self::assertInstanceOf(Platform::class, $platform);
}

#[Test]
#[DataProvider('urlProvider')]
public function handlesUrlsCorrectly(string $url): void
{
$platform = PlatformFactory::create('test-key', $url);

self::assertInstanceOf(Platform::class, $platform);
}

public static function urlProvider(): array
{
return [
'with trailing slash' => ['https://albert.example.com/'],
'without trailing slash' => ['https://albert.example.com'],
'with v1 path' => ['https://albert.example.com/v1'],
'with v1 path and trailing slash' => ['https://albert.example.com/v1/'],
];
}

#[Test]
public function throwsExceptionForNonHttpsUrl(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The Albert URL must start with "https://".');

PlatformFactory::create('test-key', 'http://albert.example.com');
}
}
Loading