Skip to content

Commit f1870bc

Browse files
committed
Decouple ModelClient and ResponseConverter implementations
1 parent 8b5514a commit f1870bc

24 files changed

+522
-275
lines changed

src/platform/src/Bridge/Azure/Meta/LlamaHandler.php renamed to src/platform/src/Bridge/Azure/Meta/LlamaModelClient.php

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,15 @@
1212
namespace Symfony\AI\Platform\Bridge\Azure\Meta;
1313

1414
use Symfony\AI\Platform\Bridge\Meta\Llama;
15-
use Symfony\AI\Platform\Exception\RuntimeException;
1615
use Symfony\AI\Platform\Model;
1716
use Symfony\AI\Platform\ModelClientInterface;
18-
use Symfony\AI\Platform\Response\ResponseInterface as LlmResponse;
19-
use Symfony\AI\Platform\Response\TextResponse;
20-
use Symfony\AI\Platform\ResponseConverterInterface;
2117
use Symfony\Contracts\HttpClient\HttpClientInterface;
2218
use Symfony\Contracts\HttpClient\ResponseInterface;
2319

2420
/**
2521
* @author Christopher Hertel <[email protected]>
2622
*/
27-
final readonly class LlamaHandler implements ModelClientInterface, ResponseConverterInterface
23+
final readonly class LlamaModelClient implements ModelClientInterface
2824
{
2925
public function __construct(
3026
private HttpClientInterface $httpClient,
@@ -50,15 +46,4 @@ public function request(Model $model, array|string $payload, array $options = []
5046
'json' => array_merge($options, $payload),
5147
]);
5248
}
53-
54-
public function convert(ResponseInterface $response, array $options = []): LlmResponse
55-
{
56-
$data = $response->toArray();
57-
58-
if (!isset($data['choices'][0]['message']['content'])) {
59-
throw new RuntimeException('Response does not contain output');
60-
}
61-
62-
return new TextResponse($data['choices'][0]['message']['content']);
63-
}
6449
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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\Azure\Meta;
13+
14+
use Symfony\AI\Platform\Bridge\Meta\Llama;
15+
use Symfony\AI\Platform\Exception\RuntimeException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\Response\ResponseInterface as LlmResponse;
18+
use Symfony\AI\Platform\Response\TextResponse;
19+
use Symfony\AI\Platform\ResponseConverterInterface;
20+
use Symfony\Contracts\HttpClient\ResponseInterface;
21+
22+
/**
23+
* @author Christopher Hertel <[email protected]>
24+
*/
25+
final readonly class LlamaResponseConverter implements ResponseConverterInterface
26+
{
27+
public function supports(Model $model): bool
28+
{
29+
return $model instanceof Llama;
30+
}
31+
32+
public function convert(ResponseInterface $response, array $options = []): LlmResponse
33+
{
34+
$data = $response->toArray();
35+
36+
if (!isset($data['choices'][0]['message']['content'])) {
37+
throw new RuntimeException('Response does not contain output');
38+
}
39+
40+
return new TextResponse($data['choices'][0]['message']['content']);
41+
}
42+
}

src/platform/src/Bridge/Azure/Meta/PlatformFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public static function create(
2828
?HttpClientInterface $httpClient = null,
2929
?Contract $contract = null,
3030
): Platform {
31-
$modelClient = new LlamaHandler($httpClient ?? HttpClient::create(), $baseUrl, $apiKey);
31+
$modelClient = new LlamaModelClient($httpClient ?? HttpClient::create(), $baseUrl, $apiKey);
3232

33-
return new Platform([$modelClient], [$modelClient], $contract);
33+
return new Platform([$modelClient], [new LlamaResponseConverter()], $contract);
3434
}
3535
}

src/platform/src/Bridge/Google/Embeddings/ModelClient.php

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,15 @@
1212
namespace Symfony\AI\Platform\Bridge\Google\Embeddings;
1313

1414
use Symfony\AI\Platform\Bridge\Google\Embeddings;
15-
use Symfony\AI\Platform\Exception\RuntimeException;
1615
use Symfony\AI\Platform\Model;
1716
use Symfony\AI\Platform\ModelClientInterface;
18-
use Symfony\AI\Platform\Response\VectorResponse;
19-
use Symfony\AI\Platform\ResponseConverterInterface;
20-
use Symfony\AI\Platform\Vector\Vector;
2117
use Symfony\Contracts\HttpClient\HttpClientInterface;
2218
use Symfony\Contracts\HttpClient\ResponseInterface;
2319

