Skip to content

Add custom Contract service injection support for Platform configurations #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ llm_chain:
platform:
openai:
api_key: '%env(OPENAI_API_KEY)%'
# contract: '@my_custom_contract_service' # Optional: Custom contract service
chain:
default:
model:
Expand All @@ -31,15 +32,18 @@ llm_chain:
platform:
anthropic:
api_key: '%env(ANTHROPIC_API_KEY)%'
contract: '@my_custom_anthropic_contract' # Optional: Custom contract service
azure:
# multiple deployments possible
gpt_deployment:
base_url: '%env(AZURE_OPENAI_BASEURL)%'
deployment: '%env(AZURE_OPENAI_GPT)%'
api_key: '%env(AZURE_OPENAI_KEY)%'
api_version: '%env(AZURE_GPT_VERSION)%'
contract: '@my_custom_azure_contract' # Optional: Custom contract service
google:
api_key: '%env(GOOGLE_API_KEY)%'
contract: '@my_custom_google_contract' # Optional: Custom contract service
chain:
rag:
platform: 'llm_chain.platform.azure.gpt_deployment'
Expand Down Expand Up @@ -86,6 +90,56 @@ llm_chain:
version: 'text-embedding-ada-002'
```

## Custom Contracts

You can inject custom contract services into your platforms to customize their behavior. This is useful for:
- Custom authentication methods
- Request/response processing
- Platform-specific normalizers and converters

### Creating a Custom Contract Service

1. Create a service that implements `PhpLlm\LlmChainBundle\Contract\ContractInterface`:

```php
<?php

namespace App\Service;

use PhpLlm\LlmChainBundle\Contract\ContractInterface;

