Skip to content

Commit 4406c4f

Browse files
committed
refactor: add extension point to payload contract handling
1 parent be5d914 commit 4406c4f

21 files changed

+281
-55
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\DallE;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE;
8+
use PhpLlm\LlmChain\Model\Model;
9+
use PhpLlm\LlmChain\Platform\Contract\Extension;
10+
11+
final class ContractExtension implements Extension
12+
{
13+
public function supports(Model $model): bool
14+
{
15+
return $model instanceof DallE;
16+
}
17+
18+
public function registerTypes(): array
19+
{
20+
return [
21+
'string' => 'handleInput',
22+
];
23+
}
24+
25+
public function handleInput(string $input): array
26+
{
27+
return [
28+
'prompt' => $input,
29+
];
30+
}
31+
}

src/Bridge/OpenAI/DallE/ModelClient.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,17 @@ public function __construct(
2828
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
2929
}
3030

31-
public function supports(Model $model, array|string|object $input): bool
31+
public function supports(Model $model): bool
3232
{
3333
return $model instanceof DallE;
3434
}
3535

36-
public function request(Model $model, object|array|string $input, array $options = []): HttpResponse
36+
public function request(Model $model, array $payload, array $options = []): HttpResponse
3737
{
3838
return $this->httpClient->request('POST', 'https://api.openai.com/v1/images/generations', [
3939
'auth_bearer' => $this->apiKey,
40-
'json' => \array_merge($options, [
40+
'json' => \array_merge($options, $payload, [
4141
'model' => $model->getName(),
42-
'prompt' => $input,
4342
]),
4443
]);
4544
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\Embeddings;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings;
8+
use PhpLlm\LlmChain\Model\Model;
9+
use PhpLlm\LlmChain\Platform\Contract\Extension;
10+
11+
final class ContractExtension implements Extension
12+
{
13+
public function supports(Model $model): bool
14+
{
15+
return $model instanceof Embeddings;
16+
}
17+
18+
public function registerTypes(): array
19+
{
20+
return [
21+
'string' => 'handleInput',
22+
];
23+
}
24+
25+
public function handleInput(string $input): array
26+
{
27+
return [
28+
'input' => $input,
29+
];
30+
}
31+
}

src/Bridge/OpenAI/Embeddings/ModelClient.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@ public function __construct(
2222
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
2323
}
2424

25-
public function supports(Model $model, array|string|object $input): bool
25+
public function supports(Model $model): bool
2626
{
2727
return $model instanceof Embeddings;
2828
}
2929

30-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
30+
public function request(Model $model, array $payload, array $options = []): ResponseInterface
3131
{
3232
return $this->httpClient->request('POST', 'https://api.openai.com/v1/embeddings', [
3333
'auth_bearer' => $this->apiKey,
34-
'json' => array_merge($model->getOptions(), $options, [
34+
'json' => array_merge($options, $payload, [
3535
'model' => $model->getName(),
36-
'input' => $input,
3736
]),
3837
]);
3938
}

src/Bridge/OpenAI/Embeddings/ResponseConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
final class ResponseConverter implements PlatformResponseConverter
1616
{
17-
public function supports(Model $model, array|string|object $input): bool
17+
public function supports(Model $model): bool
1818
{
1919
return $model instanceof Embeddings;
2020
}

src/Bridge/OpenAI/GPT/ModelClient.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,26 @@
1818

1919
public function __construct(
2020
HttpClientInterface $httpClient,
21-
#[\SensitiveParameter] private string $apiKey,
21+
#[\SensitiveParameter]
22+
private string $apiKey,
2223
) {
2324
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
2425
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
2526
Assert::startsWith($apiKey, 'sk-', 'The API key must start with "sk-".');
2627
}
2728

28-
public function supports(Model $model, array|string|object $input): bool
29+
public function supports(Model $model): bool
2930
{
3031
return $model instanceof GPT;
3132
}
3233

33-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
34+
public function request(Model $model, array $payload, array $options = []): ResponseInterface
3435
{
3536
return $this->httpClient->request('POST', 'https://api.openai.com/v1/chat/completions', [
3637
'auth_bearer' => $this->apiKey,
3738
'json' => array_merge($options, [
3839
'model' => $model->getName(),
39-
'messages' => $input,
40+
'messages' => $payload,
4041
]),
4142
]);
4243
}

src/Bridge/OpenAI/GPT/ResponseConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
final class ResponseConverter implements PlatformResponseConverter
2626
{
27-
public function supports(Model $model, array|string|object $input): bool
27+
public function supports(Model $model): bool
2828
{
2929
return $model instanceof GPT;
3030
}

src/Bridge/OpenAI/PlatformFactory.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
namespace PhpLlm\LlmChain\Bridge\OpenAI;
66

7+
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\ContractExtension as DallEContractExtension;
78
use PhpLlm\LlmChain\Bridge\OpenAI\DallE\ModelClient as DallEModelClient;
9+
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings\ContractExtension as EmbeddingsContractExtension;
810
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings\ModelClient as EmbeddingsModelClient;
911
use PhpLlm\LlmChain\Bridge\OpenAI\Embeddings\ResponseConverter as EmbeddingsResponseConverter;
1012
use PhpLlm\LlmChain\Bridge\OpenAI\GPT\ModelClient as GPTModelClient;
1113
use PhpLlm\LlmChain\Bridge\OpenAI\GPT\ResponseConverter as GPTResponseConverter;
14+
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper\ContractExtension as WhisperContractExtension;
1215
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper\ModelClient as WhisperModelClient;
1316
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper\ResponseConverter as WhisperResponseConverter;
1417
use PhpLlm\LlmChain\Platform;
18+
use PhpLlm\LlmChain\Platform\Contract;
1519
use Symfony\Component\HttpClient\EventSourceHttpClient;
1620
use Symfony\Contracts\HttpClient\HttpClientInterface;
1721

@@ -39,6 +43,11 @@ public static function create(
3943
$dallEModelClient,
4044
new WhisperResponseConverter(),
4145
],
46+
Contract::create(
47+
new DallEContractExtension(),
48+
new EmbeddingsContractExtension(),
49+
new WhisperContractExtension(),
50+
),
4251
);
4352
}
4453
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
6+
7+
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
8+
use PhpLlm\LlmChain\Model\Message\Content\Audio;
9+
use PhpLlm\LlmChain\Model\Model;
10+
use PhpLlm\LlmChain\Platform\Contract\Extension;
11+
12+
final class ContractExtension implements Extension
13+
{
14+
public function supports(Model $model): bool
15+
{
16+
return $model instanceof Whisper;
17+
}
18+
19+
public function registerTypes(): array
20+
{
21+
return [
22+
Audio::class => 'handleAudioInput',
23+
];
24+
}
25+
26+
public function handleAudioInput(Audio $audio): array
27+
{
28+
return [
29+
'file' => $audio->asResource(),
30+
];
31+
}
32+
}

src/Bridge/OpenAI/Whisper/ModelClient.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
66

77
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
8-
use PhpLlm\LlmChain\Model\Message\Content\Audio;
98
use PhpLlm\LlmChain\Model\Model;
109
use PhpLlm\LlmChain\Platform\ModelClient as BaseModelClient;
1110
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -22,22 +21,17 @@ public function __construct(
2221
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
2322
}
2423

25-
public function supports(Model $model, object|array|string $input): bool
24+
public function supports(Model $model): bool
2625
{
27-
return $model instanceof Whisper && $input instanceof Audio;
26+
return $model instanceof Whisper;
2827
}
2928

30-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
29+
public function request(Model $model, array $payload, array $options = []): ResponseInterface
3130
{
32-
assert($input instanceof Audio);
33-
3431
return $this->httpClient->request('POST', 'https://api.openai.com/v1/audio/transcriptions', [
3532
'auth_bearer' => $this->apiKey,
3633
'headers' => ['Content-Type' => 'multipart/form-data'],
37-
'body' => array_merge($options, $model->getOptions(), [
38-
'model' => $model->getName(),
39-
'file' => $input->asResource(),
40-
]),
34+
'body' => array_merge($options, $payload, ['model' => $model->getName()]),
4135
]);
4236
}
4337
}

src/Bridge/OpenAI/Whisper/ResponseConverter.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
66

77
use PhpLlm\LlmChain\Bridge\OpenAI\Whisper;
8-
use PhpLlm\LlmChain\Model\Message\Content\Audio;
98
use PhpLlm\LlmChain\Model\Model;
109
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
1110
use PhpLlm\LlmChain\Model\Response\TextResponse;
@@ -14,9 +13,9 @@
1413

1514
final class ResponseConverter implements BaseResponseConverter
1615
{
17-
public function supports(Model $model, object|array|string $input): bool
16+
public function supports(Model $model): bool
1817
{
19-
return $model instanceof Whisper && $input instanceof Audio;
18+
return $model instanceof Whisper;
2019
}
2120

2221
public function convert(HttpResponse $response, array $options = []): LlmResponse

src/Model/Message/AssistantMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function hasToolCalls(): bool
3737
public function jsonSerialize(): array
3838
{
3939
$array = [
40-
'role' => Role::Assistant,
40+
'role' => Role::Assistant->value,
4141
];
4242

4343
if (null !== $this->content) {

src/Model/Message/SystemMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function getRole(): Role
2424
public function jsonSerialize(): array
2525
{
2626
return [
27-
'role' => Role::System,
27+
'role' => Role::System->value,
2828
'content' => $this->content,
2929
];
3030
}

src/Model/Message/ToolCallMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function getRole(): Role
2929
public function jsonSerialize(): array
3030
{
3131
return [
32-
'role' => Role::ToolCall,
32+
'role' => Role::ToolCall->value,
3333
'content' => $this->content,
3434
'tool_call_id' => $this->toolCall->id,
3535
];

src/Model/Message/UserMessage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function hasImageContent(): bool
5858
*/
5959
public function jsonSerialize(): array
6060
{
61-
$array = ['role' => Role::User];
61+
$array = ['role' => Role::User->value];
6262
if (1 === count($this->content) && $this->content[0] instanceof Text) {
6363
$array['content'] = $this->content[0]->text;
6464

src/Platform.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpLlm\LlmChain\Model\Model;
99
use PhpLlm\LlmChain\Model\Response\AsyncResponse;
1010
use PhpLlm\LlmChain\Model\Response\ResponseInterface;
11+
use PhpLlm\LlmChain\Platform\Contract;
1112
use PhpLlm\LlmChain\Platform\ModelClient;
1213
use PhpLlm\LlmChain\Platform\ResponseConverter;
1314
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
@@ -28,44 +29,47 @@
2829
* @param iterable<ModelClient> $modelClients
2930
* @param iterable<ResponseConverter> $responseConverter
3031
*/
31-
public function __construct(iterable $modelClients, iterable $responseConverter)
32-
{
32+
public function __construct(
33+
iterable $modelClients,
34+
iterable $responseConverter,
35+
private Contract $contract = new Contract([]),
36+
) {
3337
$this->modelClients = $modelClients instanceof \Traversable ? iterator_to_array($modelClients) : $modelClients;
3438
$this->responseConverter = $responseConverter instanceof \Traversable ? iterator_to_array($responseConverter) : $responseConverter;
3539
}
3640

3741
public function request(Model $model, array|string|object $input, array $options = []): ResponseInterface
3842
{
43+
$payload = $this->contract->convertRequestPayload($input, $model);
3944
$options = array_merge($model->getOptions(), $options);
4045

41-
$response = $this->doRequest($model, $input, $options);
46+
$response = $this->doRequest($model, $payload, $options);
4247

43-
return $this->convertResponse($model, $input, $response, $options);
48+
return $this->convertResponse($model, $response, $options);
4449
}
4550

4651
/**
47-
* @param array<mixed>|string|object $input
48-
* @param array<string, mixed> $options
52+
* @param array<string, mixed> $payload
53+
* @param array<string, mixed> $options
4954
*/
50-
private function doRequest(Model $model, array|string|object $input, array $options = []): HttpResponse
55+
private function doRequest(Model $model, array $payload, array $options = []): HttpResponse
5156
{
5257
foreach ($this->modelClients as $modelClient) {
53-
if ($modelClient->supports($model, $input)) {
54-
return $modelClient->request($model, $input, $options);
58+
if ($modelClient->supports($model)) {
59+
return $modelClient->request($model, $payload, $options);
5560
}
5661
}
5762

5863
throw new RuntimeException('No response factory registered for model "'.$model::class.'" with given input.');
5964
}
6065

6166
/**
62-
* @param array<mixed>|string|object $input
63-
* @param array<string, mixed> $options
67+
* @param array<string, mixed> $options
6468
*/
65-
private function convertResponse(Model $model, object|array|string $input, HttpResponse $response, array $options): ResponseInterface
69+
private function convertResponse(Model $model, HttpResponse $response, array $options): ResponseInterface
6670
{
6771
foreach ($this->responseConverter as $responseConverter) {
68-
if ($responseConverter->supports($model, $input)) {
72+
if ($responseConverter->supports($model)) {
6973
return new AsyncResponse($responseConverter, $response, $options);
7074
}
7175
}

0 commit comments

Comments
 (0)