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

Commit 9271a5b

Browse files
onemoreanglechr-hertel
authored andcommitted
feat: added initial Google provider supporting basic text generation with it
1 parent d7ecb4d commit 9271a5b

18 files changed

+386
-0
lines changed

.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ PINECONE_HOST=
3939

4040
# Some examples are expensive to run, so we disable them by default
4141
RUN_EXPENSIVE_EXAMPLES=false
42+
43+
# For using Gemini
44+
GOOGLE_API_KEY=

examples/chat-gemini-google.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use PhpLlm\LlmChain\Bridge\Google\GoogleModel;
4+
use PhpLlm\LlmChain\Bridge\Google\PlatformFactory;
5+
use PhpLlm\LlmChain\Chain;
6+
use PhpLlm\LlmChain\Model\Message\Message;
7+
use PhpLlm\LlmChain\Model\Message\MessageBag;
8+
use Symfony\Component\Dotenv\Dotenv;
9+
10+
require_once dirname(__DIR__).'/vendor/autoload.php';
11+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
12+
13+
if (empty($_ENV['GOOGLE_API_KEY'])) {
14+
echo 'Please set the GOOGLE_API_KEY environment variable.'.PHP_EOL;
15+
exit(1);
16+
}
17+
18+
$platform = PlatformFactory::create($_ENV['GOOGLE_API_KEY']);
19+
$llm = new GoogleModel(GoogleModel::GEMINI_2_FLASH);
20+
21+
$chain = new Chain($platform, $llm);
22+
$messages = new MessageBag(
23+
Message::forSystem('You are a pirate and you write funny.'),
24+
Message::ofUser('What is the Symfony framework?'),
25+
);
26+
$response = $chain->call($messages);
27+
28+
echo $response->getContent().PHP_EOL;