class MyCustomOpenAIContract implements ContractInterface
{
// Implement your custom contract logic here
// This could include custom normalizers, request/response handlers, etc.
}
```

2. Register your service in Symfony (if not using autoconfigure):

```yaml
# config/services.yaml
services:
App\Service\MyCustomOpenAIContract:
tags: ['llm_chain.platform.contract']
```

3. Reference your contract service in the platform configuration:

```yaml
# config/packages/llm_chain.yaml
llm_chain:
platform:
openai:
api_key: '%env(OPENAI_API_KEY)%'
contract: '@App\Service\MyCustomOpenAIContract'
azure_openai:
api_key: '%env(AZURE_OPENAI_API_KEY)%'
contract: '@App\Service\MyCustomAzureContract'
```

The `contract` key is optional and defaults to `null` when not specified. When provided, it must reference a valid Symfony service that implements `ContractInterface`.

## Usage

### Chain Service
Expand Down
18 changes: 18 additions & 0 deletions src/Contract/ContractInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChainBundle\Contract;

/**
* Interface for custom contract implementations that can be injected into Platform instances.
*
* Implementing classes should provide the contract logic specific to their platform
* (e.g., custom request/response handling, authentication, etc.).
*/
interface ContractInterface
{
// Placeholder interface - the specific contract methods will depend on
// the actual Contract implementation in the llm-chain library.
// This interface ensures type safety for the contract parameter.
}
6 changes: 6 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('version')->defaultNull()->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->arrayNode('azure')
Expand All @@ -35,27 +36,32 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('base_url')->isRequired()->end()
->scalarNode('deployment')->isRequired()->end()
->scalarNode('api_version')->info('The used API version')->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->end()
->arrayNode('google')
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->arrayNode('openai')
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->arrayNode('mistral')
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->arrayNode('openrouter')
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('contract')->defaultNull()->end()
->end()
->end()
->end()
Expand Down
27 changes: 27 additions & 0 deletions src/DependencyInjection/LlmChainExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use PhpLlm\LlmChain\Store\Embedder;
use PhpLlm\LlmChain\Store\StoreInterface;
use PhpLlm\LlmChain\Store\VectorStoreInterface;
use PhpLlm\LlmChainBundle\Contract\ContractInterface;
use PhpLlm\LlmChainBundle\Profiler\DataCollector;
use PhpLlm\LlmChainBundle\Profiler\TraceablePlatform;
use PhpLlm\LlmChainBundle\Profiler\TraceableToolbox;
Expand Down Expand Up @@ -121,6 +122,8 @@ public function load(array $configs, ContainerBuilder $container): void
->addTag('llm_chain.platform.model_client');
$container->registerForAutoconfiguration(ResponseConverterInterface::class)
->addTag('llm_chain.platform.response_converter');
$container->registerForAutoconfiguration(ContractInterface::class)
->addTag('llm_chain.platform.contract');

if (false === $container->getParameter('kernel.debug')) {
$container->removeDefinition(DataCollector::class);
Expand Down Expand Up @@ -149,6 +152,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
$definition->replaceArgument('$version', $platform['version']);
}

if (isset($platform['contract']) && null !== $platform['contract']) {
$definition->replaceArgument('$contract', new Reference($platform['contract']));
}

$container->setDefinition($platformId, $definition);

return;
Expand All @@ -170,6 +177,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
])
->addTag('llm_chain.platform');

if (isset($config['contract']) && null !== $config['contract']) {
$definition->replaceArgument('$contract', new Reference($config['contract']));
}

$container->setDefinition($platformId, $definition);
}

Expand All @@ -186,6 +197,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');

if (isset($platform['contract']) && null !== $platform['contract']) {
$definition->replaceArgument('$contract', new Reference($platform['contract']));
}

$container->setDefinition($platformId, $definition);

return;
Expand All @@ -201,6 +216,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');

if (isset($platform['contract']) && null !== $platform['contract']) {
$definition->replaceArgument('$contract', new Reference($platform['contract']));
}

$container->setDefinition($platformId, $definition);

return;
Expand All @@ -216,6 +235,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');

if (isset($platform['contract']) && null !== $platform['contract']) {
$definition->replaceArgument('$contract', new Reference($platform['contract']));
}

$container->setDefinition($platformId, $definition);

return;
Expand All @@ -231,6 +254,10 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');

if (isset($platform['contract']) && null !== $platform['contract']) {
$definition->replaceArgument('$contract', new Reference($platform['contract']));
}

$container->setDefinition($platformId, $definition);

return;
Expand Down
37 changes: 37 additions & 0 deletions tests/Contract/ExampleCustomContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChainBundle\Tests\Contract;

use PhpLlm\LlmChainBundle\Contract\ContractInterface;

/**
* Example custom contract implementation for testing purposes.
*
* This demonstrates how to implement a custom contract service that can be
* injected into platform configurations via the bundle config.
*/
class ExampleCustomContract implements ContractInterface
{
public function __construct(
private readonly string $customSetting = 'default'
) {
}

public function getCustomSetting(): string
{
return $this->customSetting;
}

/**
* Example method that could be used for custom contract logic.
* In a real implementation, this might handle custom normalizers,
* authentication, or request/response processing.
*/
public function processCustomLogic(array $data): array
{
// Custom processing logic would go here
return array_merge($data, ['processed_by' => self::class]);
}
}
112 changes: 112 additions & 0 deletions tests/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace PhpLlm\LlmChainBundle\Tests\DependencyInjection;

use PhpLlm\LlmChainBundle\Contract\ContractInterface;
use PhpLlm\LlmChainBundle\DependencyInjection\Configuration;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Processor;

final class ConfigurationTest extends TestCase
{
#[Test]
public function platformContractConfigurationIsOptional(): void
{
$configuration = new Configuration();
$processor = new Processor();

// Test that contract is optional and defaults to null
$config = $processor->processConfiguration($configuration, [
'llm_chain' => [
'platform' => [
'openai' => [
'api_key' => 'test-key',
],
],
],
]);

self::assertArrayHasKey('platform', $config);
self::assertArrayHasKey('openai', $config['platform']);
self::assertArrayHasKey('contract', $config['platform']['openai']);
self::assertNull($config['platform']['openai']['contract']);
}

#[Test]
public function platformContractCanBeConfigured(): void
{
$configuration = new Configuration();
$processor = new Processor();

// Test that contract can be set
$config = $processor->processConfiguration($configuration, [
'llm_chain' => [
'platform' => [
'openai' => [
'api_key' => 'test-key',
'contract' => '@my_custom_contract_service',
],
],
],
]);

self::assertArrayHasKey('platform', $config);
self::assertArrayHasKey('openai', $config['platform']);
self::assertArrayHasKey('contract', $config['platform']['openai']);
self::assertSame('@my_custom_contract_service', $config['platform']['openai']['contract']);
}

#[Test]
public function allPlatformTypesAcceptContractConfiguration(): void
{
$configuration = new Configuration();
$processor = new Processor();

$config = $processor->processConfiguration($configuration, [
'llm_chain' => [
'platform' => [
'anthropic' => [
'api_key' => 'test-key',
'contract' => '@anthropic_contract',
],
'google' => [
'api_key' => 'test-key',
'contract' => '@google_contract',
],
'openai' => [
'api_key' => 'test-key',
'contract' => '@openai_contract',
],
'mistral' => [
'api_key' => 'test-key',
'contract' => '@mistral_contract',
],
'openrouter' => [
'api_key' => 'test-key',
'contract' => '@openrouter_contract',
],
'azure' => [
'gpt_deployment' => [
'api_key' => 'test-key',
'base_url' => 'https://test.openai.azure.com',
'deployment' => 'gpt-4',
'api_version' => '2023-03-15-preview',
'contract' => '@azure_contract',
],
],
],
],
]);

// Check all platforms have the contract key configured
self::assertSame('@anthropic_contract', $config['platform']['anthropic']['contract']);
self::assertSame('@google_contract', $config['platform']['google']['contract']);
self::assertSame('@openai_contract', $config['platform']['openai']['contract']);
self::assertSame('@mistral_contract', $config['platform']['mistral']['contract']);
self::assertSame('@openrouter_contract', $config['platform']['openrouter']['contract']);
self::assertSame('@azure_contract', $config['platform']['azure']['gpt_deployment']['contract']);
}
}
Loading
Loading