Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit a7bf629

Browse files
OskarStarkchr-hertel
authored andcommitted
feat: Add Albert API platform support with comprehensive tests
- Add Albert-specific model clients for OpenAI-compatible API - Add PlatformFactory for easy Albert platform creation - Add comprehensive unit tests for all components - Require explicit API version in URL (e.g., /v1, /v2) - Add strict URL validation (HTTPS required, no trailing slashes) - Use plain PHP exception handling instead of external assertions - Apply consistent code style and rector fixes
1 parent bf72975 commit a7bf629

File tree

9 files changed

+712
-0
lines changed

9 files changed

+712
-0
lines changed

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,9 @@ RUN_EXPENSIVE_EXAMPLES=false
6565
# For using Gemini
6666
GEMINI_API_KEY=
6767

68+
# For using Albert API (French Sovereign AI)
69+
ALBERT_API_KEY=
70+
ALBERT_API_URL=
71+
6872
# For MariaDB store. Server defined in compose.yaml
6973
MARIADB_URI=pdo-mysql://[email protected]:3309/my_database

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ $embeddings = new Embeddings();
6969
* [DeepSeek's R1](https://www.deepseek.com/) with [OpenRouter](https://www.openrouter.com/) as Platform
7070
* [Amazon's Nova](https://nova.amazon.com) with [AWS](https://aws.amazon.com/bedrock/) as Platform
7171
* [Mistral's Mistral](https://www.mistral.ai/) with [Mistral](https://www.mistral.ai/) as Platform
72+
* [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)
7273
* Embeddings Models
7374
* [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
7475
* [Voyage's Embeddings](https://docs.voyageai.com/docs/embeddings) with [Voyage](https://www.voyageai.com/) as Platform
@@ -166,6 +167,7 @@ $response = $chain->call($messages, [
166167
1. [Google's Gemini with Google](examples/google/chat.php)
167168
1. [Google's Gemini with OpenRouter](examples/openrouter/chat-gemini.php)
168169
1. [Mistral's Mistral with Mistral](examples/mistral/chat-mistral.php)
170+
1. [Albert API (French Sovereign AI)](examples/albert/chat.php)
169171

170172
### Tools
171173

examples/albert/chat.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpLlm\LlmChain\Chain\Chain;
6+
use PhpLlm\LlmChain\Platform\Bridge\Albert\PlatformFactory;
7+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
8+
use PhpLlm\LlmChain\Platform\Message\Message;
9+
use PhpLlm\LlmChain\Platform\Message\MessageBag;
10+
11+
require_once dirname(__DIR__).'/../vendor/autoload.php';
12+
13+
if (empty($_ENV['ALBERT_API_KEY'])) {
14+
echo 'Please set the ALBERT_API_KEY environment variable.'.\PHP_EOL;
15+
exit(1);
16+
}
17+
18+
if (empty($_ENV['ALBERT_API_URL'])) {
19+
echo 'Please set the ALBERT_API_URL environment variable (e.g., https://your-albert-instance.com/v1).'.\PHP_EOL;
20+
exit(1);
21+
}
22+
23+
$platform = PlatformFactory::create(
24+
apiKey: $_ENV['ALBERT_API_KEY'],
25+
baseUrl: $_ENV['ALBERT_API_URL'],
26+
);
27+
28+
$model = new GPT('gpt-4o');
29+
$chain = new Chain($platform, $model);
30+
31+
$documentContext = <<<'CONTEXT'
32+
Document: AI Strategy of France
33+
34+
France has launched a comprehensive national AI strategy with the following key objectives:
35+
1. Strengthening the AI ecosystem and attracting talent
36+
2. Developing sovereign AI capabilities
37+
3. Ensuring ethical and responsible AI development
38+
4. Supporting AI adoption in public services
39+
5. Investing €1.5 billion in AI research and development
40+
41+
The Albert project is part of this strategy, providing a sovereign AI solution for French public administration.
42+
CONTEXT;
43+
44+
$messages = new MessageBag(
45+
Message::forSystem(
46+
'You are an AI assistant with access to documents about French AI initiatives. '.
47+
'Use the provided context to answer questions accurately.'
48+
),
49+
Message::ofUser($documentContext),
50+
Message::ofUser('What are the main objectives of France\'s AI strategy?'),
51+
);
52+
53+
$response = $chain->call($messages);
54+
55+
echo $response->getContent().\PHP_EOL;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Albert;
6+
7+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings;
8+
use PhpLlm\LlmChain\Platform\Exception\InvalidArgumentException;
9+
use PhpLlm\LlmChain\Platform\Model;
10+
use PhpLlm\LlmChain\Platform\ModelClientInterface;
11+
use Symfony\Contracts\HttpClient\HttpClientInterface;
12+
use Symfony\Contracts\HttpClient\ResponseInterface;
13+
14+
final readonly class EmbeddingsModelClient implements ModelClientInterface
15+
{
16+
public function __construct(
17+
private HttpClientInterface $httpClient,
18+
#[\SensitiveParameter] private string $apiKey,
19+
private string $baseUrl,
20+
) {
21+
if ('' === $apiKey) {
22+
throw new InvalidArgumentException('The API key must not be empty.');
23+
}
24+
if ('' === $baseUrl) {
25+
throw new InvalidArgumentException('The base URL must not be empty.');
26+
}
27+
}
28+
29+
public function supports(Model $model): bool
30+
{
31+
return $model instanceof Embeddings;
32+
}
33+
34+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
35+
{
36+
return $this->httpClient->request('POST', \sprintf('%s/embeddings', $this->baseUrl), [
37+
'auth_bearer' => $this->apiKey,
38+
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
39+
]);
40+
}
41+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Albert;
6+
7+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
8+
use PhpLlm\LlmChain\Platform\Exception\InvalidArgumentException;
9+
use PhpLlm\LlmChain\Platform\Model;
10+
use PhpLlm\LlmChain\Platform\ModelClientInterface;
11+
use Symfony\Component\HttpClient\EventSourceHttpClient;
12+
use Symfony\Contracts\HttpClient\HttpClientInterface;
13+
use Symfony\Contracts\HttpClient\ResponseInterface;
14+
15+
final readonly class GPTModelClient implements ModelClientInterface
16+
{
17+
private EventSourceHttpClient $httpClient;
18+
19+
public function __construct(
20+
HttpClientInterface $httpClient,
21+
#[\SensitiveParameter] private string $apiKey,
22+
private string $baseUrl,
23+
) {
24+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
25+
26+
if ('' === $apiKey) {
27+
throw new InvalidArgumentException('The API key must not be empty.');
28+
}
29+
if ('' === $baseUrl) {
30+
throw new InvalidArgumentException('The base URL must not be empty.');
31+
}
32+
}
33+
34+
public function supports(Model $model): bool
35+
{
36+
return $model instanceof GPT;
37+
}
38+
39+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
40+
{
41+
return $this->httpClient->request('POST', \sprintf('%s/chat/completions', $this->baseUrl), [
42+
'auth_bearer' => $this->apiKey,
43+
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
44+
]);
45+
}
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Platform\Bridge\Albert;
6+
7+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
8+
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT\ResponseConverter as GPTResponseConverter;
9+
use PhpLlm\LlmChain\Platform\Contract;
10+
use PhpLlm\LlmChain\Platform\Exception\InvalidArgumentException;
11+
use PhpLlm\LlmChain\Platform\Platform;
12+
use Symfony\Component\HttpClient\EventSourceHttpClient;
13+
use Symfony\Contracts\HttpClient\HttpClientInterface;
14+
15+
final class PlatformFactory
16+
{
17+
public static function create(
18+
#[\SensitiveParameter] string $apiKey,
19+
string $baseUrl,
20+
?HttpClientInterface $httpClient = null,
21+
): Platform {
22+
if (!str_starts_with($baseUrl, 'https://')) {
23+
throw new InvalidArgumentException('The Albert URL must start with "https://".');
24+
}
25+
26+
if (str_ends_with($baseUrl, '/')) {
27+
throw new InvalidArgumentException('The Albert URL must not end with a trailing slash.');
28+
}
29+
30+
if (!preg_match('/\/v\d+$/', $baseUrl)) {
31+
throw new InvalidArgumentException('The Albert URL must include an API version (e.g., /v1, /v2).');
32+
}
33+
34+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
35+
36+
$gptClient = new GPTModelClient($httpClient, $apiKey, $baseUrl);
37+
$embeddingsClient = new EmbeddingsModelClient($httpClient, $apiKey, $baseUrl);
38+
39+
return new Platform(
40+
[
41+
$gptClient,
42+
$embeddingsClient,
43+
],
44+
[
45+
new GPTResponseConverter(),
46+
new EmbeddingsResponseConverter(),
47+
],
48+
Contract::create(),
49+
);
50+
}
51+
}

0 commit comments

Comments
 (0)