├── .github └── workflows │ └── pipeline.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpstan.dist.neon ├── phpunit.xml ├── profiler.png ├── rector.php ├── src ├── DependencyInjection │ ├── Configuration.php │ └── LlmChainExtension.php ├── LlmChainBundle.php ├── Profiler │ ├── DataCollector.php │ ├── TraceablePlatform.php │ └── TraceableToolbox.php └── Resources │ ├── config │ └── services.php │ └── views │ ├── data_collector.html.twig │ └── icon.svg └── tests ├── DependencyInjection └── ConfigurationTest.php └── Profiler └── TraceableToolboxTest.php /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: pipeline 2 | on: pull_request 3 | 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | php: ['8.2', '8.3', '8.4'] 10 | dependencies: ['lowest', 'highest'] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: ${{ matrix.php }} 19 | 20 | - name: Install Composer 21 | uses: "ramsey/composer-install@v3" 22 | with: 23 | dependency-versions: "${{ matrix.dependencies }}" 24 | 25 | - name: Composer Validation 26 | run: composer validate --strict 27 | 28 | - name: Install PHP Dependencies 29 | run: composer install --no-scripts 30 | 31 | - name: Tests 32 | run: vendor/bin/phpunit 33 | 34 | qa: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup PHP 41 | uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: '8.2' 44 | 45 | - name: Install Composer 46 | uses: "ramsey/composer-install@v3" 47 | 48 | - name: Composer Validation 49 | run: composer validate --strict 50 | 51 | - name: Install PHP Dependencies 52 | run: composer install --no-scripts 53 | 54 | - name: Code Style PHP 55 | run: vendor/bin/php-cs-fixer fix --dry-run 56 | 57 | - name: Rector 58 | run: vendor/bin/rector --dry-run 59 | 60 | - name: PHPStan 61 | run: vendor/bin/phpstan analyse 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .php-cs-fixer.cache 4 | .phpunit.cache 5 | coverage 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ; 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | '@Symfony' => true, 10 | ]) 11 | ->setFinder($finder) 12 | ; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Christopher Hertel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest 2 | 3 | deps-stable: 4 | composer update --prefer-stable --ignore-platform-req=ext-mongodb 5 | 6 | deps-low: 7 | composer update --prefer-lowest --ignore-platform-req=ext-mongodb 8 | 9 | deps-dev: 10 | composer require php-llm/llm-chain:dev-main 11 | 12 | cs: 13 | PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix --diff --verbose 14 | 15 | rector: 16 | vendor/bin/rector 17 | 18 | phpstan: 19 | vendor/bin/phpstan --memory-limit=-1 20 | 21 | tests: 22 | vendor/bin/phpunit 23 | 24 | coverage: 25 | XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage 26 | 27 | run-examples: 28 | ./example 29 | 30 | ci: ci-stable 31 | 32 | ci-stable: deps-stable rector cs phpstan tests 33 | 34 | ci-lowest: deps-low rector cs phpstan tests 35 | 36 | ci-dev: deps-dev rector cs phpstan tests 37 | git restore composer.json 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM Chain Bundle 2 | 3 | > [!IMPORTANT] 4 | > **PHP LLM becomes Symfony AI** - this project moved to [github.com/symfony/ai](https://github.com/symfony/ai). 5 | > Please use the new repository for all future development, issues, and contributions. 6 | > Thanks for your contributions - we hope to see you at Symfony AI! 7 | 8 | Symfony integration bundle for [php-llm/llm-chain](https://github.com/php-llm/llm-chain) library. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | composer require php-llm/llm-chain-bundle 14 | ``` 15 | 16 | ## Configuration 17 | 18 | ### Basic Example with OpenAI 19 | 20 | ```yaml 21 | # config/packages/llm_chain.yaml 22 | llm_chain: 23 | platform: 24 | openai: 25 | api_key: '%env(OPENAI_API_KEY)%' 26 | chain: 27 | default: 28 | model: 29 | name: 'GPT' 30 | ``` 31 | 32 | ### Advanced Example with Anthropic, Azure, Google and multiple chains 33 | ```yaml 34 | # config/packages/llm_chain.yaml 35 | llm_chain: 36 | platform: 37 | anthropic: 38 | api_key: '%env(ANTHROPIC_API_KEY)%' 39 | azure: 40 | # multiple deployments possible 41 | gpt_deployment: 42 | base_url: '%env(AZURE_OPENAI_BASEURL)%' 43 | deployment: '%env(AZURE_OPENAI_GPT)%' 44 | api_key: '%env(AZURE_OPENAI_KEY)%' 45 | api_version: '%env(AZURE_GPT_VERSION)%' 46 | google: 47 | api_key: '%env(GOOGLE_API_KEY)%' 48 | chain: 49 | rag: 50 | platform: 'llm_chain.platform.azure.gpt_deployment' 51 | structured_output: false # Disables support for "output_structure" option, default is true 52 | model: 53 | name: 'GPT' 54 | version: 'gpt-4o-mini' 55 | system_prompt: 'You are a helpful assistant that can answer questions.' # The default system prompt of the chain 56 | include_tools: true # Include tool definitions at the end of the system prompt 57 | tools: 58 | # Referencing a service with #[AsTool] attribute 59 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch' 60 | 61 | # Referencing a service without #[AsTool] attribute 62 | - service: 'App\Chain\Tool\CompanyName' 63 | name: 'company_name' 64 | description: 'Provides the name of your company' 65 | method: 'foo' # Optional with default value '__invoke' 66 | 67 | # Referencing a chain => chain in chain 🤯 68 | - service: 'llm_chain.chain.research' 69 | name: 'wikipedia_research' 70 | description: 'Can research on Wikipedia' 71 | is_chain: true 72 | research: 73 | platform: 'llm_chain.platform.anthropic' 74 | model: 75 | name: 'Claude' 76 | tools: # If undefined, all tools are injected into the chain, use "tools: false" to disable tools. 77 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia' 78 | fault_tolerant_toolbox: false # Disables fault tolerant toolbox, default is true 79 | store: 80 | # also azure_search, mongodb and pinecone are supported as store type 81 | chroma_db: 82 | # multiple collections possible per type 83 | default: 84 | collection: 'my_collection' 85 | embedder: 86 | default: 87 | # platform: 'llm_chain.platform.anthropic' 88 | # store: 'llm_chain.store.chroma_db.default' 89 | model: 90 | name: 'Embeddings' 91 | version: 'text-embedding-ada-002' 92 | ``` 93 | 94 | ## Usage 95 | 96 | ### Chain Service 97 | 98 | Use the `Chain` service to leverage GPT: 99 | ```php 100 | use PhpLlm\LlmChain\ChainInterface; 101 | use PhpLlm\LlmChain\Model\Message\Message; 102 | use PhpLlm\LlmChain\Model\Message\MessageBag; 103 | 104 | final readonly class MyService 105 | { 106 | public function __construct( 107 | private ChainInterface $chain, 108 | ) { 109 | } 110 | 111 | public function submit(string $message): string 112 | { 113 | $messages = new MessageBag( 114 | Message::forSystem('Speak like a pirate.'), 115 | Message::ofUser($message), 116 | ); 117 | 118 | return $this->chain->call($messages); 119 | } 120 | } 121 | ``` 122 | 123 | ### Register Tools 124 | 125 | To use existing tools, you can register them as a service: 126 | ```yaml 127 | services: 128 | _defaults: 129 | autowire: true 130 | autoconfigure: true 131 | 132 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Clock: ~ 133 | PhpLlm\LlmChain\Chain\Toolbox\Tool\OpenMeteo: ~ 134 | PhpLlm\LlmChain\Chain\Toolbox\Tool\SerpApi: 135 | $apiKey: '%env(SERP_API_KEY)%' 136 | PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch: ~ 137 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Tavily: 138 | $apiKey: '%env(TAVILY_API_KEY)%' 139 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia: ~ 140 | PhpLlm\LlmChain\Chain\Toolbox\Tool\YouTubeTranscriber: ~ 141 | ``` 142 | 143 | Custom tools can be registered by using the `#[AsTool]` attribute: 144 | 145 | ```php 146 | use PhpLlm\LlmChain\Chain\Toolbox\Attribute\AsTool; 147 | 148 | #[AsTool('company_name', 'Provides the name of your company')] 149 | final class CompanyName 150 | { 151 | public function __invoke(): string 152 | { 153 | return 'ACME Corp.' 154 | } 155 | } 156 | ``` 157 | 158 | The chain configuration by default will inject all known tools into the chain. 159 | 160 | To disable this behavior, set the `tools` option to `false`: 161 | ```yaml 162 | llm_chain: 163 | chain: 164 | my_chain: 165 | tools: false 166 | ``` 167 | 168 | To inject only specific tools, list them in the configuration: 169 | ```yaml 170 | llm_chain: 171 | chain: 172 | my_chain: 173 | tools: 174 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch' 175 | ``` 176 | 177 | ### Profiler 178 | 179 | The profiler panel provides insights into the chain's execution: 180 | 181 | ![Profiler](./profiler.png) 182 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-llm/llm-chain-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony integration bundle for php-llm/llm-chain", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Christopher Hertel", 9 | "email": "mail@christopher-hertel.de" 10 | }, 11 | { 12 | "name": "Oskar Stark", 13 | "email": "oskarstark@googlemail.com" 14 | } 15 | ], 16 | "abandoned": "symfony/ai-bundle", 17 | "require": { 18 | "php": ">=8.2", 19 | "php-llm/llm-chain": "^0.24", 20 | "symfony/config": "^6.4 || ^7.0", 21 | "symfony/dependency-injection": "^6.4 || ^7.0", 22 | "symfony/framework-bundle": "^6.4 || ^7.0", 23 | "symfony/string": "^6.4 || ^7.0" 24 | }, 25 | "require-dev": { 26 | "php-cs-fixer/shim": "^3.78", 27 | "phpstan/phpstan": "^2.1", 28 | "phpunit/phpunit": "^11.5", 29 | "rector/rector": "^2.0" 30 | }, 31 | "config": { 32 | "sort-packages": true 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "PhpLlm\\LlmChainBundle\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "PhpLlm\\LlmChainBundle\\Tests\\": "tests/" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src/ 5 | excludePaths: 6 | analyse: 7 | - src/DependencyInjection/Configuration.php 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-llm/llm-chain-bundle/cecae1e1b39c41a42099632495eeaea90c2e85a2/profiler.png -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 13 | __DIR__.'/src', 14 | __DIR__.'/tests', 15 | ]) 16 | ->withPhpSets(php82: true) 17 | ->withSets([ 18 | PHPUnitSetList::PHPUNIT_110, 19 | PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES, 20 | PHPUnitSetList::PHPUNIT_CODE_QUALITY, 21 | ]) 22 | ->withRules([ 23 | PreferPHPUnitSelfCallRector::class, 24 | ]) 25 | ->withImportNames(importShortClasses: false) 26 | ->withSkip([ 27 | ClosureToArrowFunctionRector::class, 28 | PreferPHPUnitThisCallRector::class, 29 | ]) 30 | ->withTypeCoverageLevel(0); 31 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 18 | 19 | $rootNode 20 | ->children() 21 | ->arrayNode('platform') 22 | ->children() 23 | ->arrayNode('anthropic') 24 | ->children() 25 | ->scalarNode('api_key')->isRequired()->end() 26 | ->scalarNode('version')->defaultNull()->end() 27 | ->end() 28 | ->end() 29 | ->arrayNode('azure') 30 | ->normalizeKeys(false) 31 | ->useAttributeAsKey('name') 32 | ->arrayPrototype() 33 | ->children() 34 | ->scalarNode('api_key')->isRequired()->end() 35 | ->scalarNode('base_url')->isRequired()->end() 36 | ->scalarNode('deployment')->isRequired()->end() 37 | ->scalarNode('api_version')->info('The used API version')->end() 38 | ->end() 39 | ->end() 40 | ->end() 41 | ->arrayNode('google') 42 | ->children() 43 | ->scalarNode('api_key')->isRequired()->end() 44 | ->end() 45 | ->end() 46 | ->arrayNode('openai') 47 | ->children() 48 | ->scalarNode('api_key')->isRequired()->end() 49 | ->end() 50 | ->end() 51 | ->arrayNode('mistral') 52 | ->children() 53 | ->scalarNode('api_key')->isRequired()->end() 54 | ->end() 55 | ->end() 56 | ->arrayNode('openrouter') 57 | ->children() 58 | ->scalarNode('api_key')->isRequired()->end() 59 | ->end() 60 | ->end() 61 | ->end() 62 | ->end() 63 | ->arrayNode('chain') 64 | ->normalizeKeys(false) 65 | ->useAttributeAsKey('name') 66 | ->arrayPrototype() 67 | ->children() 68 | ->scalarNode('platform') 69 | ->info('Service name of platform') 70 | ->defaultValue(PlatformInterface::class) 71 | ->end() 72 | ->arrayNode('model') 73 | ->children() 74 | ->scalarNode('name')->isRequired()->end() 75 | ->scalarNode('version')->defaultNull()->end() 76 | ->arrayNode('options') 77 | ->variablePrototype()->end() 78 | ->end() 79 | ->end() 80 | ->end() 81 | ->booleanNode('structured_output')->defaultTrue()->end() 82 | ->scalarNode('system_prompt') 83 | ->validate() 84 | ->ifTrue(fn ($v) => null !== $v && '' === trim($v)) 85 | ->thenInvalid('The default system prompt must not be an empty string') 86 | ->end() 87 | ->defaultNull() 88 | ->info('The default system prompt of the chain') 89 | ->end() 90 | ->booleanNode('include_tools') 91 | ->info('Include tool definitions at the end of the system prompt') 92 | ->defaultFalse() 93 | ->end() 94 | ->arrayNode('tools') 95 | ->addDefaultsIfNotSet() 96 | ->treatFalseLike(['enabled' => false]) 97 | ->treatTrueLike(['enabled' => true]) 98 | ->treatNullLike(['enabled' => true]) 99 | ->beforeNormalization() 100 | ->ifArray() 101 | ->then(function (array $v) { 102 | return [ 103 | 'enabled' => $v['enabled'] ?? true, 104 | 'services' => $v['services'] ?? $v, 105 | ]; 106 | }) 107 | ->end() 108 | ->children() 109 | ->booleanNode('enabled')->defaultTrue()->end() 110 | ->arrayNode('services') 111 | ->arrayPrototype() 112 | ->children() 113 | ->scalarNode('service')->isRequired()->end() 114 | ->scalarNode('name')->end() 115 | ->scalarNode('description')->end() 116 | ->scalarNode('method')->end() 117 | ->booleanNode('is_chain')->defaultFalse()->end() 118 | ->end() 119 | ->beforeNormalization() 120 | ->ifString() 121 | ->then(function (string $v) { 122 | return ['service' => $v]; 123 | }) 124 | ->end() 125 | ->end() 126 | ->end() 127 | ->end() 128 | ->end() 129 | ->booleanNode('fault_tolerant_toolbox')->defaultTrue()->end() 130 | ->end() 131 | ->end() 132 | ->end() 133 | ->arrayNode('store') 134 | ->children() 135 | ->arrayNode('azure_search') 136 | ->normalizeKeys(false) 137 | ->useAttributeAsKey('name') 138 | ->arrayPrototype() 139 | ->children() 140 | ->scalarNode('endpoint')->isRequired()->end() 141 | ->scalarNode('api_key')->isRequired()->end() 142 | ->scalarNode('index_name')->isRequired()->end() 143 | ->scalarNode('api_version')->isRequired()->end() 144 | ->scalarNode('vector_field')->end() 145 | ->end() 146 | ->end() 147 | ->end() 148 | ->arrayNode('chroma_db') 149 | ->normalizeKeys(false) 150 | ->useAttributeAsKey('name') 151 | ->arrayPrototype() 152 | ->children() 153 | ->scalarNode('collection')->isRequired()->end() 154 | ->end() 155 | ->end() 156 | ->end() 157 | ->arrayNode('mongodb') 158 | ->normalizeKeys(false) 159 | ->useAttributeAsKey('name') 160 | ->arrayPrototype() 161 | ->children() 162 | ->scalarNode('database')->isRequired()->end() 163 | ->scalarNode('collection')->isRequired()->end() 164 | ->scalarNode('index_name')->isRequired()->end() 165 | ->scalarNode('vector_field')->end() 166 | ->booleanNode('bulk_write')->end() 167 | ->end() 168 | ->end() 169 | ->end() 170 | ->arrayNode('pinecone') 171 | ->normalizeKeys(false) 172 | ->useAttributeAsKey('name') 173 | ->arrayPrototype() 174 | ->children() 175 | ->scalarNode('namespace')->end() 176 | ->arrayNode('filter') 177 | ->scalarPrototype()->end() 178 | ->end() 179 | ->integerNode('top_k')->end() 180 | ->end() 181 | ->end() 182 | ->end() 183 | ->end() 184 | ->end() 185 | ->arrayNode('indexer') 186 | ->normalizeKeys(false) 187 | ->useAttributeAsKey('name') 188 | ->arrayPrototype() 189 | ->children() 190 | ->scalarNode('store') 191 | ->info('Service name of store') 192 | ->defaultValue(StoreInterface::class) 193 | ->end() 194 | ->scalarNode('platform') 195 | ->info('Service name of platform') 196 | ->defaultValue(PlatformInterface::class) 197 | ->end() 198 | ->arrayNode('model') 199 | ->children() 200 | ->scalarNode('name')->isRequired()->end() 201 | ->scalarNode('version')->defaultNull()->end() 202 | ->arrayNode('options') 203 | ->variablePrototype()->end() 204 | ->end() 205 | ->end() 206 | ->end() 207 | ->end() 208 | ->end() 209 | ->end() 210 | ->end() 211 | ; 212 | 213 | return $treeBuilder; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/DependencyInjection/LlmChainExtension.php: -------------------------------------------------------------------------------- 1 | load('services.php'); 65 | 66 | $configuration = new Configuration(); 67 | $config = $this->processConfiguration($configuration, $configs); 68 | foreach ($config['platform'] ?? [] as $type => $platform) { 69 | $this->processPlatformConfig($type, $platform, $container); 70 | } 71 | $platforms = array_keys($container->findTaggedServiceIds('llm_chain.platform')); 72 | if (1 === count($platforms)) { 73 | $container->setAlias(PlatformInterface::class, reset($platforms)); 74 | } 75 | if ($container->getParameter('kernel.debug')) { 76 | foreach ($platforms as $platform) { 77 | $traceablePlatformDefinition = (new Definition(TraceablePlatform::class)) 78 | ->setDecoratedService($platform) 79 | ->setAutowired(true) 80 | ->addTag('llm_chain.traceable_platform'); 81 | $suffix = u($platform)->afterLast('.')->toString(); 82 | $container->setDefinition('llm_chain.traceable_platform.'.$suffix, $traceablePlatformDefinition); 83 | } 84 | } 85 | 86 | foreach ($config['chain'] as $chainName => $chain) { 87 | $this->processChainConfig($chainName, $chain, $container); 88 | } 89 | if (1 === count($config['chain']) && isset($chainName)) { 90 | $container->setAlias(ChainInterface::class, 'llm_chain.chain.'.$chainName); 91 | } 92 | 93 | foreach ($config['store'] ?? [] as $type => $store) { 94 | $this->processStoreConfig($type, $store, $container); 95 | } 96 | $stores = array_keys($container->findTaggedServiceIds('llm_chain.store')); 97 | if (1 === count($stores)) { 98 | $container->setAlias(VectorStoreInterface::class, reset($stores)); 99 | $container->setAlias(StoreInterface::class, reset($stores)); 100 | } 101 | 102 | foreach ($config['indexer'] as $indexerName => $indexer) { 103 | $this->processIndexerConfig($indexerName, $indexer, $container); 104 | } 105 | if (1 === count($config['indexer']) && isset($indexerName)) { 106 | $container->setAlias(Indexer::class, 'llm_chain.indexer.'.$indexerName); 107 | } 108 | 109 | $container->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void { 110 | $definition->addTag('llm_chain.tool', [ 111 | 'name' => $attribute->name, 112 | 'description' => $attribute->description, 113 | 'method' => $attribute->method, 114 | ]); 115 | }); 116 | 117 | $container->registerForAutoconfiguration(InputProcessorInterface::class) 118 | ->addTag('llm_chain.chain.input_processor'); 119 | $container->registerForAutoconfiguration(OutputProcessorInterface::class) 120 | ->addTag('llm_chain.chain.output_processor'); 121 | $container->registerForAutoconfiguration(ModelClientInterface::class) 122 | ->addTag('llm_chain.platform.model_client'); 123 | $container->registerForAutoconfiguration(ResponseConverterInterface::class) 124 | ->addTag('llm_chain.platform.response_converter'); 125 | 126 | if (false === $container->getParameter('kernel.debug')) { 127 | $container->removeDefinition(DataCollector::class); 128 | $container->removeDefinition(TraceableToolbox::class); 129 | } 130 | } 131 | 132 | /** 133 | * @param array $platform 134 | */ 135 | private function processPlatformConfig(string $type, array $platform, ContainerBuilder $container): void 136 | { 137 | if ('anthropic' === $type) { 138 | $platformId = 'llm_chain.platform.anthropic'; 139 | $definition = (new Definition(Platform::class)) 140 | ->setFactory(AnthropicPlatformFactory::class.'::create') 141 | ->setAutowired(true) 142 | ->setLazy(true) 143 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 144 | ->setArguments([ 145 | '$apiKey' => $platform['api_key'], 146 | ]) 147 | ->addTag('llm_chain.platform'); 148 | 149 | if (isset($platform['version'])) { 150 | $definition->replaceArgument('$version', $platform['version']); 151 | } 152 | 153 | $container->setDefinition($platformId, $definition); 154 | 155 | return; 156 | } 157 | 158 | if ('azure' === $type) { 159 | foreach ($platform as $name => $config) { 160 | $platformId = 'llm_chain.platform.azure.'.$name; 161 | $definition = (new Definition(Platform::class)) 162 | ->setFactory(AzureOpenAIPlatformFactory::class.'::create') 163 | ->setAutowired(true) 164 | ->setLazy(true) 165 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 166 | ->setArguments([ 167 | '$baseUrl' => $config['base_url'], 168 | '$deployment' => $config['deployment'], 169 | '$apiVersion' => $config['api_version'], 170 | '$apiKey' => $config['api_key'], 171 | ]) 172 | ->addTag('llm_chain.platform'); 173 | 174 | $container->setDefinition($platformId, $definition); 175 | } 176 | 177 | return; 178 | } 179 | 180 | if ('google' === $type) { 181 | $platformId = 'llm_chain.platform.google'; 182 | $definition = (new Definition(Platform::class)) 183 | ->setFactory(GooglePlatformFactory::class.'::create') 184 | ->setAutowired(true) 185 | ->setLazy(true) 186 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 187 | ->setArguments(['$apiKey' => $platform['api_key']]) 188 | ->addTag('llm_chain.platform'); 189 | 190 | $container->setDefinition($platformId, $definition); 191 | 192 | return; 193 | } 194 | 195 | if ('openai' === $type) { 196 | $platformId = 'llm_chain.platform.openai'; 197 | $definition = (new Definition(Platform::class)) 198 | ->setFactory(OpenAIPlatformFactory::class.'::create') 199 | ->setAutowired(true) 200 | ->setLazy(true) 201 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 202 | ->setArguments(['$apiKey' => $platform['api_key']]) 203 | ->addTag('llm_chain.platform'); 204 | 205 | $container->setDefinition($platformId, $definition); 206 | 207 | return; 208 | } 209 | 210 | if ('openrouter' === $type) { 211 | $platformId = 'llm_chain.platform.openrouter'; 212 | $definition = (new Definition(Platform::class)) 213 | ->setFactory(OpenRouterPlatformFactory::class.'::create') 214 | ->setAutowired(true) 215 | ->setLazy(true) 216 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 217 | ->setArguments(['$apiKey' => $platform['api_key']]) 218 | ->addTag('llm_chain.platform'); 219 | 220 | $container->setDefinition($platformId, $definition); 221 | 222 | return; 223 | } 224 | 225 | if ('mistral' === $type) { 226 | $platformId = 'llm_chain.platform.mistral'; 227 | $definition = (new Definition(Platform::class)) 228 | ->setFactory(MistralPlatformFactory::class.'::create') 229 | ->setAutowired(true) 230 | ->setLazy(true) 231 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 232 | ->setArguments(['$apiKey' => $platform['api_key']]) 233 | ->addTag('llm_chain.platform'); 234 | 235 | $container->setDefinition($platformId, $definition); 236 | 237 | return; 238 | } 239 | 240 | throw new \InvalidArgumentException(sprintf('Platform "%s" is not supported for configuration via bundle at this point.', $type)); 241 | } 242 | 243 | /** 244 | * @param array $config 245 | */ 246 | private function processChainConfig(string $name, array $config, ContainerBuilder $container): void 247 | { 248 | // MODEL 249 | ['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model']; 250 | 251 | $modelClass = match (strtolower((string) $modelName)) { 252 | 'gpt' => GPT::class, 253 | 'claude' => Claude::class, 254 | 'llama' => Llama::class, 255 | 'gemini' => Gemini::class, 256 | 'mistral' => Mistral::class, 257 | 'openrouter' => Model::class, 258 | default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)), 259 | }; 260 | $modelDefinition = new Definition($modelClass); 261 | if (null !== $version) { 262 | $modelDefinition->setArgument('$name', $version); 263 | } 264 | if (0 !== count($options)) { 265 | $modelDefinition->setArgument('$options', $options); 266 | } 267 | $modelDefinition->addTag('llm_chain.model.language_model'); 268 | $container->setDefinition('llm_chain.chain.'.$name.'.model', $modelDefinition); 269 | 270 | // CHAIN 271 | $chainDefinition = (new Definition(Chain::class)) 272 | ->setAutowired(true) 273 | ->setArgument('$platform', new Reference($config['platform'])) 274 | ->setArgument('$model', new Reference('llm_chain.chain.'.$name.'.model')); 275 | 276 | $inputProcessors = []; 277 | $outputProcessors = []; 278 | 279 | // TOOL & PROCESSOR 280 | if ($config['tools']['enabled']) { 281 | // Create specific toolbox and process if tools are explicitly defined 282 | if (0 !== count($config['tools']['services'])) { 283 | $memoryFactoryDefinition = new Definition(MemoryToolFactory::class); 284 | $container->setDefinition('llm_chain.toolbox.'.$name.'.memory_factory', $memoryFactoryDefinition); 285 | $chainFactoryDefinition = new Definition(ChainFactory::class, [ 286 | '$factories' => [new Reference('llm_chain.toolbox.'.$name.'.memory_factory'), new Reference(ReflectionToolFactory::class)], 287 | ]); 288 | $container->setDefinition('llm_chain.toolbox.'.$name.'.chain_factory', $chainFactoryDefinition); 289 | 290 | $tools = []; 291 | foreach ($config['tools']['services'] as $tool) { 292 | $reference = new Reference($tool['service']); 293 | // We use the memory factory in case method, description and name are set 294 | if (isset($tool['name'], $tool['description'])) { 295 | if ($tool['is_chain']) { 296 | $chainWrapperDefinition = new Definition(ChainTool::class, ['$chain' => $reference]); 297 | $container->setDefinition('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name'], $chainWrapperDefinition); 298 | $reference = new Reference('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name']); 299 | } 300 | $memoryFactoryDefinition->addMethodCall('addTool', [$reference, $tool['name'], $tool['description'], $tool['method'] ?? '__invoke']); 301 | } 302 | $tools[] = $reference; 303 | } 304 | 305 | $toolboxDefinition = (new ChildDefinition('llm_chain.toolbox.abstract')) 306 | ->replaceArgument('$toolFactory', new Reference('llm_chain.toolbox.'.$name.'.chain_factory')) 307 | ->replaceArgument('$tools', $tools); 308 | $container->setDefinition('llm_chain.toolbox.'.$name, $toolboxDefinition); 309 | 310 | if ($config['fault_tolerant_toolbox']) { 311 | $faultTolerantToolboxDefinition = (new Definition('llm_chain.fault_tolerant_toolbox.'.$name)) 312 | ->setClass(FaultTolerantToolbox::class) 313 | ->setAutowired(true) 314 | ->setDecoratedService('llm_chain.toolbox.'.$name); 315 | $container->setDefinition('llm_chain.fault_tolerant_toolbox.'.$name, $faultTolerantToolboxDefinition); 316 | } 317 | 318 | if ($container->getParameter('kernel.debug')) { 319 | $traceableToolboxDefinition = (new Definition('llm_chain.traceable_toolbox.'.$name)) 320 | ->setClass(TraceableToolbox::class) 321 | ->setAutowired(true) 322 | ->setDecoratedService('llm_chain.toolbox.'.$name) 323 | ->addTag('llm_chain.traceable_toolbox'); 324 | $container->setDefinition('llm_chain.traceable_toolbox.'.$name, $traceableToolboxDefinition); 325 | } 326 | 327 | $toolProcessorDefinition = (new ChildDefinition('llm_chain.tool.chain_processor.abstract')) 328 | ->replaceArgument('$toolbox', new Reference('llm_chain.toolbox.'.$name)); 329 | $container->setDefinition('llm_chain.tool.chain_processor.'.$name, $toolProcessorDefinition); 330 | 331 | $inputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name); 332 | $outputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name); 333 | } else { 334 | $inputProcessors[] = new Reference(ToolProcessor::class); 335 | $outputProcessors[] = new Reference(ToolProcessor::class); 336 | } 337 | } 338 | 339 | // STRUCTURED OUTPUT 340 | if ($config['structured_output']) { 341 | $inputProcessors[] = new Reference(StructureOutputProcessor::class); 342 | $outputProcessors[] = new Reference(StructureOutputProcessor::class); 343 | } 344 | 345 | // SYSTEM PROMPT 346 | if (is_string($config['system_prompt'])) { 347 | $systemPromptInputProcessorDefinition = new Definition(SystemPromptInputProcessor::class); 348 | $systemPromptInputProcessorDefinition 349 | ->setAutowired(true) 350 | ->setArguments([ 351 | '$systemPrompt' => $config['system_prompt'], 352 | '$toolbox' => $config['include_tools'] ? new Reference('llm_chain.toolbox.'.$name) : null, 353 | ]); 354 | 355 | $inputProcessors[] = $systemPromptInputProcessorDefinition; 356 | } 357 | 358 | $chainDefinition 359 | ->setArgument('$inputProcessors', $inputProcessors) 360 | ->setArgument('$outputProcessors', $outputProcessors); 361 | 362 | $container->setDefinition('llm_chain.chain.'.$name, $chainDefinition); 363 | } 364 | 365 | /** 366 | * @param array $stores 367 | */ 368 | private function processStoreConfig(string $type, array $stores, ContainerBuilder $container): void 369 | { 370 | if ('azure_search' === $type) { 371 | foreach ($stores as $name => $store) { 372 | $arguments = [ 373 | '$endpointUrl' => $store['endpoint'], 374 | '$apiKey' => $store['api_key'], 375 | '$indexName' => $store['index_name'], 376 | '$apiVersion' => $store['api_version'], 377 | ]; 378 | 379 | if (array_key_exists('vector_field', $store)) { 380 | $arguments['$vectorFieldName'] = $store['vector_field']; 381 | } 382 | 383 | $definition = new Definition(AzureSearchStore::class); 384 | $definition 385 | ->setAutowired(true) 386 | ->addTag('llm_chain.store') 387 | ->setArguments($arguments); 388 | 389 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 390 | } 391 | } 392 | 393 | if ('chroma_db' === $type) { 394 | foreach ($stores as $name => $store) { 395 | $definition = new Definition(ChromaDBStore::class); 396 | $definition 397 | ->setAutowired(true) 398 | ->setArgument('$collectionName', $store['collection']) 399 | ->addTag('llm_chain.store'); 400 | 401 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 402 | } 403 | } 404 | 405 | if ('mongodb' === $type) { 406 | foreach ($stores as $name => $store) { 407 | $arguments = [ 408 | '$databaseName' => $store['database'], 409 | '$collectionName' => $store['collection'], 410 | '$indexName' => $store['index_name'], 411 | ]; 412 | 413 | if (array_key_exists('vector_field', $store)) { 414 | $arguments['$vectorFieldName'] = $store['vector_field']; 415 | } 416 | 417 | if (array_key_exists('bulk_write', $store)) { 418 | $arguments['$bulkWrite'] = $store['bulk_write']; 419 | } 420 | 421 | $definition = new Definition(MongoDBStore::class); 422 | $definition 423 | ->setAutowired(true) 424 | ->addTag('llm_chain.store') 425 | ->setArguments($arguments); 426 | 427 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 428 | } 429 | } 430 | 431 | if ('pinecone' === $type) { 432 | foreach ($stores as $name => $store) { 433 | $arguments = [ 434 | '$namespace' => $store['namespace'], 435 | ]; 436 | 437 | if (array_key_exists('filter', $store)) { 438 | $arguments['$filter'] = $store['filter']; 439 | } 440 | 441 | if (array_key_exists('top_k', $store)) { 442 | $arguments['$topK'] = $store['top_k']; 443 | } 444 | 445 | $definition = new Definition(PineconeStore::class); 446 | $definition 447 | ->setAutowired(true) 448 | ->addTag('llm_chain.store') 449 | ->setArguments($arguments); 450 | 451 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 452 | } 453 | } 454 | } 455 | 456 | /** 457 | * @param array $config 458 | */ 459 | private function processIndexerConfig(int|string $name, array $config, ContainerBuilder $container): void 460 | { 461 | ['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model']; 462 | 463 | $modelClass = match (strtolower((string) $modelName)) { 464 | 'embeddings' => Embeddings::class, 465 | 'voyage' => Voyage::class, 466 | default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)), 467 | }; 468 | $modelDefinition = (new Definition($modelClass)); 469 | if (null !== $version) { 470 | $modelDefinition->setArgument('$name', $version); 471 | } 472 | if (0 !== count($options)) { 473 | $modelDefinition->setArgument('$options', $options); 474 | } 475 | $modelDefinition->addTag('llm_chain.model.embeddings_model'); 476 | $container->setDefinition('llm_chain.indexer.'.$name.'.model', $modelDefinition); 477 | 478 | $vectorizerDefinition = new Definition(Vectorizer::class, [ 479 | '$platform' => new Reference($config['platform']), 480 | '$model' => new Reference('llm_chain.indexer.'.$name.'.model'), 481 | ]); 482 | $container->setDefinition('llm_chain.indexer.'.$name.'.vectorizer', $vectorizerDefinition); 483 | 484 | $definition = new Definition(Indexer::class, [ 485 | '$vectorizer' => new Reference('llm_chain.indexer.'.$name.'.vectorizer'), 486 | '$store' => new Reference($config['store']), 487 | ]); 488 | 489 | $container->setDefinition('llm_chain.indexer.'.$name, $definition); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/LlmChainBundle.php: -------------------------------------------------------------------------------- 1 | platforms = $platforms instanceof \Traversable ? iterator_to_array($platforms) : $platforms; 43 | $this->toolboxes = $toolboxes instanceof \Traversable ? iterator_to_array($toolboxes) : $toolboxes; 44 | } 45 | 46 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 47 | { 48 | $this->data = [ 49 | 'tools' => $this->defaultToolBox->getTools(), 50 | 'platform_calls' => array_merge(...array_map($this->awaitCallResults(...), $this->platforms)), 51 | 'tool_calls' => array_merge(...array_map(fn (TraceableToolbox $toolbox) => $toolbox->calls, $this->toolboxes)), 52 | ]; 53 | } 54 | 55 | public static function getTemplate(): string 56 | { 57 | return '@LlmChain/data_collector.html.twig'; 58 | } 59 | 60 | /** 61 | * @return PlatformCallData[] 62 | */ 63 | public function getPlatformCalls(): array 64 | { 65 | return $this->data['platform_calls'] ?? []; 66 | } 67 | 68 | /** 69 | * @return Tool[] 70 | */ 71 | public function getTools(): array 72 | { 73 | return $this->data['tools'] ?? []; 74 | } 75 | 76 | /** 77 | * @return ToolCallData[] 78 | */ 79 | public function getToolCalls(): array 80 | { 81 | return $this->data['tool_calls'] ?? []; 82 | } 83 | 84 | /** 85 | * @return array{ 86 | * model: Model, 87 | * input: array|string|object, 88 | * options: array, 89 | * response: string|iterable|object|null 90 | * }[] 91 | */ 92 | private function awaitCallResults(TraceablePlatform $platform): array 93 | { 94 | $calls = $platform->calls; 95 | foreach ($calls as $key => $call) { 96 | $call['response'] = $call['response']->await()->getContent(); 97 | $calls[$key] = $call; 98 | } 99 | 100 | return $calls; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Profiler/TraceablePlatform.php: -------------------------------------------------------------------------------- 1 | |string|object, 16 | * options: array, 17 | * response: ResponsePromise, 18 | * } 19 | */ 20 | final class TraceablePlatform implements PlatformInterface 21 | { 22 | /** 23 | * @var PlatformCallData[] 24 | */ 25 | public array $calls = []; 26 | 27 | public function __construct( 28 | private readonly PlatformInterface $platform, 29 | ) { 30 | } 31 | 32 | public function request(Model $model, array|string|object $input, array $options = []): ResponsePromise 33 | { 34 | $response = $this->platform->request($model, $input, $options); 35 | 36 | if ($input instanceof File) { 37 | $input = $input::class.': '.$input->getFormat(); 38 | } 39 | 40 | $this->calls[] = [ 41 | 'model' => $model, 42 | 'input' => is_object($input) ? clone $input : $input, 43 | 'options' => $options, 44 | 'response' => $response, 45 | ]; 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Profiler/TraceableToolbox.php: -------------------------------------------------------------------------------- 1 | toolbox->getTools(); 31 | } 32 | 33 | public function execute(ToolCall $toolCall): mixed 34 | { 35 | $result = $this->toolbox->execute($toolCall); 36 | 37 | $this->calls[] = [ 38 | 'call' => $toolCall, 39 | 'result' => $result, 40 | ]; 41 | 42 | return $result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | services() 22 | ->defaults() 23 | ->autowire() 24 | 25 | // structured output 26 | ->set(ResponseFormatFactory::class) 27 | ->alias(ResponseFormatFactoryInterface::class, ResponseFormatFactory::class) 28 | ->set(StructureOutputProcessor::class) 29 | ->tag('llm_chain.chain.input_processor') 30 | ->tag('llm_chain.chain.output_processor') 31 | 32 | // tools 33 | ->set('llm_chain.toolbox.abstract') 34 | ->class(Toolbox::class) 35 | ->autowire() 36 | ->abstract() 37 | ->args([ 38 | '$toolFactory' => service(ToolFactoryInterface::class), 39 | '$tools' => abstract_arg('Collection of tools'), 40 | ]) 41 | ->set(Toolbox::class) 42 | ->parent('llm_chain.toolbox.abstract') 43 | ->args([ 44 | '$tools' => tagged_iterator('llm_chain.tool'), 45 | ]) 46 | ->alias(ToolboxInterface::class, Toolbox::class) 47 | ->set(ReflectionToolFactory::class) 48 | ->alias(ToolFactoryInterface::class, ReflectionToolFactory::class) 49 | ->set(ToolResultConverter::class) 50 | ->set(ToolCallArgumentResolver::class) 51 | ->set('llm_chain.tool.chain_processor.abstract') 52 | ->class(ToolProcessor::class) 53 | ->abstract() 54 | ->args([ 55 | '$toolbox' => abstract_arg('Toolbox'), 56 | ]) 57 | ->set(ToolProcessor::class) 58 | ->parent('llm_chain.tool.chain_processor.abstract') 59 | ->tag('llm_chain.chain.input_processor') 60 | ->tag('llm_chain.chain.output_processor') 61 | ->args([ 62 | '$toolbox' => service(ToolboxInterface::class), 63 | '$eventDispatcher' => service('event_dispatcher')->nullOnInvalid(), 64 | ]) 65 | 66 | // profiler 67 | ->set(DataCollector::class) 68 | ->tag('data_collector') 69 | ->set(TraceableToolbox::class) 70 | ->decorate(ToolboxInterface::class) 71 | ->tag('llm_chain.traceable_toolbox') 72 | ; 73 | }; 74 | -------------------------------------------------------------------------------- /src/Resources/views/data_collector.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 2 | 3 | {% block toolbar %} 4 | {% if collector.platformCalls|length > 0 %} 5 | {% set icon %} 6 | {{ include('@LlmChain/icon.svg', { y: 18 }) }} 7 | {{ collector.platformCalls|length }} 8 | 9 | calls 10 | 11 | {% endset %} 12 | 13 | {% set text %} 14 |
15 |
16 | Configured Platforms 17 | 1 18 |
19 |
20 | Platform Calls 21 | {{ collector.platformCalls|length }} 22 |
23 |
24 | Registered Tools 25 | {{ collector.tools|length }} 26 |
27 |
28 | Tool Calls 29 | {{ collector.toolCalls|length }} 30 |
31 |
32 | {% endset %} 33 | 34 | {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }} 35 | {% endif %} 36 | {% endblock %} 37 | 38 | {% block menu %} 39 | 40 | {{ include('@LlmChain/icon.svg', { y: 16 }) }} 41 | LLM Chain 42 | {{ collector.platformCalls|length }} 43 | 44 | {% endblock %} 45 | 46 | {% macro tool_calls(toolCalls) %} 47 | Tool call{{ toolCalls|length > 1 ? 's' }}: 48 |
    49 | {% for toolCall in toolCalls %} 50 |
  1. 51 | {{ toolCall.name }}({{ toolCall.arguments|map((value, key) => "#{key}: #{value}")|join(', ') }}) 52 | (ID: {{ toolCall.id }}) 53 |
  2. 54 | {% endfor %} 55 |
