Skip to content

Commit 1541b6b

Browse files
committed
refactor: switch from JsonSerializable to normalizer
1 parent 964e4a8 commit 1541b6b

File tree

90 files changed

+2116
-704
lines changed

Some content is hidden

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

90 files changed

+2116
-704
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"mongodb/mongodb": "^1.21",
4747
"php-cs-fixer/shim": "^3.70",
4848
"phpstan/phpstan": "^2.0",
49+
"phpstan/phpstan-symfony": "^2.0",
4950
"phpstan/phpstan-webmozart-assert": "^2.0",
5051
"phpunit/phpunit": "^11.5",
5152
"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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpLlm\LlmChain\Bridge\Anthropic\Contract;
6+
7+
use PhpLlm\LlmChain\Model\Message\Content\Image;
8+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
9+
10+
use function Symfony\Component\String\u;
11+
12+
final class ImageNormalizer implements NormalizerInterface
13+
{
14+
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
15+
{
16+
return $data instanceof Image;
17+
}
18+
19+
public function getSupportedTypes(?string $format): array
20+
{
21+
return [
22+
Image::class => true,
23+
];
24+
}
25+
26+
/**
27+
* @param Image $data
28+
*
29+
* @return array{
30+
* type: 'image',
31+
* source: array{
32+
* type: 'base64',
33+
* media_type: string,
34+
* data: string
35+
* }
36+
* }
37+
*/
38+
public function normalize(mixed $data, ?string $format = null, array $context = []): array
39+
{
40+
return [
41+
'type' => 'image',
42+
'source' => [
43+
'type' => 'base64',
44+
'media_type' => u($data->getFormat())->replace('jpg', 'jpeg')->toString(),
45+
'data' => $data->asBase64(),
46+
],
47+
];
48+
}
49+
}
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 & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +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\Content\Content;
11-
use PhpLlm\LlmChain\Model\Message\Content\Image;
12-
use PhpLlm\LlmChain\Model\Message\MessageBagInterface;
13-
use PhpLlm\LlmChain\Model\Message\MessageInterface;
14-
use PhpLlm\LlmChain\Model\Message\ToolCallMessage;
15-
use PhpLlm\LlmChain\Model\Message\UserMessage;
169
use PhpLlm\LlmChain\Model\Model;
1710
use PhpLlm\LlmChain\Model\Response\ResponseInterface as LlmResponse;
1811
use PhpLlm\LlmChain\Model\Response\StreamResponse;
@@ -26,9 +19,6 @@
2619
use Symfony\Component\HttpClient\Exception\JsonException;
2720
use Symfony\Contracts\HttpClient\HttpClientInterface;
2821
use Symfony\Contracts\HttpClient\ResponseInterface;
29-
use Webmozart\Assert\Assert;
30-
31-
use function Symfony\Component\String\u;
3222

3323
final readonly class ModelHandler implements ModelClient, ResponseConverter
3424
{
@@ -42,15 +32,13 @@ public function __construct(
4232
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
4333
}
4434

45-
public function supports(Model $model, array|string|object $input): bool
35+
public function supports(Model $model): bool
4636
{
47-
return $model instanceof Claude && $input instanceof MessageBagInterface;
37+
return $model instanceof Claude;
4838
}
4939

50-
public function request(Model $model, object|array|string $input, array $options = []): ResponseInterface
40+
public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
5141
{
52-
Assert::isInstanceOf($input, MessageBagInterface::class);
53-
5442
if (isset($options['tools'])) {
5543
$tools = $options['tools'];
5644
$options['tools'] = [];
@@ -66,71 +54,12 @@ public function request(Model $model, object|array|string $input, array $options
6654
$options['tool_choice'] = ['type' => 'auto'];
6755
}
6856

69-
$body = [
70-
'model' => $model->getName(),
71-
'messages' => $input->withoutSystemMessage()->jsonSerialize(),
72-
];
73-
74-
$body['messages'] = array_map(static function (MessageInterface $message) {
75-
if ($message instanceof ToolCallMessage) {
76-
return [
77-
'role' => 'user',
78-
'content' => [
79-
[
80-
'type' => 'tool_result',
81-
'tool_use_id' => $message->toolCall->id,
82-
'content' => $message->content,
83-
],
84-
],
85-
];
86-
}
87-
if ($message instanceof AssistantMessage && $message->hasToolCalls()) {
88-
return [
89-
'role' => 'assistant',
90-
'content' => array_map(static function (ToolCall $toolCall) {
91-
return [
92-
'type' => 'tool_use',
93-
'id' => $toolCall->id,
94-
'name' => $toolCall->name,
95-
'input' => empty($toolCall->arguments) ? new \stdClass() : $toolCall->arguments,
96-
];
97-
}, $message->toolCalls),
98-
];
99-
}
100-
if ($message instanceof UserMessage && $message->hasImageContent()) {
101-
// make sure images are encoded for Bedrock invocation
102-
return [
103-
'role' => 'user',
104-
'content' => array_map(static function (Content $content) {
105-
if ($content instanceof Image) {
106-
return [
107-
'type' => 'image',
108-
'source' => [
109-
'type' => 'base64',
110-
'media_type' => u($content->getFormat())->replace('jpg', 'jpeg')->toString(),
111-
'data' => $content->asBase64(),
112-
],
113-
];
114-
}
115-
116-
return $content;
117-
}, $message->content),
118-
];
119-
}
120-
121-
return $message;
122-
}, $body['messages']);
123-
124-
if ($system = $input->getSystemMessage()) {
125-
$body['system'] = $system->content;
126-
}
127-
12857
return $this->httpClient->request('POST', 'https://api.anthropic.com/v1/messages', [
12958
'headers' => [
13059
'x-api-key' => $this->apiKey,
13160
'anthropic-version' => $this->version,
13261
],
133-
'json' => array_merge($options, $body),
62+
'json' => array_merge($options, $payload),
13463
]);
13564
}
13665

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
}

0 commit comments

Comments
 (0)