Skip to content

Commit 2b5477b

Browse files
committed
refactor: switch from JsonSerializable to normalizer
1 parent e7faba1 commit 2b5477b

File tree

89 files changed

+2067
-679
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+2067
-679
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
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Model\Message\AssistantMessage;
8+
use PhpLlm\LlmChain\Model\Response\ToolCall;
9+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
10+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
11+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
12+
13+
final class AssistantMessageNormalizer implements NormalizerInterface, NormalizerAwareInterface
14+
{
15+
use NormalizerAwareTrait;
16+
17+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
18+
{
19+
return $data instanceof AssistantMessage && $data->hasToolCalls();
20+
}
21+
22+
public function getSupportedTypes(?string $format): array
23+
{
24+
return [
25+
AssistantMessage::class => true,
26+
];
27+
}
28+
29+
/**
30+
* @param AssistantMessage $data
31+
*
32+
* @return array{
33+
* role: 'assistant',
34+
* content: list<array{
35+
* type: 'tool_use',
36+
* id: string,
37+
* name: string,
38+
* input: array<string, mixed>
39+
* }>
40+
* }
41+
*/
42+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
43+
{
44+
return [
45+
'role' => 'assistant',
46+
'content' => array_map(static function (ToolCall $toolCall) {
47+
return [
48+
'type' => 'tool_use',
49+
'id' => $toolCall->id,
50+
'name' => $toolCall->name,
51+
'input' => empty($toolCall->arguments) ? new \stdClass() : $toolCall->arguments,
52+
];
53+
}, $data->toolCalls),
54+
];
55+
}
56+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
8+
use PhpLlm\LlmChain\Model\Model;
9+
use PhpLlm\LlmChain\Platform\Contract;
10+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
11+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
12+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
13+
14+
final class MessageBagNormalizer implements NormalizerInterface, NormalizerAwareInterface
15+
{
16+
use NormalizerAwareTrait;
17+
18+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
19+
{
20+
return $data instanceof MessageBagInterface;
21+
}
22+
23+
public function getSupportedTypes(?string $format): array
24+
{
25+
return [
26+
MessageBagInterface::class => true,
27+
];
28+
}
29+
30+
/**
31+
* @param MessageBagInterface $data
32+
*
33+
* @return array{
34+
* messages: array<string, mixed>,
35+
* model?: string,
36+
* system?: string,
37+
* }
38+
*/
39+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
40+
{
41+
$array = [
42+
'messages' => $this->normalizer->normalize($data->withoutSystemMessage()->getMessages(), $format, $context),
43+
];
44+
45+
if (null !== $system = $data->getSystemMessage()) {
46+
$array['system'] = $system->content;
47+
}
48+
49+
if (isset($context[Contract::CONTEXT_MODEL]) && $context[Contract::CONTEXT_MODEL] instanceof Model) {
50+
$array['model'] = $context[Contract::CONTEXT_MODEL]->getName();
51+
}
52+
53+
return $array;
54+
}
55+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Model\Message\ToolCallMessage;
8+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
9+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
10+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
11+
12+
final class ToolCallMessageNormalizer implements NormalizerInterface, NormalizerAwareInterface
13+
{
14+
use NormalizerAwareTrait;
15+
16+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
17+
{
18+
return $data instanceof ToolCallMessage;
19+
}
20+
21+
public function getSupportedTypes(?string $format): array
22+
{
23+
return [
24+
ToolCallMessage::class => true,
25+
];
26+
}
27+
28+
/**
29+
* @param ToolCallMessage $data
30+
*
31+
* @return array{
32+
* role: 'user',
33+
* content: list<array{
34+
* type: 'tool_result',
35+
* tool_use_id: string,
36+
* content: string,
37+
* }>
38+
* }
39+
*/
40+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
41+
{
42+
return [
43+
'role' => 'user',
44+
'content' => [
45+
[
46+
'type' => 'tool_result',
47+
'tool_use_id' => $data->toolCall->id,
48+
'content' => $data->content,
49+
],
50+
],
51+
];
52+
}
53+
}

src/Bridge/Anthropic/ModelHandler.php

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
use PhpLlm\LlmChain\Chain\Toolbox\Metadata;
88
use PhpLlm\LlmChain\Exception\RuntimeException;
9-
use PhpLlm\LlmChain\Model\Message\AssistantMessage;
10-
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
11-
use PhpLlm\LlmChain\Model\Message\MessageInterface;
12-
use PhpLlm\LlmChain\Model\Message\ToolCallMessage;
139
use PhpLlm\LlmChain\Model\Model;
1410
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
1511
use PhpLlm\LlmChain\Model\Response\StreamResponse;
@@ -23,7 +19,6 @@
2319
use Symfony\Component\HttpClient\Exception\JsonException;
2420
use Symfony\Contracts\HttpClient\HttpClientInterface;
2521
use Symfony\Contracts\HttpClient\ResponseInterface;
26-
use Webmozart\Assert\Assert;
2722

2823
final readonly class ModelHandler implements ModelClient, ResponseConverter
2924
{
@@ -37,15 +32,13 @@ public function __construct(
3732
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
3833
}
3934

40-
public function supports(Model $model, array|string|object $input): bool
35+
public function supports(Model $model): bool
4136
{
42-
return $model instanceof Claude && $input instanceof MessageBagInterface;
37+
return $model instanceof Claude;
4338
}
4439

45-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
40+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
4641
{
47-
Assert::isInstanceOf($input, MessageBagInterface::class);
48-
4942
if (isset($options['tools'])) {
5043
$tools = $options['tools'];
5144
$options['tools'] = [];
@@ -61,51 +54,12 @@ public function request(Model $model, object|array|string $input, array $options
6154
$options['tool_choice'] = ['type' => 'auto'];
6255
}
6356

64-
$body = [
65-
'model' => $model->getName(),
66-
'messages' => $input->withoutSystemMessage()->jsonSerialize(),
67-
];
68-
69-
$body['messages'] = array_map(static function (MessageInterface $message) {
70-
if ($message instanceof ToolCallMessage) {
71-
return [
72-
'role' => 'user',
73-
'content' => [
74-
[
75-
'type' => 'tool_result',
76-
'tool_use_id' => $message->toolCall->id,
77-
'content' => $message->content,
78-
],
79-
],
80-
];
81-
}
82-
if ($message instanceof AssistantMessage && $message->hasToolCalls()) {
83-
return [
84-
'role' => 'assistant',
85-
'content' => array_map(static function (ToolCall $toolCall) {
86-
return [
87-
'type' => 'tool_use',
88-
'id' => $toolCall->id,
89-
'name' => $toolCall->name,
90-
'input' => empty($toolCall->arguments) ? new \stdClass() : $toolCall->arguments,
91-
];
92-
}, $message->toolCalls),
93-
];
94-
}
95-
96-
return $message;
97-
}, $body['messages']);
98-
99-
if ($system = $input->getSystemMessage()) {
100-
$body['system'] = $system->content;
101-
}
102-
10357
return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [
10458
'headers' => [
10559
'x-api-key' => $this->apiKey,
10660
'anthropic-version' => $this->version,
10761
],
108-
'json' => array_merge($options, $body),
62+
'json' => array_merge($options, $payload),
10963
]);
11064
}
11165

src/Bridge/Anthropic/PlatformFactory.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
namespace PhpLlm\LlmChain\Bridge\Anthropic;
66

7+
use PhpLlm\LlmChain\Bridge\Anthropic\Contract\AssistantMessageNormalizer;
8+
use PhpLlm\LlmChain\Bridge\Anthropic\Contract\MessageBagNormalizer;
9+
use PhpLlm\LlmChain\Bridge\Anthropic\Contract\ToolCallMessageNormalizer;
710
use PhpLlm\LlmChain\Platform;
11+
use PhpLlm\LlmChain\Platform\Contract;
812
use Symfony\Component\HttpClient\EventSourceHttpClient;
913
use Symfony\Contracts\HttpClient\HttpClientInterface;
1014

@@ -19,6 +23,10 @@ public static function create(
1923
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
2024
$responseHandler = new ModelHandler($httpClient, $apiKey, $version);
2125

22-
return new Platform([$responseHandler], [$responseHandler]);
26+
return new Platform([$responseHandler], [$responseHandler], Contract::create(
27+
new AssistantMessageNormalizer(),
28+
new MessageBagNormalizer(),
29+
new ToolCallMessageNormalizer())
30+
);
2331
}
2432
}

src/Bridge/Azure/Meta/LlamaHandler.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@
66

77
use PhpLlm\LlmChain\Bridge\Meta\Llama;
88
use PhpLlm\LlmChain\Exception\RuntimeException;
9-
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
109
use PhpLlm\LlmChain\Model\Model;
1110
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
1211
use PhpLlm\LlmChain\Model\Response\TextResponse;
1312
use PhpLlm\LlmChain\Platform\ModelClient;
1413
use PhpLlm\LlmChain\Platform\ResponseConverter;
1514
use Symfony\Contracts\HttpClient\HttpClientInterface;
1615
use Symfony\Contracts\HttpClient\ResponseInterface;
17-
use Webmozart\Assert\Assert;
1816

1917
final readonly class LlamaHandler implements ModelClient, ResponseConverter
2018
{
@@ -25,25 +23,21 @@ public function __construct(
2523
) {
2624
}
2725

28-
public function supports(Model $model, object|array|string $input): bool
26+
public function supports(Model $model): bool
2927
{
30-
return $model instanceof Llama && $input instanceof MessageBagInterface;
28+
return $model instanceof Llama;
3129
}
3230

33-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
31+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
3432
{
35-
Assert::isInstanceOf($input, MessageBagInterface::class);
3633
$url = sprintf('https://%s/chat/completions', $this->baseUrl);
3734

3835
return $this->httpClient->request('POST', $url, [
3936
'headers' => [
4037
'Content-Type' => 'application/json',
4138
'Authorization' => $this->apiKey,
4239
],
43-
'json' => array_merge($options, [
44-
'model' => $model->getName(),
45-
'messages' => $input,
46-
]),
40+
'json' => array_merge($options, $payload),
4741
]);
4842
}
4943

src/Bridge/Azure/OpenAI/EmbeddingsModelClient.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ public function __construct(
3131
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
3232
}
3333

34-
public function supports(Model $model, object|array|string $input): bool
34+
public function supports(Model $model): bool
3535
{
3636
return $model instanceof Embeddings;
3737
}
3838

39-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
39+
public function request(Model $model, object|array|string $payload, array $options = []): ResponseInterface
4040
{
4141
$url = sprintf('https://%s/openai/deployments/%s/embeddings', $this->baseUrl, $this->deployment);
4242

@@ -47,7 +47,7 @@ public function request(Model $model, object|array|string $input, array $options
4747
'query' => ['api-version' => $this->apiVersion],
4848
'json' => array_merge($options, [
4949
'model' => $model->getName(),
50-
'input' => $input,
50+
'input' => $payload,
5151
]),
5252
]);
5353
}

src/Bridge/Azure/OpenAI/GPTModelClient.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ public function __construct(
3131
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
3232
}
3333

34-
public function supports(Model $model, object|array|string $input): bool
34+
public function supports(Model $model): bool
3535
{
3636
return $model instanceof GPT;
3737
}
3838

39-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
39+
public function request(Model $model, object|array|string $payload, array $options = []): ResponseInterface
4040
{
4141
$url = sprintf('https://%s/openai/deployments/%s/chat/completions', $this->baseUrl, $this->deployment);
4242

@@ -45,10 +45,7 @@ public function request(Model $model, object|array|string $input, array $options
4545
'api-key' => $this->apiKey,
4646
],
4747
'query' => ['api-version' => $this->apiVersion],
48-
'json' => array_merge($options, [
49-
'model' => $model->getName(),
50-
'messages' => $input,
51-
]),
48+
'json' => array_merge($options, $payload),
5249
]);
5350
}
5451
}

0 commit comments

Comments
 (0)