56 | {% endmacro %} 57 | 58 | {% block panel %} 59 |

LLM Chain

60 |
61 |
62 |
63 | 1 64 | Platforms 65 |
66 |
67 | {{ collector.platformCalls|length }} 68 | Platform Calls 69 |
70 |
71 |
72 |
73 |
74 | {{ collector.tools|length }} 75 | Tools 76 |
77 |
78 | {{ collector.toolCalls|length }} 79 | Tool Calls 80 |
81 |
82 |
83 |

Platform Calls

84 | {% if collector.platformCalls|length %} 85 |
86 |
87 |

Platform Calls {{ collector.platformCalls|length }}

88 |
89 | {% for call in collector.platformCalls %} 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 132 | 133 | 134 | 135 | 152 | 153 | 154 | 155 | 168 | 169 | 170 |
Call {{ loop.index }}
Model{{ constant('class', call.model) }} (Version: {{ call.model.name }})
Input 104 | {% if call.input.messages is defined %}{# expect MessageBag #} 105 |
    106 | {% for message in call.input.messages %} 107 |
  1. 108 | {{ message.role.value|title }}: 109 | {% if 'assistant' == message.role.value and message.hasToolCalls%} 110 | {{ _self.tool_calls(message.toolCalls) }} 111 | {% elseif 'tool' == message.role.value %} 112 | Result of tool call with ID {{ message.toolCall.id }}
    113 | {{ message.content|nl2br }} 114 | {% elseif 'user' == message.role.value %} 115 | {% for item in message.content %} 116 | {% if item.text is defined %} 117 | {{ item.text|nl2br }} 118 | {% else %} 119 | 120 | {% endif %} 121 | {% endfor %} 122 | {% else %} 123 | {{ message.content|nl2br }} 124 | {% endif %} 125 |
  2. 126 | {% endfor %} 127 |
128 | {% else %} 129 | {{ dump(call.input) }} 130 | {% endif %} 131 |
Options 136 |
    137 | {% for key, value in call.options %} 138 | {% if key == 'tools' %} 139 |
  • {{ key }}: 140 |
      141 | {% for tool in value %} 142 |
    • {{ tool.name }}
    • 143 | {% endfor %} 144 |
    145 |
  • 146 | {% else %} 147 |
  • {{ key }}: {{ dump(value) }}
  • 148 | {% endif %} 149 | {% endfor %} 150 |
151 |
Response 156 | {% if call.input.messages is defined and call.response is iterable %}{# expect array of ToolCall #} 157 | {{ _self.tool_calls(call.response) }} 158 | {% elseif call.response is iterable %}{# expect array of Vectors #} 159 |
    160 | {% for vector in call.response %} 161 |
  1. Vector with {{ vector.dimensions }} dimensions
  2. 162 | {% endfor %} 163 |
164 | {% else %} 165 | {{ call.response }} 166 | {% endif %} 167 |
171 | {% endfor %} 172 |
173 |
174 |
175 | {% else %} 176 |
177 |

No platform calls were made.

178 |
179 | {% endif %} 180 | 181 |

Tools

182 | {% if collector.tools|length %} 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | {% for tool in collector.tools %} 194 | 195 | 196 | 197 | 198 | 212 | 213 | {% endfor %} 214 | 215 |
NameDescriptionClass & MethodParameters
{{ tool.name }}{{ tool.description }}{{ tool.reference.class }}::{{ tool.reference.method }} 199 | {% if tool.parameters %} 200 |
    201 | {% for name, parameter in tool.parameters.properties %} 202 |
  • 203 | {{ name }} ({{ parameter.type }})
    204 | {{ parameter.description|default() }} 205 |
  • 206 | {% endfor %} 207 |
208 | {% else %} 209 | none 210 | {% endif %} 211 |
216 | {% else %} 217 |
218 |

No tools were registered.

219 |
220 | {% endif %} 221 | 222 |

Tool Calls

223 | {% if collector.toolCalls|length %} 224 | {% for call in collector.toolCalls %} 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 |
{{ call.call.name }}
ID{{ call.call.id }}
Arguments{{ dump(call.call.arguments) }}
Response{{ call.result|nl2br }}
246 | {% endfor %} 247 | {% else %} 248 |
249 |

No tool calls were made.

250 |
251 | {% endif %} 252 | {% endblock %} 253 | -------------------------------------------------------------------------------- /src/Resources/views/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | LLM 16 | 17 | -------------------------------------------------------------------------------- /tests/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | setParameter('kernel.debug', true); 23 | $extension = new LlmChainExtension(); 24 | 25 | $configs = $this->getFullConfig(); 26 | $extension->load($configs, $container); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | private function getFullConfig(): array 33 | { 34 | return [ 35 | 'llm_chain' => [ 36 | 'platform' => [ 37 | 'anthropic' => [ 38 | 'api_key' => 'anthropic_key_full', 39 | ], 40 | 'azure' => [ 41 | 'my_azure_instance' => [ 42 | 'api_key' => 'azure_key_full', 43 | 'base_url' => 'https://myazure.openai.azure.com/', 44 | 'deployment' => 'gpt-35-turbo', 45 | 'api_version' => '2024-02-15-preview', 46 | ], 47 | 'another_azure_instance' => [ 48 | 'api_key' => 'azure_key_2', 49 | 'base_url' => 'https://myazure2.openai.azure.com/', 50 | 'deployment' => 'gpt-4', 51 | 'api_version' => '2024-02-15-preview', 52 | ], 53 | ], 54 | 'google' => [ 55 | 'api_key' => 'google_key_full', 56 | ], 57 | 'openai' => [ 58 | 'api_key' => 'openai_key_full', 59 | ], 60 | 'mistral' => [ 61 | 'api_key' => 'mistral_key_full', 62 | ], 63 | 'openrouter' => [ 64 | 'api_key' => 'openrouter_key_full', 65 | ], 66 | ], 67 | 'chain' => [ 68 | 'my_chat_chain' => [ 69 | 'platform' => 'openai_platform_service_id', 70 | 'model' => [ 71 | 'name' => 'gpt', 72 | 'version' => 'gpt-3.5-turbo', 73 | 'options' => [ 74 | 'temperature' => 0.7, 75 | 'max_tokens' => 150, 76 | 'nested' => ['options' => ['work' => 'too']], 77 | ], 78 | ], 79 | 'structured_output' => false, 80 | 'system_prompt' => 'You are a helpful assistant.', 81 | 'include_tools' => true, 82 | 'tools' => [ 83 | 'enabled' => true, 84 | 'services' => [ 85 | ['service' => 'my_tool_service_id', 'name' => 'myTool', 'description' => 'A test tool'], 86 | 'another_tool_service_id', // String format 87 | ], 88 | ], 89 | 'fault_tolerant_toolbox' => false, 90 | ], 91 | 'another_chain' => [ 92 | 'model' => ['name' => 'claude', 'version' => 'claude-3-opus-20240229'], 93 | 'system_prompt' => 'Be concise.', 94 | ], 95 | ], 96 | 'store' => [ 97 | 'azure_search' => [ 98 | 'my_azure_search_store' => [ 99 | 'endpoint' => 'https://mysearch.search.windows.net', 100 | 'api_key' => 'azure_search_key', 101 | 'index_name' => 'my-documents', 102 | 'api_version' => '2023-11-01', 103 | 'vector_field' => 'contentVector', 104 | ], 105 | ], 106 | 'chroma_db' => [ 107 | 'my_chroma_store' => [ 108 | 'collection' => 'my_collection', 109 | ], 110 | ], 111 | 'mongodb' => [ 112 | 'my_mongo_store' => [ 113 | 'database' => 'my_db', 114 | 'collection' => 'my_collection', 115 | 'index_name' => 'vector_index', 116 | 'vector_field' => 'embedding', 117 | 'bulk_write' => true, 118 | ], 119 | ], 120 | 'pinecone' => [ 121 | 'my_pinecone_store' => [ 122 | 'namespace' => 'my_namespace', 123 | 'filter' => ['category' => 'books'], 124 | 'top_k' => 10, 125 | ], 126 | ], 127 | ], 128 | 'indexer' => [ 129 | 'my_text_indexer' => [ 130 | 'store' => 'my_azure_search_store_service_id', 131 | 'platform' => 'google_platform_service_id', 132 | 'model' => [ 133 | 'name' => 'embeddings', 134 | 'version' => 'text-embedding-004', 135 | 'options' => ['dimension' => 768], 136 | ], 137 | ], 138 | ], 139 | ], 140 | ]; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/Profiler/TraceableToolboxTest.php: -------------------------------------------------------------------------------- 1 | createToolbox(['tool' => $metadata]); 26 | $traceableToolbox = new TraceableToolbox($toolbox); 27 | 28 | $map = $traceableToolbox->getTools(); 29 | 30 | self::assertSame(['tool' => $metadata], $map); 31 | } 32 | 33 | #[Test] 34 | public function execute(): void 35 | { 36 | $metadata = new Tool(new ExecutionReference('Foo\Bar'), 'bar', 'description', null); 37 | $toolbox = $this->createToolbox(['tool' => $metadata]); 38 | $traceableToolbox = new TraceableToolbox($toolbox); 39 | $toolCall = new ToolCall('foo', '__invoke'); 40 | 41 | $result = $traceableToolbox->execute($toolCall); 42 | 43 | self::assertSame('tool_result', $result); 44 | self::assertCount(1, $traceableToolbox->calls); 45 | self::assertSame($toolCall, $traceableToolbox->calls[0]['call']); 46 | self::assertSame('tool_result', $traceableToolbox->calls[0]['result']); 47 | } 48 | 49 | /** 50 | * @param Tool[] $tools 51 | */ 52 | private function createToolbox(array $tools): ToolboxInterface 53 | { 54 | return new class($tools) implements ToolboxInterface { 55 | public function __construct( 56 | private readonly array $tools, 57 | ) { 58 | } 59 | 60 | public function getTools(): array 61 | { 62 | return $this->tools; 63 | } 64 | 65 | public function execute(ToolCall $toolCall): string 66 | { 67 | return 'tool_result'; 68 | } 69 | }; 70 | } 71 | } 72 | --------------------------------------------------------------------------------