Skip to content

Commit d090593

Browse files
committed
refactor: add extension point to payload contract handling
1 parent e7faba1 commit d090593

27 files changed

+414
-65
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"mongodb/mongodb": "^1.21",
4646
"php-cs-fixer/shim": "^3.70",
4747
"phpstan/phpstan": "^2.0",
48+
"phpstan/phpstan-symfony": "^2.0",
4849
"phpstan/phpstan-webmozart-assert": "^2.0",
4950
"phpunit/phpunit": "^11.5",
5051
"probots-io/pinecone-php": "^1.0",

phpstan.dist.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
includes:
22
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
3+
- vendor/phpstan/phpstan-symfony/extension.neon
34

45
parameters:
56
level: 6

src/Bridge/Anthropic/ModelHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
3838
}
3939

40-
public function supports(Model $model, array|string|object $input): bool
40+
public function supports(Model $model): bool
4141
{
4242
return $model instanceof Claude && $input instanceof MessageBagInterface;
4343
}
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ public function hasToolCalls(): bool
2929

3030
/**
3131
* @return array{
32-
* role: Role::Assistant,
32+
* role: 'assistant',
3333
* content: ?string,
3434
* tool_calls?: ToolCall[],
3535
* }
3636
*/
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ public function getRole(): Role
1717

1818
/**
1919
* @return array{
20-
* role: Role::System,
20+
* role: 'system',
2121
* content: string
2222
* }
2323
*/
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public function getRole(): Role
2121

2222
/**
2323
* @return array{
24-
* role: Role::ToolCall,
24+
* role: 'tool',
2525
* content: string,
2626
* tool_call_id: string,
2727
* }
2828
*/
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ public function hasImageContent(): bool
5252

5353
/**
5454
* @return array{
55-
* role: Role::User,
55+
* role: 'user',
5656
* content: string|list<Content>
5757
* }
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

0 commit comments

Comments
 (0)