├── .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 └── Profiler └── TraceableToolboxTest.php /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: pipeline 2 | on: pull_request 3 | 4 | permissions: 5 | contents: read 6 | pull-requests: write 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | php: ['8.2', '8.3', '8.4'] 14 | dependencies: ['lowest', 'highest'] 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | 24 | - name: Install Composer 25 | uses: "ramsey/composer-install@v3" 26 | with: 27 | dependency-versions: "${{ matrix.dependencies }}" 28 | 29 | - name: Composer Validation 30 | run: composer validate --strict 31 | 32 | - name: Install PHP Dependencies 33 | run: composer install --no-scripts 34 | 35 | - name: Tests 36 | run: vendor/bin/phpunit 37 | 38 | qa: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Conventional Commit 45 | uses: ytanikin/pr-conventional-commits@1.4.1 46 | with: 47 | task_types: '["feat", "fix", "docs", "test", "ci", "style", "refactor", "perf", "chore", "revert"]' 48 | add_label: 'true' 49 | custom_labels: '{"feat": "feature", "fix": "bug", "docs": "documentation", "test": "test", "ci": "CI/CD", "style": "codestyle", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert"}' 50 | 51 | - name: Setup PHP 52 | uses: shivammathur/setup-php@v2 53 | with: 54 | php-version: '8.2' 55 | 56 | - name: Install Composer 57 | uses: "ramsey/composer-install@v3" 58 | 59 | - name: Composer Validation 60 | run: composer validate --strict 61 | 62 | - name: Install PHP Dependencies 63 | run: composer install --no-scripts 64 | 65 | - name: Code Style PHP 66 | run: vendor/bin/php-cs-fixer fix --dry-run 67 | 68 | - name: Rector 69 | run: vendor/bin/rector --dry-run 70 | 71 | - name: PHPStan 72 | run: vendor/bin/phpstan analyse 73 | -------------------------------------------------------------------------------- /.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 | Symfony integration bundle for [php-llm/llm-chain](https://github.com/php-llm/llm-chain) library. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer require php-llm/llm-chain-bundle 9 | ``` 10 | 11 | ## Configuration 12 | 13 | ### Basic Example with OpenAI 14 | 15 | ```yaml 16 | # config/packages/llm_chain.yaml 17 | llm_chain: 18 | platform: 19 | openai: 20 | api_key: '%env(OPENAI_API_KEY)%' 21 | chain: 22 | default: 23 | model: 24 | name: 'GPT' 25 | ``` 26 | 27 | ### Advanced Example with Anthropic, Azure, Google and multiple chains 28 | ```yaml 29 | # config/packages/llm_chain.yaml 30 | llm_chain: 31 | platform: 32 | anthropic: 33 | api_key: '%env(ANTHROPIC_API_KEY)%' 34 | azure: 35 | # multiple deployments possible 36 | gpt_deployment: 37 | base_url: '%env(AZURE_OPENAI_BASEURL)%' 38 | deployment: '%env(AZURE_OPENAI_GPT)%' 39 | api_key: '%env(AZURE_OPENAI_KEY)%' 40 | api_version: '%env(AZURE_GPT_VERSION)%' 41 | google: 42 | api_key: '%env(GOOGLE_API_KEY)%' 43 | chain: 44 | rag: 45 | platform: 'llm_chain.platform.azure.gpt_deployment' 46 | structured_output: false # Disables support for "output_structure" option, default is true 47 | model: 48 | name: 'GPT' 49 | version: 'gpt-4o-mini' 50 | system_prompt: 'You are a helpful assistant that can answer questions.' # The default system prompt of the chain 51 | include_tools: true # Include tool definitions at the end of the system prompt 52 | tools: 53 | # Referencing a service with #[AsTool] attribute 54 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch' 55 | 56 | # Referencing a service without #[AsTool] attribute 57 | - service: 'App\Chain\Tool\CompanyName' 58 | name: 'company_name' 59 | description: 'Provides the name of your company' 60 | method: 'foo' # Optional with default value '__invoke' 61 | 62 | # Referencing a chain => chain in chain 🤯 63 | - service: 'llm_chain.chain.research' 64 | name: 'wikipedia_research' 65 | description: 'Can research on Wikipedia' 66 | is_chain: true 67 | research: 68 | platform: 'llm_chain.platform.anthropic' 69 | model: 70 | name: 'Claude' 71 | tools: # If undefined, all tools are injected into the chain, use "tools: false" to disable tools. 72 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia' 73 | fault_tolerant_toolbox: false # Disables fault tolerant toolbox, default is true 74 | store: 75 | # also azure_search, mongodb and pinecone are supported as store type 76 | chroma_db: 77 | # multiple collections possible per type 78 | default: 79 | collection: 'my_collection' 80 | embedder: 81 | default: 82 | # platform: 'llm_chain.platform.anthropic' 83 | # store: 'llm_chain.store.chroma_db.default' 84 | model: 85 | name: 'Embeddings' 86 | version: 'text-embedding-ada-002' 87 | ``` 88 | 89 | ## Usage 90 | 91 | ### Chain Service 92 | 93 | Use the `Chain` service to leverage GPT: 94 | ```php 95 | use PhpLlm\LlmChain\ChainInterface; 96 | use PhpLlm\LlmChain\Model\Message\Message; 97 | use PhpLlm\LlmChain\Model\Message\MessageBag; 98 | 99 | final readonly class MyService 100 | { 101 | public function __construct( 102 | private ChainInterface $chain, 103 | ) { 104 | } 105 | 106 | public function submit(string $message): string 107 | { 108 | $messages = new MessageBag( 109 | Message::forSystem('Speak like a pirate.'), 110 | Message::ofUser($message), 111 | ); 112 | 113 | return $this->chain->call($messages); 114 | } 115 | } 116 | ``` 117 | 118 | ### Register Tools 119 | 120 | To use existing tools, you can register them as a service: 121 | ```yaml 122 | services: 123 | _defaults: 124 | autowire: true 125 | autoconfigure: true 126 | 127 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Clock: ~ 128 | PhpLlm\LlmChain\Chain\Toolbox\Tool\OpenMeteo: ~ 129 | PhpLlm\LlmChain\Chain\Toolbox\Tool\SerpApi: 130 | $apiKey: '%env(SERP_API_KEY)%' 131 | PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch: ~ 132 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Tavily: 133 | $apiKey: '%env(TAVILY_API_KEY)%' 134 | PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia: ~ 135 | PhpLlm\LlmChain\Chain\Toolbox\Tool\YouTubeTranscriber: ~ 136 | ``` 137 | 138 | Custom tools can be registered by using the `#[AsTool]` attribute: 139 | 140 | ```php 141 | use PhpLlm\LlmChain\Chain\Toolbox\Attribute\AsTool; 142 | 143 | #[AsTool('company_name', 'Provides the name of your company')] 144 | final class CompanyName 145 | { 146 | public function __invoke(): string 147 | { 148 | return 'ACME Corp.' 149 | } 150 | } 151 | ``` 152 | 153 | The chain configuration by default will inject all known tools into the chain. 154 | 155 | To disable this behavior, set the `tools` option to `false`: 156 | ```yaml 157 | llm_chain: 158 | chain: 159 | my_chain: 160 | tools: false 161 | ``` 162 | 163 | To inject only specific tools, list them in the configuration: 164 | ```yaml 165 | llm_chain: 166 | chain: 167 | my_chain: 168 | tools: 169 | - 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch' 170 | ``` 171 | 172 | ### Profiler 173 | 174 | The profiler panel provides insights into the chain's execution: 175 | 176 | ![Profiler](./profiler.png) 177 | -------------------------------------------------------------------------------- /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 | "require": { 17 | "php": ">=8.2", 18 | "php-llm/llm-chain": "^0.22", 19 | "symfony/config": "^6.4 || ^7.0", 20 | "symfony/dependency-injection": "^6.4 || ^7.0", 21 | "symfony/framework-bundle": "^6.4 || ^7.0", 22 | "symfony/string": "^6.4 || ^7.0" 23 | }, 24 | "require-dev": { 25 | "php-cs-fixer/shim": "^3.69", 26 | "phpstan/phpstan": "^2.1", 27 | "phpunit/phpunit": "^11.5", 28 | "rector/rector": "^2.0" 29 | }, 30 | "config": { 31 | "sort-packages": true 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "PhpLlm\\LlmChainBundle\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "PhpLlm\\LlmChainBundle\\Tests\\": "tests/" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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/fd4881163854a4518a468967106d975c1c97564e/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 | ->end() 52 | ->end() 53 | ->arrayNode('chain') 54 | ->normalizeKeys(false) 55 | ->useAttributeAsKey('name') 56 | ->arrayPrototype() 57 | ->children() 58 | ->scalarNode('platform') 59 | ->info('Service name of platform') 60 | ->defaultValue(PlatformInterface::class) 61 | ->end() 62 | ->arrayNode('model') 63 | ->children() 64 | ->scalarNode('name')->isRequired()->end() 65 | ->scalarNode('version')->defaultNull()->end() 66 | ->arrayNode('options') 67 | ->scalarPrototype()->end() 68 | ->end() 69 | ->end() 70 | ->end() 71 | ->booleanNode('structured_output')->defaultTrue()->end() 72 | ->scalarNode('system_prompt') 73 | ->validate() 74 | ->ifTrue(fn ($v) => null !== $v && '' === trim($v)) 75 | ->thenInvalid('The default system prompt must not be an empty string') 76 | ->end() 77 | ->defaultNull() 78 | ->info('The default system prompt of the chain') 79 | ->end() 80 | ->booleanNode('include_tools') 81 | ->info('Include tool definitions at the end of the system prompt') 82 | ->defaultFalse() 83 | ->end() 84 | ->arrayNode('tools') 85 | ->addDefaultsIfNotSet() 86 | ->treatFalseLike(['enabled' => false]) 87 | ->treatTrueLike(['enabled' => true]) 88 | ->treatNullLike(['enabled' => true]) 89 | ->beforeNormalization() 90 | ->ifArray() 91 | ->then(function (array $v) { 92 | return [ 93 | 'enabled' => $v['enabled'] ?? true, 94 | 'services' => $v['services'] ?? $v, 95 | ]; 96 | }) 97 | ->end() 98 | ->children() 99 | ->booleanNode('enabled')->defaultTrue()->end() 100 | ->arrayNode('services') 101 | ->arrayPrototype() 102 | ->children() 103 | ->scalarNode('service')->isRequired()->end() 104 | ->scalarNode('name')->end() 105 | ->scalarNode('description')->end() 106 | ->scalarNode('method')->end() 107 | ->booleanNode('is_chain')->defaultFalse()->end() 108 | ->end() 109 | ->beforeNormalization() 110 | ->ifString() 111 | ->then(function (string $v) { 112 | return ['service' => $v]; 113 | }) 114 | ->end() 115 | ->end() 116 | ->end() 117 | ->end() 118 | ->end() 119 | ->booleanNode('fault_tolerant_toolbox')->defaultTrue()->end() 120 | ->end() 121 | ->end() 122 | ->end() 123 | ->arrayNode('store') 124 | ->children() 125 | ->arrayNode('azure_search') 126 | ->normalizeKeys(false) 127 | ->useAttributeAsKey('name') 128 | ->arrayPrototype() 129 | ->children() 130 | ->scalarNode('endpoint')->isRequired()->end() 131 | ->scalarNode('api_key')->isRequired()->end() 132 | ->scalarNode('index_name')->isRequired()->end() 133 | ->scalarNode('api_version')->isRequired()->end() 134 | ->scalarNode('vector_field')->end() 135 | ->end() 136 | ->end() 137 | ->end() 138 | ->arrayNode('chroma_db') 139 | ->normalizeKeys(false) 140 | ->useAttributeAsKey('name') 141 | ->arrayPrototype() 142 | ->children() 143 | ->scalarNode('collection')->isRequired()->end() 144 | ->end() 145 | ->end() 146 | ->end() 147 | ->arrayNode('mongodb') 148 | ->normalizeKeys(false) 149 | ->useAttributeAsKey('name') 150 | ->arrayPrototype() 151 | ->children() 152 | ->scalarNode('database')->isRequired()->end() 153 | ->scalarNode('collection')->isRequired()->end() 154 | ->scalarNode('index_name')->isRequired()->end() 155 | ->scalarNode('vector_field')->end() 156 | ->booleanNode('bulk_write')->end() 157 | ->end() 158 | ->end() 159 | ->end() 160 | ->arrayNode('pinecone') 161 | ->normalizeKeys(false) 162 | ->useAttributeAsKey('name') 163 | ->arrayPrototype() 164 | ->children() 165 | ->scalarNode('namespace')->end() 166 | ->arrayNode('filter') 167 | ->scalarPrototype()->end() 168 | ->end() 169 | ->integerNode('top_k')->end() 170 | ->end() 171 | ->end() 172 | ->end() 173 | ->end() 174 | ->end() 175 | ->arrayNode('embedder') 176 | ->normalizeKeys(false) 177 | ->useAttributeAsKey('name') 178 | ->arrayPrototype() 179 | ->children() 180 | ->scalarNode('store') 181 | ->info('Service name of store') 182 | ->defaultValue(StoreInterface::class) 183 | ->end() 184 | ->scalarNode('platform') 185 | ->info('Service name of platform') 186 | ->defaultValue(PlatformInterface::class) 187 | ->end() 188 | ->arrayNode('model') 189 | ->children() 190 | ->scalarNode('name')->isRequired()->end() 191 | ->scalarNode('version')->defaultNull()->end() 192 | ->arrayNode('options') 193 | ->scalarPrototype()->end() 194 | ->end() 195 | ->end() 196 | ->end() 197 | ->end() 198 | ->end() 199 | ->end() 200 | ->end() 201 | ; 202 | 203 | return $treeBuilder; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/DependencyInjection/LlmChainExtension.php: -------------------------------------------------------------------------------- 1 | load('services.php'); 60 | 61 | $configuration = new Configuration(); 62 | $config = $this->processConfiguration($configuration, $configs); 63 | foreach ($config['platform'] ?? [] as $type => $platform) { 64 | $this->processPlatformConfig($type, $platform, $container); 65 | } 66 | $platforms = array_keys($container->findTaggedServiceIds('llm_chain.platform')); 67 | if (1 === count($platforms)) { 68 | $container->setAlias(PlatformInterface::class, reset($platforms)); 69 | } 70 | if ($container->getParameter('kernel.debug')) { 71 | foreach ($platforms as $platform) { 72 | $traceablePlatformDefinition = (new Definition(TraceablePlatform::class)) 73 | ->setDecoratedService($platform) 74 | ->setAutowired(true) 75 | ->addTag('llm_chain.traceable_platform'); 76 | $suffix = u($platform)->afterLast('.')->toString(); 77 | $container->setDefinition('llm_chain.traceable_platform.'.$suffix, $traceablePlatformDefinition); 78 | } 79 | } 80 | 81 | foreach ($config['chain'] as $chainName => $chain) { 82 | $this->processChainConfig($chainName, $chain, $container); 83 | } 84 | if (1 === count($config['chain']) && isset($chainName)) { 85 | $container->setAlias(ChainInterface::class, 'llm_chain.chain.'.$chainName); 86 | } 87 | 88 | foreach ($config['store'] ?? [] as $type => $store) { 89 | $this->processStoreConfig($type, $store, $container); 90 | } 91 | $stores = array_keys($container->findTaggedServiceIds('llm_chain.store')); 92 | if (1 === count($stores)) { 93 | $container->setAlias(VectorStoreInterface::class, reset($stores)); 94 | $container->setAlias(StoreInterface::class, reset($stores)); 95 | } 96 | 97 | foreach ($config['embedder'] as $embedderName => $embedder) { 98 | $this->processEmbedderConfig($embedderName, $embedder, $container); 99 | } 100 | if (1 === count($config['embedder']) && isset($embedderName)) { 101 | $container->setAlias(Embedder::class, 'llm_chain.embedder.'.$embedderName); 102 | } 103 | 104 | $container->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void { 105 | $definition->addTag('llm_chain.tool', [ 106 | 'name' => $attribute->name, 107 | 'description' => $attribute->description, 108 | 'method' => $attribute->method, 109 | ]); 110 | }); 111 | 112 | $container->registerForAutoconfiguration(InputProcessorInterface::class) 113 | ->addTag('llm_chain.chain.input_processor'); 114 | $container->registerForAutoconfiguration(OutputProcessorInterface::class) 115 | ->addTag('llm_chain.chain.output_processor'); 116 | $container->registerForAutoconfiguration(ModelClientInterface::class) 117 | ->addTag('llm_chain.platform.model_client'); 118 | $container->registerForAutoconfiguration(ResponseConverterInterface::class) 119 | ->addTag('llm_chain.platform.response_converter'); 120 | 121 | if (false === $container->getParameter('kernel.debug')) { 122 | $container->removeDefinition(DataCollector::class); 123 | $container->removeDefinition(TraceableToolbox::class); 124 | } 125 | } 126 | 127 | /** 128 | * @param array $platform 129 | */ 130 | private function processPlatformConfig(string $type, array $platform, ContainerBuilder $container): void 131 | { 132 | if ('anthropic' === $type) { 133 | $platformId = 'llm_chain.platform.anthropic'; 134 | $definition = (new Definition(Platform::class)) 135 | ->setFactory(AnthropicPlatformFactory::class.'::create') 136 | ->setAutowired(true) 137 | ->setLazy(true) 138 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 139 | ->setArguments([ 140 | '$apiKey' => $platform['api_key'], 141 | ]) 142 | ->addTag('llm_chain.platform'); 143 | 144 | if (isset($platform['version'])) { 145 | $definition->replaceArgument('$version', $platform['version']); 146 | } 147 | 148 | $container->setDefinition($platformId, $definition); 149 | 150 | return; 151 | } 152 | 153 | if ('azure' === $type) { 154 | foreach ($platform as $name => $config) { 155 | $platformId = 'llm_chain.platform.azure.'.$name; 156 | $definition = (new Definition(Platform::class)) 157 | ->setFactory(AzureOpenAIPlatformFactory::class.'::create') 158 | ->setAutowired(true) 159 | ->setLazy(true) 160 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 161 | ->setArguments([ 162 | '$baseUrl' => $config['base_url'], 163 | '$deployment' => $config['deployment'], 164 | '$apiVersion' => $config['api_version'], 165 | '$apiKey' => $config['api_key'], 166 | ]) 167 | ->addTag('llm_chain.platform'); 168 | 169 | $container->setDefinition($platformId, $definition); 170 | } 171 | 172 | return; 173 | } 174 | 175 | if ('google' === $type) { 176 | $platformId = 'llm_chain.platform.google'; 177 | $definition = (new Definition(Platform::class)) 178 | ->setFactory(GooglePlatformFactory::class.'::create') 179 | ->setAutowired(true) 180 | ->setLazy(true) 181 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 182 | ->setArguments(['$apiKey' => $platform['api_key']]) 183 | ->addTag('llm_chain.platform'); 184 | 185 | $container->setDefinition($platformId, $definition); 186 | 187 | return; 188 | } 189 | 190 | if ('openai' === $type) { 191 | $platformId = 'llm_chain.platform.openai'; 192 | $definition = (new Definition(Platform::class)) 193 | ->setFactory(OpenAIPlatformFactory::class.'::create') 194 | ->setAutowired(true) 195 | ->setLazy(true) 196 | ->addTag('proxy', ['interface' => PlatformInterface::class]) 197 | ->setArguments(['$apiKey' => $platform['api_key']]) 198 | ->addTag('llm_chain.platform'); 199 | 200 | $container->setDefinition($platformId, $definition); 201 | 202 | return; 203 | } 204 | 205 | throw new \InvalidArgumentException(sprintf('Platform "%s" is not supported for configuration via bundle at this point.', $type)); 206 | } 207 | 208 | /** 209 | * @param array $config 210 | */ 211 | private function processChainConfig(string $name, array $config, ContainerBuilder $container): void 212 | { 213 | // MODEL 214 | ['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model']; 215 | 216 | $modelClass = match (strtolower((string) $modelName)) { 217 | 'gpt' => GPT::class, 218 | 'claude' => Claude::class, 219 | 'llama' => Llama::class, 220 | 'gemini' => Gemini::class, 221 | default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)), 222 | }; 223 | $modelDefinition = new Definition($modelClass); 224 | if (null !== $version) { 225 | $modelDefinition->setArgument('$name', $version); 226 | } 227 | if (0 !== count($options)) { 228 | $modelDefinition->setArgument('$options', $options); 229 | } 230 | $modelDefinition->addTag('llm_chain.model.language_model'); 231 | $container->setDefinition('llm_chain.chain.'.$name.'.model', $modelDefinition); 232 | 233 | // CHAIN 234 | $chainDefinition = (new Definition(Chain::class)) 235 | ->setAutowired(true) 236 | ->setArgument('$platform', new Reference($config['platform'])) 237 | ->setArgument('$model', new Reference('llm_chain.chain.'.$name.'.model')); 238 | 239 | $inputProcessors = []; 240 | $outputProcessors = []; 241 | 242 | // TOOL & PROCESSOR 243 | if ($config['tools']['enabled']) { 244 | // Create specific toolbox and process if tools are explicitly defined 245 | if (0 !== count($config['tools']['services'])) { 246 | $memoryFactoryDefinition = new Definition(MemoryToolFactory::class); 247 | $container->setDefinition('llm_chain.toolbox.'.$name.'.memory_factory', $memoryFactoryDefinition); 248 | $chainFactoryDefinition = new Definition(ChainFactory::class, [ 249 | '$factories' => [new Reference('llm_chain.toolbox.'.$name.'.memory_factory'), new Reference(ReflectionToolFactory::class)], 250 | ]); 251 | $container->setDefinition('llm_chain.toolbox.'.$name.'.chain_factory', $chainFactoryDefinition); 252 | 253 | $tools = []; 254 | foreach ($config['tools']['services'] as $tool) { 255 | $reference = new Reference($tool['service']); 256 | // We use the memory factory in case method, description and name are set 257 | if (isset($tool['name'], $tool['description'])) { 258 | if ($tool['is_chain']) { 259 | $chainWrapperDefinition = new Definition(ChainTool::class, ['$chain' => $reference]); 260 | $container->setDefinition('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name'], $chainWrapperDefinition); 261 | $reference = new Reference('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name']); 262 | } 263 | $memoryFactoryDefinition->addMethodCall('addTool', [$reference, $tool['name'], $tool['description'], $tool['method'] ?? '__invoke']); 264 | } 265 | $tools[] = $reference; 266 | } 267 | 268 | $toolboxDefinition = (new ChildDefinition('llm_chain.toolbox.abstract')) 269 | ->replaceArgument('$toolFactory', new Reference('llm_chain.toolbox.'.$name.'.chain_factory')) 270 | ->replaceArgument('$tools', $tools); 271 | $container->setDefinition('llm_chain.toolbox.'.$name, $toolboxDefinition); 272 | 273 | if ($config['fault_tolerant_toolbox']) { 274 | $faultTolerantToolboxDefinition = (new Definition('llm_chain.fault_tolerant_toolbox.'.$name)) 275 | ->setClass(FaultTolerantToolbox::class) 276 | ->setAutowired(true) 277 | ->setDecoratedService('llm_chain.toolbox.'.$name); 278 | $container->setDefinition('llm_chain.fault_tolerant_toolbox.'.$name, $faultTolerantToolboxDefinition); 279 | } 280 | 281 | if ($container->getParameter('kernel.debug')) { 282 | $traceableToolboxDefinition = (new Definition('llm_chain.traceable_toolbox.'.$name)) 283 | ->setClass(TraceableToolbox::class) 284 | ->setAutowired(true) 285 | ->setDecoratedService('llm_chain.toolbox.'.$name) 286 | ->addTag('llm_chain.traceable_toolbox'); 287 | $container->setDefinition('llm_chain.traceable_toolbox.'.$name, $traceableToolboxDefinition); 288 | } 289 | 290 | $toolProcessorDefinition = (new ChildDefinition('llm_chain.tool.chain_processor.abstract')) 291 | ->replaceArgument('$toolbox', new Reference('llm_chain.toolbox.'.$name)); 292 | $container->setDefinition('llm_chain.tool.chain_processor.'.$name, $toolProcessorDefinition); 293 | 294 | $inputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name); 295 | $outputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name); 296 | } else { 297 | $inputProcessors[] = new Reference(ToolProcessor::class); 298 | $outputProcessors[] = new Reference(ToolProcessor::class); 299 | } 300 | } 301 | 302 | // STRUCTURED OUTPUT 303 | if ($config['structured_output']) { 304 | $inputProcessors[] = new Reference(StructureOutputProcessor::class); 305 | $outputProcessors[] = new Reference(StructureOutputProcessor::class); 306 | } 307 | 308 | // SYSTEM PROMPT 309 | if (is_string($config['system_prompt'])) { 310 | $systemPromptInputProcessorDefinition = new Definition(SystemPromptInputProcessor::class); 311 | $systemPromptInputProcessorDefinition 312 | ->setAutowired(true) 313 | ->setArguments([ 314 | '$systemPrompt' => $config['system_prompt'], 315 | '$toolbox' => $config['include_tools'] ? new Reference('llm_chain.toolbox.'.$name) : null, 316 | ]); 317 | 318 | $inputProcessors[] = $systemPromptInputProcessorDefinition; 319 | } 320 | 321 | $chainDefinition 322 | ->setArgument('$inputProcessors', $inputProcessors) 323 | ->setArgument('$outputProcessors', $outputProcessors); 324 | 325 | $container->setDefinition('llm_chain.chain.'.$name, $chainDefinition); 326 | } 327 | 328 | /** 329 | * @param array $stores 330 | */ 331 | private function processStoreConfig(string $type, array $stores, ContainerBuilder $container): void 332 | { 333 | if ('azure_search' === $type) { 334 | foreach ($stores as $name => $store) { 335 | $arguments = [ 336 | '$endpointUrl' => $store['endpoint'], 337 | '$apiKey' => $store['api_key'], 338 | '$indexName' => $store['index_name'], 339 | '$apiVersion' => $store['api_version'], 340 | ]; 341 | 342 | if (array_key_exists('vector_field', $store)) { 343 | $arguments['$vectorFieldName'] = $store['vector_field']; 344 | } 345 | 346 | $definition = new Definition(AzureSearchStore::class); 347 | $definition 348 | ->setAutowired(true) 349 | ->addTag('llm_chain.store') 350 | ->setArguments($arguments); 351 | 352 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 353 | } 354 | } 355 | 356 | if ('chroma_db' === $type) { 357 | foreach ($stores as $name => $store) { 358 | $definition = new Definition(ChromaDBStore::class); 359 | $definition 360 | ->setAutowired(true) 361 | ->setArgument('$collectionName', $store['collection']) 362 | ->addTag('llm_chain.store'); 363 | 364 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 365 | } 366 | } 367 | 368 | if ('mongodb' === $type) { 369 | foreach ($stores as $name => $store) { 370 | $arguments = [ 371 | '$databaseName' => $store['database'], 372 | '$collectionName' => $store['collection'], 373 | '$indexName' => $store['index_name'], 374 | ]; 375 | 376 | if (array_key_exists('vector_field', $store)) { 377 | $arguments['$vectorFieldName'] = $store['vector_field']; 378 | } 379 | 380 | if (array_key_exists('bulk_write', $store)) { 381 | $arguments['$bulkWrite'] = $store['bulk_write']; 382 | } 383 | 384 | $definition = new Definition(MongoDBStore::class); 385 | $definition 386 | ->setAutowired(true) 387 | ->addTag('llm_chain.store') 388 | ->setArguments($arguments); 389 | 390 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 391 | } 392 | } 393 | 394 | if ('pinecone' === $type) { 395 | foreach ($stores as $name => $store) { 396 | $arguments = [ 397 | '$namespace' => $store['namespace'], 398 | ]; 399 | 400 | if (array_key_exists('filter', $store)) { 401 | $arguments['$filter'] = $store['filter']; 402 | } 403 | 404 | if (array_key_exists('top_k', $store)) { 405 | $arguments['$topK'] = $store['top_k']; 406 | } 407 | 408 | $definition = new Definition(PineconeStore::class); 409 | $definition 410 | ->setAutowired(true) 411 | ->addTag('llm_chain.store') 412 | ->setArguments($arguments); 413 | 414 | $container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition); 415 | } 416 | } 417 | } 418 | 419 | /** 420 | * @param array $config 421 | */ 422 | private function processEmbedderConfig(int|string $name, array $config, ContainerBuilder $container): void 423 | { 424 | ['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model']; 425 | 426 | $modelClass = match (strtolower((string) $modelName)) { 427 | 'embeddings' => Embeddings::class, 428 | 'voyage' => Voyage::class, 429 | default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)), 430 | }; 431 | $modelDefinition = (new Definition($modelClass)); 432 | if (null !== $version) { 433 | $modelDefinition->setArgument('$name', $version); 434 | } 435 | if (0 !== count($options)) { 436 | $modelDefinition->setArgument('$options', $options); 437 | } 438 | $modelDefinition->addTag('llm_chain.model.embeddings_model'); 439 | $container->setDefinition('llm_chain.embedder.'.$name.'.model', $modelDefinition); 440 | 441 | $definition = new Definition(Embedder::class, [ 442 | '$model' => new Reference('llm_chain.embedder.'.$name.'.model'), 443 | '$platform' => new Reference($config['platform']), 444 | '$store' => new Reference($config['store']), 445 | ]); 446 | 447 | $container->setDefinition('llm_chain.embedder.'.$name, $definition); 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /src/LlmChainBundle.php: -------------------------------------------------------------------------------- 1 | platforms = $platforms instanceof \Traversable ? iterator_to_array($platforms) : $platforms; 42 | $this->toolboxes = $toolboxes instanceof \Traversable ? iterator_to_array($toolboxes) : $toolboxes; 43 | } 44 | 45 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 46 | { 47 | $this->data = [ 48 | 'tools' => $this->defaultToolBox->getTools(), 49 | 'platform_calls' => array_merge(...array_map(fn (TraceablePlatform $platform) => $platform->calls, $this->platforms)), 50 | 'tool_calls' => array_merge(...array_map(fn (TraceableToolbox $toolbox) => $toolbox->calls, $this->toolboxes)), 51 | ]; 52 | } 53 | 54 | public static function getTemplate(): string 55 | { 56 | return '@LlmChain/data_collector.html.twig'; 57 | } 58 | 59 | /** 60 | * @return PlatformCallData[] 61 | */ 62 | public function getPlatformCalls(): array 63 | { 64 | return $this->data['platform_calls'] ?? []; 65 | } 66 | 67 | /** 68 | * @return Tool[] 69 | */ 70 | public function getTools(): array 71 | { 72 | return $this->data['tools'] ?? []; 73 | } 74 | 75 | /** 76 | * @return ToolCallData[] 77 | */ 78 | public function getToolCalls(): array 79 | { 80 | return $this->data['tool_calls'] ?? []; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Profiler/TraceablePlatform.php: -------------------------------------------------------------------------------- 1 | |string|object, 16 | * options: array, 17 | * response: ResponseInterface, 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 = []): ResponseInterface 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->getContent(), 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() 20 | ->defaults() 21 | ->autowire() 22 | 23 | // structured output 24 | ->set(ResponseFormatFactory::class) 25 | ->alias(ResponseFormatFactoryInterface::class, ResponseFormatFactory::class) 26 | ->set(StructureOutputProcessor::class) 27 | ->tag('llm_chain.chain.input_processor') 28 | ->tag('llm_chain.chain.output_processor') 29 | 30 | // tools 31 | ->set('llm_chain.toolbox.abstract') 32 | ->class(Toolbox::class) 33 | ->autowire() 34 | ->abstract() 35 | ->args([ 36 | '$toolFactory' => service(ToolFactoryInterface::class), 37 | '$tools' => abstract_arg('Collection of tools'), 38 | ]) 39 | ->set(Toolbox::class) 40 | ->parent('llm_chain.toolbox.abstract') 41 | ->args([ 42 | '$tools' => tagged_iterator('llm_chain.tool'), 43 | ]) 44 | ->alias(ToolboxInterface::class, Toolbox::class) 45 | ->set(ReflectionToolFactory::class) 46 | ->alias(ToolFactoryInterface::class, ReflectionToolFactory::class) 47 | ->set('llm_chain.tool.chain_processor.abstract') 48 | ->class(ToolProcessor::class) 49 | ->abstract() 50 | ->args([ 51 | '$toolbox' => abstract_arg('Toolbox'), 52 | ]) 53 | ->set(ToolProcessor::class) 54 | ->parent('llm_chain.tool.chain_processor.abstract') 55 | ->tag('llm_chain.chain.input_processor') 56 | ->tag('llm_chain.chain.output_processor') 57 | ->args([ 58 | '$toolbox' => service(ToolboxInterface::class), 59 | '$eventDispatcher' => service('event_dispatcher')->nullOnInvalid(), 60 | ]) 61 | 62 | // profiler 63 | ->set(DataCollector::class) 64 | ->tag('data_collector') 65 | ->set(TraceableToolbox::class) 66 | ->decorate(ToolboxInterface::class) 67 | ->tag('llm_chain.traceable_toolbox') 68 | ; 69 | }; 70 | -------------------------------------------------------------------------------- /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 }} 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/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 | --------------------------------------------------------------------------------