src/Bridge/Google/GoogleModel.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Google;
6+
7+
use PhpLlm\LlmChain\Model\LanguageModel;
8+
9+
final readonly class GoogleModel implements LanguageModel
10+
{
11+
public const GEMINI_2_FLASH = 'gemini-2.0-flash';
12+
public const GEMINI_2_PRO = 'gemini-2.0-pro-exp-02-05';
13+
public const GEMINI_2_FLASH_LITE = 'gemini-2.0-flash-lite-preview-02-05';
14+
public const GEMINI_2_FLASH_THINKING = 'gemini-2.0-flash-thinking-exp-01-21';
15+
public const GEMINI_1_5_FLASH = 'gemini-1.5-flash';
16+
17+
/**
18+
* @param array<string, mixed> $options The default options for the model usage
19+
*/
20+
public function __construct(
21+
private string $version = self::GEMINI_2_PRO,
22+
private array $options = ['temperature' => 1.0],
23+
) {
24+
}
25+
26+
public function getVersion(): string
27+
{
28+
return $this->version;
29+
}
30+
31+
public function getOptions(): array
32+
{
33+
return $this->options;
34+
}
35+
36+
public function supportsAudioInput(): bool
37+
{
38+
return false; // it does, but implementation here is still open; in_array($this->version, [self::GEMINI_2_FLASH, self::GEMINI_2_PRO, self::GEMINI_1_5_FLASH], true);
39+
}
40+
41+
public function supportsImageInput(): bool
42+
{
43+
return false; // it does, but implementation here is still open;in_array($this->version, [self::GEMINI_2_FLASH, self::GEMINI_2_PRO, self::GEMINI_2_FLASH_LITE, self::GEMINI_2_FLASH_THINKING, self::GEMINI_1_5_FLASH], true);
44+
}
45+
46+
public function supportsStreaming(): bool
47+
{
48+
return true;
49+
}
50+
51+
public function supportsStructuredOutput(): bool
52+
{
53+
return false;
54+
}
55+
56+
public function supportsToolCalling(): bool
57+
{
58+
return false;
59+
}
60+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Bridge\Google;
4+
5+
use PhpLlm\LlmChain\Model\Message\AssistantMessage;
6+
use PhpLlm\LlmChain\Model\Message\Content\Audio;
7+
use PhpLlm\LlmChain\Model\Message\Content\ContentVisitor;
8+
use PhpLlm\LlmChain\Model\Message\Content\Image;
9+
use PhpLlm\LlmChain\Model\Message\Content\Text;
10+
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
11+
use PhpLlm\LlmChain\Model\Message\MessageVisitor;
12+
use PhpLlm\LlmChain\Model\Message\SystemMessage;
13+
use PhpLlm\LlmChain\Model\Message\ToolCallMessage;
14+
use PhpLlm\LlmChain\Model\Message\UserMessage;
15+
use PhpLlm\LlmChain\Platform\RequestBodyProducer;
16+
17+
final class GoogleRequestBodyProducer implements RequestBodyProducer, MessageVisitor, ContentVisitor, \JsonSerializable
18+
{
19+
protected MessageBagInterface $bag;
20+
21+
public function __construct(MessageBagInterface $bag)
22+
{
23+
$this->bag = $bag;
24+
}
25+
26+
public function createBody(): array
27+
{
28+
$contents = [];
29+
foreach ($this->bag->withoutSystemMessage()->getMessages() as $message) {
30+
$contents[] = [
31+
'role' => $message->getRole(),
32+
'parts' => $message->accept($this),
33+
];
34+
}
35+
36+
$body = [
37+
'contents' => $contents,
38+
];
39+
40+
$systemMessage = $this->bag->getSystemMessage();
41+
if (null !== $systemMessage) {
42+
$body['systemInstruction'] = [
43+
'parts' => $systemMessage->accept($this),
44+
];
45+
}
46+
47+
return $body;
48+
}
49+
50+
public function visitUserMessage(UserMessage $message): array
51+
{
52+
$parts = [];
53+
foreach ($message->content as $content) {
54+
$parts[] = [...$content->accept($this)];
55+
}
56+
57+
return $parts;
58+
}
59+
60+
public function visitAssistantMessage(AssistantMessage $message): array
61+
{
62+
return [['text' => $message->content]];
63+
}
64+
65+
public function visitSystemMessage(SystemMessage $message): array
66+
{
67+
return [['text' => $message->content]];
68+
}
69+
70+
public function visitText(Text $content): array
71+
{
72+
return ['text' => $content->text];
73+
}
74+
75+
public function visitImage(Image $content): array
76+
{
77+
// TODO: support image
78+
return [];
79+
}
80+
81+
public function visitAudio(Audio $content): array
82+
{
83+
// TODO: support audio
84+
return [];
85+
}
86+
87+
public function visitToolCallMessage(ToolCallMessage $message): array
88+
{
89+
// TODO: support tool call message
90+
return [];
91+
}
92+
93+
public function jsonSerialize(): array
94+
{
95+
return $this->createBody();
96+
}
97+
}

src/Bridge/Google/ModelHandler.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Google;
6+
7+
use PhpLlm\LlmChain\Exception\RuntimeException;
8+
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
9+
use PhpLlm\LlmChain\Model\Model;
10+
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
11+
use PhpLlm\LlmChain\Model\Response\StreamResponse;
12+
use PhpLlm\LlmChain\Model\Response\TextResponse;
13+
use PhpLlm\LlmChain\Platform\ModelClient;
14+
use PhpLlm\LlmChain\Platform\ResponseConverter;
15+
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
16+
use Symfony\Component\HttpClient\EventSourceHttpClient;
17+
use Symfony\Component\HttpClient\Exception\JsonException;
18+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
19+
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
20+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
21+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
22+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
23+
use Symfony\Contracts\HttpClient\HttpClientInterface;
24+
use Symfony\Contracts\HttpClient\ResponseInterface;
25+
use Webmozart\Assert\Assert;
26+
27+
final readonly class ModelHandler implements ModelClient, ResponseConverter
28+
{
29+
private EventSourceHttpClient $httpClient;
30+
31+
public function __construct(
32+
HttpClientInterface $httpClient,
33+
#[\SensitiveParameter] private string $apiKey,
34+
) {
35+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
36+
}
37+
38+
public function supports(Model $model, array|string|object $input): bool
39+
{
40+
return $model instanceof GoogleModel && $input instanceof MessageBagInterface;
41+
}
42+
43+
/**
44+
* @throws TransportExceptionInterface
45+
*/
46+
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
47+
{
48+
Assert::isInstanceOf($input, MessageBagInterface::class);
49+
50+
$body = new GoogleRequestBodyProducer($input);
51+
52+
return $this->httpClient->request('POST', sprintf('https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent', $model->getVersion()), [
53+
'headers' => [
54+
'x-goog-api-key' => $this->apiKey,
55+
],
56+
'json' => $body,
57+
]);
58+
}
59+
60+
/**
61+
* @throws TransportExceptionInterface
62+
* @throws ServerExceptionInterface
63+
* @throws RedirectionExceptionInterface
64+
* @throws DecodingExceptionInterface
65+
* @throws ClientExceptionInterface
66+
*/
67+
public function convert(ResponseInterface $response, array $options = []): LlmResponse
68+
{
69+
if ($options['stream'] ?? false) {
70+
return new StreamResponse($this->convertStream($response));
71+
}
72+
73+
$data = $response->toArray();
74+
75+
if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
76+
throw new RuntimeException('Response does not contain any content');
77+
}
78+
79+
return new TextResponse($data['candidates'][0]['content']['parts'][0]['text']);
80+
}
81+
82+
private function convertStream(ResponseInterface $response): \Generator
83+
{
84+
foreach ((new EventSourceHttpClient())->stream($response) as $chunk) {
85+
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
86+
continue;
87+
}
88+
89+
try {
90+
$data = $chunk->getArrayData();
91+
} catch (JsonException) {
92+
// try catch only needed for Symfony 6.4
93+
continue;
94+
}
95+
96+
if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
97+
continue;
98+
}
99+
100+
yield $data['candidates'][0]['content']['parts'][0]['text'];
101+
}
102+
}
103+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Google;
6+
7+
use PhpLlm\LlmChain\Platform;
8+
use Symfony\Component\HttpClient\EventSourceHttpClient;
9+
use Symfony\Contracts\HttpClient\HttpClientInterface;
10+
11+
final readonly class PlatformFactory
12+
{
13+
public static function create(
14+
#[\SensitiveParameter]
15+
string $apiKey,
16+
?HttpClientInterface $httpClient = null,
17+
): Platform {
18+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
19+
$responseHandler = new ModelHandler($httpClient, $apiKey);
20+
21+
return new Platform([$responseHandler], [$responseHandler]);
22+
}
23+
}

src/Model/Message/AssistantMessage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ public function jsonSerialize(): array
5050

5151
return $array;
5252
}
53+
54+
public function accept(MessageVisitor $visitor): array
55+
{
56+
return $visitor->visitAssistantMessage($this);
57+
}
5358
}

src/Model/Message/Content/Audio.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@ public function jsonSerialize(): array
3232
],
3333
];
3434
}
35+
36+
public function accept(ContentVisitor $visitor): array
37+
{
38+
return $visitor->visitAudio($this);
39+
}
3540
}

src/Model/Message/Content/Content.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66

77
interface Content extends \JsonSerializable
88
{
9+
public function accept(ContentVisitor $visitor): array;
910
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace PhpLlm\LlmChain\Model\Message\Content;
4+
5+
interface ContentVisitor
6+
{
7+
public function visitAudio(Audio $content): array;
8+
9+
public function visitImage(Image $content): array;
10+
11+
public function visitText(Text $content): array;
12+
}

0 commit comments

Comments
 (0)