Skip to content

[Platform] add Albert API support #140

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

Merged
merged 1 commit into from
Jul 17, 2025
Merged
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 examples/.env
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ RUN_EXPENSIVE_EXAMPLES=false
# For using Gemini
GEMINI_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

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

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Platform\Bridge\Albert\PlatformFactory;
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

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

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

$platform = PlatformFactory::create($_SERVER['ALBERT_API_KEY'], $_SERVER['ALBERT_API_URL']);

$model = new GPT('gpt-4o');
$agent = new Agent($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 = $agent->call($messages);

echo $response->getContent().\PHP_EOL;
3 changes: 3 additions & 0 deletions src/platform/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ usually defined by the specific models and their documentation.
* `DeepSeek's R1`_ with `OpenRouter`_ as Platform
* `Amazon's Nova`_ with `AWS Bedrock`_ as Platform
* `Mistral's Mistral`_ with `Mistral`_ as Platform
* `Albert API`_ models with `Albert`_ as Platform (French government's sovereign AI gateway)
* **Embeddings Models**
* `Google's Text Embeddings`_ with `Google`_
* `OpenAI's Text Embeddings`_ with `OpenAI`_ and `Azure`_ as Platform
Expand Down Expand Up @@ -307,6 +308,8 @@ which can be useful to speed up the processing::
.. _`DeepSeek's R1`: https://www.deepseek.com/
.. _`Amazon's Nova`: https://nova.amazon.com
.. _`Mistral's Mistral`: https://www.mistral.ai/
.. _`Albert API`: https://github.com/etalab-ia/albert-api
.. _`Albert`: https://alliance.numerique.gouv.fr/produit/albert/
.. _`Mistral`: https://www.mistral.ai/
.. _`Google's Text Embeddings`: https://ai.google.dev/gemini-api/docs/embeddings
.. _`OpenAI's Text Embeddings`: https://platform.openai.com/docs/guides/embeddings/embedding-models
Expand Down
2 changes: 1 addition & 1 deletion src/platform/phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ parameters:
path: src/*
reportUnmatched: false # only needed for older Symfony versions
-
message: '#no value type specified in iterable type array#'
identifier: missingType.iterableValue
path: tests/*
47 changes: 47 additions & 0 deletions src/platform/src/Bridge/Albert/EmbeddingsModelClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Albert;

use Symfony\AI\Platform\Bridge\OpenAI\Embeddings;
use Symfony\AI\Platform\Exception\InvalidArgumentException;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\ModelClientInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Oskar Stark <[email protected]>
*/
final readonly class EmbeddingsModelClient implements ModelClientInterface
{
public function __construct(
private HttpClientInterface $httpClient,
#[\SensitiveParameter] private string $apiKey,
private string $baseUrl,
) {
'' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.');
'' !== $baseUrl || throw new InvalidArgumentException('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', \sprintf('%s/embeddings', $this->baseUrl), [
'auth_bearer' => $this->apiKey,
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
]);
}
}
52 changes: 52 additions & 0 deletions src/platform/src/Bridge/Albert/GPTModelClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Albert;

use Symfony\AI\Platform\Bridge\OpenAI\GPT;
use Symfony\AI\Platform\Exception\InvalidArgumentException;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\ModelClientInterface;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @author Oskar Stark <[email protected]>
*/
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);

'' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.');
'' !== $baseUrl || throw new InvalidArgumentException('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', \sprintf('%s/chat/completions', $this->baseUrl), [
'auth_bearer' => $this->apiKey,
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
]);
}
}
47 changes: 47 additions & 0 deletions src/platform/src/Bridge/Albert/PlatformFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Albert;

use Symfony\AI\Platform\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
use Symfony\AI\Platform\Bridge\OpenAI\GPT\ResponseConverter as GPTResponseConverter;
use Symfony\AI\Platform\Contract;
use Symfony\AI\Platform\Exception\InvalidArgumentException;
use Symfony\AI\Platform\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
* @author Oskar Stark <[email protected]>
*/
final class PlatformFactory
{
public static function create(
#[\SensitiveParameter] string $apiKey,
string $baseUrl,
?HttpClientInterface $httpClient = null,
): Platform {
str_starts_with($baseUrl, 'https://') || throw new InvalidArgumentException('The Albert URL must start with "https://".');
!str_ends_with($baseUrl, '/') || throw new InvalidArgumentException('The Albert URL must not end with a trailing slash.');
preg_match('/\/v\d+$/', $baseUrl) || throw new InvalidArgumentException('The Albert URL must include an API version (e.g., /v1, /v2).');

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

return new Platform(
[
new GPTModelClient($httpClient, $apiKey, $baseUrl),
new EmbeddingsModelClient($httpClient, $apiKey, $baseUrl),
],
[new GPTResponseConverter(), new EmbeddingsResponseConverter()],
Contract::create(),
);
}
}
Loading