Skip to content

Commit 3929446

Browse files
committed
minor #135 [Platform] Decouple ModelClient and ResponseConverter implementations (chr-hertel)
This PR was merged into the main branch. Discussion ---------- [Platform] Decouple `ModelClient` and `ResponseConverter` implementations | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | Docs? | no | Issues | | License | MIT Being more strict on decoupling ModelClient and ResponseConverter implementations will help refactoring that part in the next steps. It basically moves code from classes handling both to two separate implementations. Commits ------- f1870bc Decouple ModelClient and ResponseConverter implementations
2 parents 4f7be09 + f1870bc commit 3929446

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)