From 4eb5f124d09bc063ffa332290ff356c98ddc61ad Mon Sep 17 00:00:00 2001 From: Denis Zunke Date: Sat, 5 Jul 2025 16:43:22 +0200 Subject: [PATCH] feat: open contract for more flexible normalizer configuration (#372) Could fix part of #371 - the question for flexibility. Why? Messages are given on platform calls and are given with the `MessageInterface`. So it is very flexible what could be given on call. Normalizers are only configured once, internally. So one side is highly flexible, the other side not. Giving the contract an interface allows creating own contracts with whatever backed technology one wants. So more flexible. Additionally it allows to add a contract that has an amount of normalizers one wants. For making it easier i thought about having model contract sets. I did it for anthropic, google and the platform itself. So a custom contract could look like ```php use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory; use PhpLlm\LlmChain\Platform\Contract; $platform = PlatformFactory::create( 'my-api-key', contract: Contract::create(new MyOwnMessageBagNormalizer(), new MyOwnMessageThingyNormalizer()), ); ``` or, totally flexible: ```php use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory; $platform = PlatformFactory::create( 'my-api-key', contract: new MyVeryOwnContractUtilizingJsonStreamer(), ); ``` Maybe this could be a way for more flexibility with the combination of non configurable normalizer and "unknown" message input? --- .../Anthropic/Contract/AnthropicContract.php | 36 +++++++++++++++++++ .../src/Bridge/Anthropic/PlatformFactory.php | 21 ++--------- .../src/Bridge/Azure/Meta/PlatformFactory.php | 4 ++- .../Bridge/Azure/OpenAI/PlatformFactory.php | 3 +- .../src/Bridge/Bedrock/PlatformFactory.php | 4 ++- .../Bridge/Google/Contract/GoogleContract.php | 33 +++++++++++++++++ .../src/Bridge/Google/PlatformFactory.php | 19 ++++------ .../Bridge/HuggingFace/PlatformFactory.php | 3 +- .../src/Bridge/Mistral/PlatformFactory.php | 3 +- .../src/Bridge/Ollama/PlatformFactory.php | 4 ++- .../src/Bridge/OpenAI/PlatformFactory.php | 3 +- .../src/Bridge/OpenRouter/PlatformFactory.php | 15 +++++--- .../src/Bridge/Replicate/PlatformFactory.php | 3 +- .../src/Bridge/Voyage/PlatformFactory.php | 4 ++- src/platform/src/Contract.php | 10 +++--- 15 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 src/platform/src/Bridge/Anthropic/Contract/AnthropicContract.php create mode 100644 src/platform/src/Bridge/Google/Contract/GoogleContract.php diff --git a/src/platform/src/Bridge/Anthropic/Contract/AnthropicContract.php b/src/platform/src/Bridge/Anthropic/Contract/AnthropicContract.php new file mode 100644 index 00000000..76ca37fd --- /dev/null +++ b/src/platform/src/Bridge/Anthropic/Contract/AnthropicContract.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Anthropic\Contract; + +use Symfony\AI\Platform\Contract; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Denis Zunke + */ +final readonly class AnthropicContract extends Contract +{ + public static function create(NormalizerInterface ...$normalizer): Contract + { + return parent::create( + new AssistantMessageNormalizer(), + new DocumentNormalizer(), + new DocumentUrlNormalizer(), + new ImageNormalizer(), + new ImageUrlNormalizer(), + new MessageBagNormalizer(), + new ToolCallMessageNormalizer(), + new ToolNormalizer(), + ...$normalizer, + ); + } +} diff --git a/src/platform/src/Bridge/Anthropic/PlatformFactory.php b/src/platform/src/Bridge/Anthropic/PlatformFactory.php index 5a674ca5..428949fe 100644 --- a/src/platform/src/Bridge/Anthropic/PlatformFactory.php +++ b/src/platform/src/Bridge/Anthropic/PlatformFactory.php @@ -11,14 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Anthropic; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\AssistantMessageNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\DocumentNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\DocumentUrlNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\ImageNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\ImageUrlNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\MessageBagNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\ToolCallMessageNormalizer; -use Symfony\AI\Platform\Bridge\Anthropic\Contract\ToolNormalizer; +use Symfony\AI\Platform\Bridge\Anthropic\Contract\AnthropicContract; use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Platform; use Symfony\Component\HttpClient\EventSourceHttpClient; @@ -34,22 +27,14 @@ public static function create( string $apiKey, string $version = '2023-06-01', ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); return new Platform( [new ModelClient($httpClient, $apiKey, $version)], [new ResponseConverter()], - Contract::create( - new AssistantMessageNormalizer(), - new DocumentNormalizer(), - new DocumentUrlNormalizer(), - new ImageNormalizer(), - new ImageUrlNormalizer(), - new MessageBagNormalizer(), - new ToolCallMessageNormalizer(), - new ToolNormalizer(), - ) + $contract ?? AnthropicContract::create(), ); } } diff --git a/src/platform/src/Bridge/Azure/Meta/PlatformFactory.php b/src/platform/src/Bridge/Azure/Meta/PlatformFactory.php index 59b8b5e6..0e8c30ef 100644 --- a/src/platform/src/Bridge/Azure/Meta/PlatformFactory.php +++ b/src/platform/src/Bridge/Azure/Meta/PlatformFactory.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Azure\Meta; +use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Platform; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -25,9 +26,10 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $modelClient = new LlamaHandler($httpClient ?? HttpClient::create(), $baseUrl, $apiKey); - return new Platform([$modelClient], [$modelClient]); + return new Platform([$modelClient], [$modelClient], $contract); } } diff --git a/src/platform/src/Bridge/Azure/OpenAI/PlatformFactory.php b/src/platform/src/Bridge/Azure/OpenAI/PlatformFactory.php index fd4af948..9949803e 100644 --- a/src/platform/src/Bridge/Azure/OpenAI/PlatformFactory.php +++ b/src/platform/src/Bridge/Azure/OpenAI/PlatformFactory.php @@ -31,6 +31,7 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); $embeddingsResponseFactory = new EmbeddingsModelClient($httpClient, $baseUrl, $deployment, $apiVersion, $apiKey); @@ -40,7 +41,7 @@ public static function create( return new Platform( [$GPTResponseFactory, $embeddingsResponseFactory, $whisperResponseFactory], [new ResponseConverter(), new Embeddings\ResponseConverter(), new \Symfony\AI\Platform\Bridge\OpenAI\Whisper\ResponseConverter()], - Contract::create(new AudioNormalizer()), + $contract ?? Contract::create(new AudioNormalizer()), ); } } diff --git a/src/platform/src/Bridge/Bedrock/PlatformFactory.php b/src/platform/src/Bridge/Bedrock/PlatformFactory.php index bd5d8784..c4d7d252 100644 --- a/src/platform/src/Bridge/Bedrock/PlatformFactory.php +++ b/src/platform/src/Bridge/Bedrock/PlatformFactory.php @@ -15,6 +15,7 @@ use Symfony\AI\Platform\Bridge\Bedrock\Anthropic\ClaudeHandler; use Symfony\AI\Platform\Bridge\Bedrock\Meta\LlamaModelClient; use Symfony\AI\Platform\Bridge\Bedrock\Nova\NovaHandler; +use Symfony\AI\Platform\Contract; /** * @author Björn Altmann @@ -23,11 +24,12 @@ { public static function create( BedrockRuntimeClient $bedrockRuntimeClient = new BedrockRuntimeClient(), + ?Contract $contract = null, ): Platform { $modelClient[] = new ClaudeHandler($bedrockRuntimeClient); $modelClient[] = new NovaHandler($bedrockRuntimeClient); $modelClient[] = new LlamaModelClient($bedrockRuntimeClient); - return new Platform($modelClient); + return new Platform($modelClient, $contract); } } diff --git a/src/platform/src/Bridge/Google/Contract/GoogleContract.php b/src/platform/src/Bridge/Google/Contract/GoogleContract.php new file mode 100644 index 00000000..298908a2 --- /dev/null +++ b/src/platform/src/Bridge/Google/Contract/GoogleContract.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\Google\Contract; + +use Symfony\AI\Platform\Contract; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Denis Zunke + */ +final readonly class GoogleContract extends Contract +{ + public static function create(NormalizerInterface ...$normalizer): Contract + { + return parent::create( + new AssistantMessageNormalizer(), + new MessageBagNormalizer(), + new ToolNormalizer(), + new ToolCallMessageNormalizer(), + new UserMessageNormalizer(), + ...$normalizer, + ); + } +} diff --git a/src/platform/src/Bridge/Google/PlatformFactory.php b/src/platform/src/Bridge/Google/PlatformFactory.php index 56729767..b0496afe 100644 --- a/src/platform/src/Bridge/Google/PlatformFactory.php +++ b/src/platform/src/Bridge/Google/PlatformFactory.php @@ -11,11 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Google; -use Symfony\AI\Platform\Bridge\Google\Contract\AssistantMessageNormalizer; -use Symfony\AI\Platform\Bridge\Google\Contract\MessageBagNormalizer; -use Symfony\AI\Platform\Bridge\Google\Contract\ToolCallMessageNormalizer; -use Symfony\AI\Platform\Bridge\Google\Contract\ToolNormalizer; -use Symfony\AI\Platform\Bridge\Google\Contract\UserMessageNormalizer; +use Symfony\AI\Platform\Bridge\Google\Contract\GoogleContract; use Symfony\AI\Platform\Bridge\Google\Embeddings\ModelClient; use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Platform; @@ -31,17 +27,16 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); $responseHandler = new ModelHandler($httpClient, $apiKey); $embeddings = new ModelClient($httpClient, $apiKey); - return new Platform([$responseHandler, $embeddings], [$responseHandler, $embeddings], Contract::create( - new AssistantMessageNormalizer(), - new MessageBagNormalizer(), - new ToolNormalizer(), - new ToolCallMessageNormalizer(), - new UserMessageNormalizer(), - )); + return new Platform( + [$responseHandler, $embeddings], + [$responseHandler, $embeddings], + $contract ?? GoogleContract::create(), + ); } } diff --git a/src/platform/src/Bridge/HuggingFace/PlatformFactory.php b/src/platform/src/Bridge/HuggingFace/PlatformFactory.php index f25c51c1..75850141 100644 --- a/src/platform/src/Bridge/HuggingFace/PlatformFactory.php +++ b/src/platform/src/Bridge/HuggingFace/PlatformFactory.php @@ -28,13 +28,14 @@ public static function create( string $apiKey, string $provider = Provider::HF_INFERENCE, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); return new Platform( [new ModelClient($httpClient, $provider, $apiKey)], [new ResponseConverter()], - Contract::create( + $contract ?? Contract::create( new FileNormalizer(), new MessageBagNormalizer(), ), diff --git a/src/platform/src/Bridge/Mistral/PlatformFactory.php b/src/platform/src/Bridge/Mistral/PlatformFactory.php index 0b5ccabb..3f8f7890 100644 --- a/src/platform/src/Bridge/Mistral/PlatformFactory.php +++ b/src/platform/src/Bridge/Mistral/PlatformFactory.php @@ -30,13 +30,14 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); return new Platform( [new EmbeddingsModelClient($httpClient, $apiKey), new MistralModelClient($httpClient, $apiKey)], [new EmbeddingsResponseConverter(), new MistralResponseConverter()], - Contract::create(new ToolNormalizer()), + $contract ?? Contract::create(new ToolNormalizer()), ); } } diff --git a/src/platform/src/Bridge/Ollama/PlatformFactory.php b/src/platform/src/Bridge/Ollama/PlatformFactory.php index fdde43e3..7ef4fd63 100644 --- a/src/platform/src/Bridge/Ollama/PlatformFactory.php +++ b/src/platform/src/Bridge/Ollama/PlatformFactory.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Ollama; +use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Platform; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -23,10 +24,11 @@ final class PlatformFactory public static function create( string $hostUrl = 'http://localhost:11434', ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); $handler = new LlamaModelHandler($httpClient, $hostUrl); - return new Platform([$handler], [$handler]); + return new Platform([$handler], [$handler], $contract); } } diff --git a/src/platform/src/Bridge/OpenAI/PlatformFactory.php b/src/platform/src/Bridge/OpenAI/PlatformFactory.php index 1cea3e09..2de87d58 100644 --- a/src/platform/src/Bridge/OpenAI/PlatformFactory.php +++ b/src/platform/src/Bridge/OpenAI/PlatformFactory.php @@ -33,6 +33,7 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); @@ -51,7 +52,7 @@ public static function create( $dallEModelClient, new WhisperResponseConverter(), ], - Contract::create(new AudioNormalizer()), + $contract ?? Contract::create(new AudioNormalizer()), ); } } diff --git a/src/platform/src/Bridge/OpenRouter/PlatformFactory.php b/src/platform/src/Bridge/OpenRouter/PlatformFactory.php index 15b53da2..4f42a03f 100644 --- a/src/platform/src/Bridge/OpenRouter/PlatformFactory.php +++ b/src/platform/src/Bridge/OpenRouter/PlatformFactory.php @@ -28,14 +28,19 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); $handler = new Client($httpClient, $apiKey); - return new Platform([$handler], [$handler], Contract::create( - new AssistantMessageNormalizer(), - new MessageBagNormalizer(), - new UserMessageNormalizer(), - )); + return new Platform( + [$handler], + [$handler], + $contract ?? Contract::create( + new AssistantMessageNormalizer(), + new MessageBagNormalizer(), + new UserMessageNormalizer(), + ), + ); } } diff --git a/src/platform/src/Bridge/Replicate/PlatformFactory.php b/src/platform/src/Bridge/Replicate/PlatformFactory.php index 51e9d7a8..49701e34 100644 --- a/src/platform/src/Bridge/Replicate/PlatformFactory.php +++ b/src/platform/src/Bridge/Replicate/PlatformFactory.php @@ -27,11 +27,12 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { return new Platform( [new LlamaModelClient(new Client($httpClient ?? HttpClient::create(), new Clock(), $apiKey))], [new LlamaResponseConverter()], - Contract::create(new LlamaMessageBagNormalizer()), + $contract ?? Contract::create(new LlamaMessageBagNormalizer()), ); } } diff --git a/src/platform/src/Bridge/Voyage/PlatformFactory.php b/src/platform/src/Bridge/Voyage/PlatformFactory.php index 8497a9c0..537fe797 100644 --- a/src/platform/src/Bridge/Voyage/PlatformFactory.php +++ b/src/platform/src/Bridge/Voyage/PlatformFactory.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Voyage; +use Symfony\AI\Platform\Contract; use Symfony\AI\Platform\Platform; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -24,10 +25,11 @@ public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, + ?Contract $contract = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); $handler = new ModelHandler($httpClient, $apiKey); - return new Platform([$handler], [$handler]); + return new Platform([$handler], [$handler], $contract); } } diff --git a/src/platform/src/Contract.php b/src/platform/src/Contract.php index e18f5efc..ed2b8003 100644 --- a/src/platform/src/Contract.php +++ b/src/platform/src/Contract.php @@ -30,12 +30,12 @@ /** * @author Christopher Hertel */ -final readonly class Contract +readonly class Contract { public const CONTEXT_MODEL = 'model'; - public function __construct( - private NormalizerInterface $normalizer, + final public function __construct( + protected NormalizerInterface $normalizer, ) { } @@ -70,7 +70,7 @@ public static function create(NormalizerInterface ...$normalizer): self * * @return array|string */ - public function createRequestPayload(Model $model, object|array|string $input): string|array + final public function createRequestPayload(Model $model, object|array|string $input): string|array { return $this->normalizer->normalize($input, context: [self::CONTEXT_MODEL => $model]); } @@ -80,7 +80,7 @@ public function createRequestPayload(Model $model, object|array|string $input): * * @return array */ - public function createToolOption(array $tools, Model $model): array + final public function createToolOption(array $tools, Model $model): array { return $this->normalizer->normalize($tools, context: [ self::CONTEXT_MODEL => $model,