Skip to content

Commit 88826f8

Browse files
committed
feature #140 [Platform] add Albert API support (OskarStark)
This PR was merged into the main branch. Discussion ---------- [Platform] add Albert API support | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | | License | MIT Cherry picking php-llm/llm-chain#366 Commits ------- 99ce42a feat: add Albert API support (#366)
2 parents 73efdf6 + 99ce42a commit 88826f8

File tree

10 files changed

+733
-1
lines changed

10 files changed

+733
-1
lines changed

examples/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ RUN_EXPENSIVE_EXAMPLES=false
6868
# For using Gemini
6969
GEMINI_API_KEY=
7070

71+
# For using Albert API (French Sovereign AI)
72+
ALBERT_API_KEY=
73+
ALBERT_API_URL=
74+
7175
# For MariaDB store. Server defined in compose.yaml
7276
MARIADB_URI=pdo-mysql://[email protected]:3309/my_database
7377

examples/albert/chat.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Albert\PlatformFactory;
14+
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/../vendor/autoload.php';
19+
20+
if (!isset($_SERVER['ALBERT_API_KEY'], $_SERVER['ALBERT_API_URL'])) {
21+
echo 'Please set the ALBERT_API_KEY and ALBERT_API_URL environment variable (e.g., https://your-albert-instance.com/v1).'.\PHP_EOL;
22+
exit(1);
23+
}
24+
25+
$platform = PlatformFactory::create($_SERVER['ALBERT_API_KEY'], $_SERVER['ALBERT_API_URL']);
26+
27+
$model = new GPT('gpt-4o');
28+
$agent = new Agent($platform, $model);
29+
30+
$documentContext = <<<'CONTEXT'
31+
Document: AI Strategy of France
32+
33+
France has launched a comprehensive national AI strategy with the following key objectives:
34+
1. Strengthening the AI ecosystem and attracting talent
35+
2. Developing sovereign AI capabilities
36+
3. Ensuring ethical and responsible AI development
37+
4. Supporting AI adoption in public services
38+
5. Investing €1.5 billion in AI research and development
39+
40+
The Albert project is part of this strategy, providing a sovereign AI solution for French public administration.
41+
CONTEXT;
42+
43+
$messages = new MessageBag(
44+
Message::forSystem(
45+
'You are an AI assistant with access to documents about French AI initiatives. '.
46+
'Use the provided context to answer questions accurately.'
47+
),
48+
Message::ofUser($documentContext),
49+
Message::ofUser('What are the main objectives of France\'s AI strategy?'),
50+
);
51+
52+
$response = $agent->call($messages);
53+
54+
echo $response->getContent().\PHP_EOL;