2420
/**
2521
* @author Valtteri R <[email protected]>
2622
*/
27-
final readonly class ModelClient implements ModelClientInterface, ResponseConverterInterface
23+
final readonly class ModelClient implements ModelClientInterface
2824
{
2925
public function __construct(
3026
private HttpClientInterface $httpClient,
@@ -61,20 +57,4 @@ public function request(Model $model, array|string $payload, array $options = []
6157
],
6258
]);
6359
}
64-
65-
public function convert(ResponseInterface $response, array $options = []): VectorResponse
66-
{
67-
$data = $response->toArray();
68-
69-
if (!isset($data['embeddings'])) {
70-
throw new RuntimeException('Response does not contain data');
71-
}
72-
73-
return new VectorResponse(
74-
...array_map(
75-
static fn (array $item): Vector => new Vector($item['values']),
76-
$data['embeddings'],
77-
),
78-
);
79-
}
8060
}
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\Google\Embeddings;
13+
14+
use Symfony\AI\Platform\Bridge\Google\Embeddings;
15+
use Symfony\AI\Platform\Exception\RuntimeException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\Response\VectorResponse;
18+
use Symfony\AI\Platform\ResponseConverterInterface;
19+
use Symfony\AI\Platform\Vector\Vector;
20+
use Symfony\Contracts\HttpClient\ResponseInterface;
21+
22+
/**
23+
* @author Valtteri R <[email protected]>
24+
*/
25+
final readonly class ResponseConverter implements ResponseConverterInterface
26+
{
27+
public function supports(Model $model): bool
28+
{
29+
return $model instanceof Embeddings;
30+
}
31+
32+
public function convert(ResponseInterface $response, array $options = []): VectorResponse
33+
{
34+
$data = $response->toArray();
35+
36+
if (!isset($data['embeddings'])) {
37+
throw new RuntimeException('Response does not contain data');
38+
}
39+
40+
return new VectorResponse(
41+
...array_map(
42+
static fn (array $item): Vector => new Vector($item['values']),
43+
$data['embeddings'],
44+
),
45+
);
46+
}
47+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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\Google\Gemini;
13+
14+
use Symfony\AI\Platform\Bridge\Google\Gemini;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelClientInterface;
17+
use Symfony\Component\HttpClient\EventSourceHttpClient;
18+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
use Symfony\Contracts\HttpClient\ResponseInterface;
21+
22+
/**
23+
* @author Roy Garrido
24+
*/
25+
final readonly class ModelClient implements ModelClientInterface
26+
{
27+
private EventSourceHttpClient $httpClient;
28+
29+
public function __construct(
30+
HttpClientInterface $httpClient,
31+
#[\SensitiveParameter] private string $apiKey,
32+
) {
33+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
34+
}
35+
36+
public function supports(Model $model): bool
37+
{
38+
return $model instanceof Gemini;
39+
}
40+
41+
/**
42+
* @throws TransportExceptionInterface
43+
*/
44+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
45+
{
46+
$url = \sprintf(
47+
'https://generativelanguage.googleapis.com/v1beta/models/%s:%s',
48+
$model->getName(),
49+
$options['stream'] ?? false ? 'streamGenerateContent' : 'generateContent',
50+
);
51+
52+
if (isset($options['response_format']['json_schema']['schema'])) {
53+
$options['responseMimeType'] = 'application/json';
54+
$options['responseJsonSchema'] = $options['response_format']['json_schema']['schema'];
55+
unset($options['response_format']);
56+
}
57+
58+
$generationConfig = ['generationConfig' => $options];
59+
unset($generationConfig['generationConfig']['stream']);
60+
unset($generationConfig['generationConfig']['tools']);
61+
unset($generationConfig['generationConfig']['server_tools']);
62+
63+
if (isset($options['tools'])) {
64+
$generationConfig['tools'][] = ['functionDeclarations' => $options['tools']];
65+
unset($options['tools']);
66+
}
67+
68+
foreach ($options['server_tools'] ?? [] as $tool => $params) {
69+
if (!$params) {
70+
continue;
71+
}
72+
73+
$generationConfig['tools'][] = [$tool => true === $params ? new \ArrayObject() : $params];
74+
}
75+
76+
return $this->httpClient->request('POST', $url, [
77+
'headers' => [
78+
'x-goog-api-key' => $this->apiKey,
79+
],
80+
'json' => array_merge($generationConfig, $payload),
81+
]);
82+
}
83+
}

src/platform/src/Bridge/Google/ModelHandler.php renamed to src/platform/src/Bridge/Google/Gemini/ResponseConverter.php

Lines changed: 3 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\AI\Platform\Bridge\Google;
12+
namespace Symfony\AI\Platform\Bridge\Google\Gemini;
1313

14+
use Symfony\AI\Platform\Bridge\Google\Gemini;
1415
use Symfony\AI\Platform\Exception\RuntimeException;
1516
use Symfony\AI\Platform\Model;
16-
use Symfony\AI\Platform\ModelClientInterface;
1717
use Symfony\AI\Platform\Response\Choice;
1818
use Symfony\AI\Platform\Response\ChoiceResponse;
1919
use Symfony\AI\Platform\Response\ResponseInterface as LlmResponse;
@@ -23,83 +23,18 @@
2323
use Symfony\AI\Platform\Response\ToolCallResponse;
2424
use Symfony\AI\Platform\ResponseConverterInterface;
2525
use Symfony\Component\HttpClient\EventSourceHttpClient;
26-
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
27-
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
28-
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
29-
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
30-
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
31-
use Symfony\Contracts\HttpClient\HttpClientInterface;
3226
use Symfony\Contracts\HttpClient\ResponseInterface;
3327

3428
/**
3529
* @author Roy Garrido
3630
*/
37-
final readonly class ModelHandler implements ModelClientInterface, ResponseConverterInterface
31+
final readonly class ResponseConverter implements ResponseConverterInterface
3832
{
39-
private EventSourceHttpClient $httpClient;
40-
41-
public function __construct(
42-
HttpClientInterface $httpClient,
43-
#[\SensitiveParameter] private string $apiKey,
44-
) {
45-
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
46-
}
47-
4833
public function supports(Model $model): bool
4934
{
5035
return $model instanceof Gemini;
5136
}
5237

53-
/**
54-
* @throws TransportExceptionInterface
55-
*/
56-
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
57-
{
58-
$url = \sprintf(
59-
'https://generativelanguage.googleapis.com/v1beta/models/%s:%s',
60-
$model->getName(),
61-
$options['stream'] ?? false ? 'streamGenerateContent' : 'generateContent',
62-
);
63-
64-
if (isset($options['response_format']['json_schema']['schema'])) {
65-
$options['responseMimeType'] = 'application/json';
66-
$options['responseJsonSchema'] = $options['response_format']['json_schema']['schema'];
67-
unset($options['response_format']);
68-
}
69-
70-
$generationConfig = ['generationConfig' => $options];
71-
unset($generationConfig['generationConfig']['stream']);
72-
unset($generationConfig['generationConfig']['tools']);
73-
unset($generationConfig['generationConfig']['server_tools']);
74-
75-
if (isset($options['tools'])) {
76-
$generationConfig['tools'][] = ['functionDeclarations' => $options['tools']];
77-
unset($options['tools']);
78-
}
79-
80-
foreach ($options['server_tools'] ?? [] as $tool => $params) {
81-
if (!$params) {
82-
continue;
83-
}
84-
85-
$generationConfig['tools'][] = [$tool => true === $params ? new \ArrayObject() : $params];
86-
}
87-
88-
return $this->httpClient->request('POST', $url, [
89-
'headers' => [
90-
'x-goog-api-key' => $this->apiKey,
91-
],
92-
'json' => array_merge($generationConfig, $payload),
93-
]);
94-
}
95-
96-
/**
97-
* @throws TransportExceptionInterface
98-
* @throws ServerExceptionInterface
99-
* @throws RedirectionExceptionInterface
100-
* @throws DecodingExceptionInterface
101-
* @throws ClientExceptionInterface
102-
*/
10338
public function convert(ResponseInterface $response, array $options = []): LlmResponse
10439
{
10540
if ($options['stream'] ?? false) {

src/platform/src/Bridge/Google/PlatformFactory.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
namespace Symfony\AI\Platform\Bridge\Google;
1313

1414
use Symfony\AI\Platform\Bridge\Google\Contract\GoogleContract;
15-
use Symfony\AI\Platform\Bridge\Google\Embeddings\ModelClient;
15+
use Symfony\AI\Platform\Bridge\Google\Embeddings\ModelClient as EmbeddingsModelClient;
16+
use Symfony\AI\Platform\Bridge\Google\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
17+
use Symfony\AI\Platform\Bridge\Google\Gemini\ModelClient as GeminiModelClient;
18+
use Symfony\AI\Platform\Bridge\Google\Gemini\ResponseConverter as GeminiResponseConverter;
1619
use Symfony\AI\Platform\Contract;
1720
use Symfony\AI\Platform\Platform;
1821
use Symfony\Component\HttpClient\EventSourceHttpClient;
@@ -30,12 +33,10 @@ public static function create(
3033
?Contract $contract = null,
3134
): Platform {
3235
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
33-
$responseHandler = new ModelHandler($httpClient, $apiKey);
34-
$embeddings = new ModelClient($httpClient, $apiKey);
3536

3637
return new Platform(
37-
[$responseHandler, $embeddings],
38-
[$responseHandler, $embeddings],
38+
[new EmbeddingsModelClient($httpClient, $apiKey), new GeminiModelClient($httpClient, $apiKey)],
39+
[new EmbeddingsResponseConverter(), new GeminiResponseConverter()],
3940
$contract ?? GoogleContract::create(),
4041
);
4142
}

0 commit comments

Comments
 (0)