src/platform/doc/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ usually defined by the specific models and their documentation.
7777
* `DeepSeek's R1`_ with `OpenRouter`_ as Platform
7878
* `Amazon's Nova`_ with `AWS Bedrock`_ as Platform
7979
* `Mistral's Mistral`_ with `Mistral`_ as Platform
80+
* `Albert API`_ models with `Albert`_ as Platform (French government's sovereign AI gateway)
8081
* **Embeddings Models**
8182
* `Google's Text Embeddings`_ with `Google`_
8283
* `OpenAI's Text Embeddings`_ with `OpenAI`_ and `Azure`_ as Platform
@@ -307,6 +308,8 @@ which can be useful to speed up the processing::
307308
.. _`DeepSeek's R1`: https://www.deepseek.com/
308309
.. _`Amazon's Nova`: https://nova.amazon.com
309310
.. _`Mistral's Mistral`: https://www.mistral.ai/
311+
.. _`Albert API`: https://github.com/etalab-ia/albert-api
312+
.. _`Albert`: https://alliance.numerique.gouv.fr/produit/albert/
310313
.. _`Mistral`: https://www.mistral.ai/
311314
.. _`Google's Text Embeddings`: https://ai.google.dev/gemini-api/docs/embeddings
312315
.. _`OpenAI's Text Embeddings`: https://platform.openai.com/docs/guides/embeddings/embedding-models

src/platform/phpstan.dist.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ parameters:
1717
path: src/*
1818
reportUnmatched: false # only needed for older Symfony versions
1919
-
20-
message: '#no value type specified in iterable type array#'
20+
identifier: missingType.iterableValue
2121
path: tests/*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Albert;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAI\Embeddings;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\ModelClientInterface;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
/**
22+
* @author Oskar Stark <[email protected]>
23+
*/
24+
final readonly class EmbeddingsModelClient implements ModelClientInterface
25+
{
26+
public function __construct(
27+
private HttpClientInterface $httpClient,
28+
#[\SensitiveParameter] private string $apiKey,
29+
private string $baseUrl,
30+
) {
31+
'' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.');
32+
'' !== $baseUrl || throw new InvalidArgumentException('The base URL must not be empty.');
33+
}
34+
35+
public function supports(Model $model): bool
36+
{
37+
return $model instanceof Embeddings;
38+
}
39+
40+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
41+
{
42+
return $this->httpClient->request('POST', \sprintf('%s/embeddings', $this->baseUrl), [
43+
'auth_bearer' => $this->apiKey,
44+
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
45+
]);
46+
}
47+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Albert;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAI\GPT;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\ModelClientInterface;
18+
use Symfony\Component\HttpClient\EventSourceHttpClient;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
use Symfony\Contracts\HttpClient\ResponseInterface;
21+
22+
/**
23+
* @author Oskar Stark <[email protected]>
24+
*/
25+
final readonly class GPTModelClient implements ModelClientInterface
26+
{
27+
private EventSourceHttpClient $httpClient;
28+
29+
public function __construct(
30+
HttpClientInterface $httpClient,
31+
#[\SensitiveParameter] private string $apiKey,
32+
private string $baseUrl,
33+
) {
34+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
35+
36+
'' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.');
37+
'' !== $baseUrl || throw new InvalidArgumentException('The base URL must not be empty.');
38+
}
39+
40+
public function supports(Model $model): bool
41+
{
42+
return $model instanceof GPT;
43+
}
44+
45+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
46+
{
47+
return $this->httpClient->request('POST', \sprintf('%s/chat/completions', $this->baseUrl), [
48+
'auth_bearer' => $this->apiKey,
49+
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
50+
]);
51+
}
52+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Albert;
13+
14+
use Symfony\AI\Platform\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
15+
use Symfony\AI\Platform\Bridge\OpenAI\GPT\ResponseConverter as GPTResponseConverter;
16+
use Symfony\AI\Platform\Contract;
17+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
18+
use Symfony\AI\Platform\Platform;
19+
use Symfony\Component\HttpClient\EventSourceHttpClient;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @author Oskar Stark <[email protected]>
24+
*/
25+
final class PlatformFactory
26+
{
27+
public static function create(
28+
#[\SensitiveParameter] string $apiKey,
29+
string $baseUrl,
30+
?HttpClientInterface $httpClient = null,
31+
): Platform {
32+
str_starts_with($baseUrl, 'https://') || throw new InvalidArgumentException('The Albert URL must start with "https://".');
33+
!str_ends_with($baseUrl, '/') || throw new InvalidArgumentException('The Albert URL must not end with a trailing slash.');
34+
preg_match('/\/v\d+$/', $baseUrl) || throw new InvalidArgumentException('The Albert URL must include an API version (e.g., /v1, /v2).');
35+
36+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
37+
38+
return new Platform(
39+
[
40+
new GPTModelClient($httpClient, $apiKey, $baseUrl),
41+
new EmbeddingsModelClient($httpClient, $apiKey, $baseUrl),
42+
],
43+
[new GPTResponseConverter(), new EmbeddingsResponseConverter()],
44+
Contract::create(),
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)