refactor: restructure

This commit is contained in:
Christopher Hertel
2025-06-13 14:10:57 +02:00
commit d2d30f9fa4
21 changed files with 1673 additions and 0 deletions

72
.github/workflows/pipeline.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: pipeline
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
dependencies: ['lowest', 'highest']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install Composer
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "${{ matrix.dependencies }}"
- name: Composer Validation
run: composer validate --strict
- name: Install PHP Dependencies
run: composer install --no-scripts
- name: Tests
run: vendor/bin/phpunit
qa:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Conventional Commit
uses: ytanikin/pr-conventional-commits@1.4.1
with:
task_types: '["feat", "fix", "docs", "test", "ci", "style", "refactor", "perf", "chore", "revert"]'
add_label: 'true'
custom_labels: '{"feat": "feature", "fix": "bug", "docs": "documentation", "test": "test", "ci": "CI/CD", "style": "codestyle", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert"}'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Composer
uses: "ramsey/composer-install@v3"
- name: Composer Validation
run: composer validate --strict
- name: Install PHP Dependencies
run: composer install --no-scripts
- name: Code Style PHP
run: vendor/bin/php-cs-fixer fix --dry-run
- name: Rector
run: vendor/bin/rector --dry-run
- name: PHPStan
run: vendor/bin/phpstan analyse

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
vendor
composer.lock
.php-cs-fixer.cache
.phpunit.cache
coverage

12
.php-cs-fixer.dist.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
;
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
->setFinder($finder)
;

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2024 Christopher Hertel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
.PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest
deps-stable:
composer update --prefer-stable --ignore-platform-req=ext-mongodb
deps-low:
composer update --prefer-lowest --ignore-platform-req=ext-mongodb
deps-dev:
composer require php-llm/llm-chain:dev-main
cs:
PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix --diff --verbose
rector:
vendor/bin/rector
phpstan:
vendor/bin/phpstan --memory-limit=-1
tests:
vendor/bin/phpunit
coverage:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage
run-examples:
./example
ci: ci-stable
ci-stable: deps-stable rector cs phpstan tests
ci-lowest: deps-low rector cs phpstan tests
ci-dev: deps-dev rector cs phpstan tests
git restore composer.json

176
README.md Normal file
View File

@@ -0,0 +1,176 @@
# LLM Chain Bundle
Symfony integration bundle for [php-llm/llm-chain](https://github.com/php-llm/llm-chain) library.
## Installation
```bash
composer require php-llm/llm-chain-bundle
```
## Configuration
### Simple Example with OpenAI
```yaml
# config/packages/llm_chain.yaml
llm_chain:
platform:
openai:
api_key: '%env(OPENAI_API_KEY)%'
chain:
default:
model:
name: 'GPT'
```
### Advanced Example with Anthropic, Azure, Google and multiple chains
```yaml
# config/packages/llm_chain.yaml
llm_chain:
platform:
anthropic:
api_key: '%env(ANTHROPIC_API_KEY)%'
azure:
# multiple deployments possible
gpt_deployment:
base_url: '%env(AZURE_OPENAI_BASEURL)%'
deployment: '%env(AZURE_OPENAI_GPT)%'
api_key: '%env(AZURE_OPENAI_KEY)%'
api_version: '%env(AZURE_GPT_VERSION)%'
google:
api_key: '%env(GOOGLE_API_KEY)%'
chain:
rag:
platform: 'llm_chain.platform.azure.gpt_deployment'
structured_output: false # Disables support for "output_structure" option, default is true
model:
name: 'GPT'
version: 'gpt-4o-mini'
system_prompt: 'You are a helpful assistant that can answer questions.' # The default system prompt of the chain
include_tools: true # Include tool definitions at the end of the system prompt
tools:
# Referencing a service with #[AsTool] attribute
- 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch'
# Referencing a service without #[AsTool] attribute
- service: 'App\Chain\Tool\CompanyName'
name: 'company_name'
description: 'Provides the name of your company'
method: 'foo' # Optional with default value '__invoke'
# Referencing a chain => chain in chain 🤯
- service: 'llm_chain.chain.research'
name: 'wikipedia_research'
description: 'Can research on Wikipedia'
is_chain: true
research:
platform: 'llm_chain.platform.anthropic'
model:
name: 'Claude'
tools: # If undefined, all tools are injected into the chain, use "tools: false" to disable tools.
- 'PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia'
fault_tolerant_toolbox: false # Disables fault tolerant toolbox, default is true
store:
# also azure_search, mongodb and pinecone are supported as store type
chroma_db:
# multiple collections possible per type
default:
collection: 'my_collection'
embedder:
default:
# platform: 'llm_chain.platform.anthropic'
# store: 'llm_chain.store.chroma_db.default'
model:
name: 'Embeddings'
version: 'text-embedding-ada-002'
```
## Usage
### Chain Service
Use the `Chain` service to leverage GPT:
```php
use PhpLlm\LlmChain\ChainInterface;
use PhpLlm\LlmChain\Model\Message\Message;
use PhpLlm\LlmChain\Model\Message\MessageBag;
final readonly class MyService
{
public function __construct(
private ChainInterface $chain,
) {
}
public function submit(string $message): string
{
$messages = new MessageBag(
Message::forSystem('Speak like a pirate.'),
Message::ofUser($message),
);
return $this->chain->call($messages);
}
}
```
### Register Tools
To use existing tools, you can register them as a service:
```yaml
services:
_defaults:
autowire: true
autoconfigure: true
PhpLlm\LlmChain\Chain\Toolbox\Tool\Clock: ~
PhpLlm\LlmChain\Chain\Toolbox\Tool\OpenMeteo: ~
PhpLlm\LlmChain\Chain\Toolbox\Tool\SerpApi:
$apiKey: '%env(SERP_API_KEY)%'
PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch: ~
PhpLlm\LlmChain\Chain\Toolbox\Tool\Tavily:
$apiKey: '%env(TAVILY_API_KEY)%'
PhpLlm\LlmChain\Chain\Toolbox\Tool\Wikipedia: ~
PhpLlm\LlmChain\Chain\Toolbox\Tool\YouTubeTranscriber: ~
```
Custom tools can be registered by using the `#[AsTool]` attribute:
```php
use PhpLlm\LlmChain\Chain\Toolbox\Attribute\AsTool;
#[AsTool('company_name', 'Provides the name of your company')]
final class CompanyName
{
public function __invoke(): string
{
return 'ACME Corp.'
}
}
```
The chain configuration by default will inject all known tools into the chain.
To disable this behavior, set the `tools` option to `false`:
```yaml
llm_chain:
chain:
my_chain:
tools: false
```
To inject only specific tools, list them in the configuration:
```yaml
llm_chain:
chain:
my_chain:
tools:
- 'PhpLlm\LlmChain\Chain\Toolbox\Tool\SimilaritySearch'
```
### Profiler
The profiler panel provides insights into the chain's execution:
![Profiler](./profiler.png)

43
composer.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "php-llm/llm-chain-bundle",
"type": "symfony-bundle",
"description": "Symfony integration bundle for php-llm/llm-chain",
"license": "MIT",
"authors": [
{
"name": "Christopher Hertel",
"email": "mail@christopher-hertel.de"
},
{
"name": "Oskar Stark",
"email": "oskarstark@googlemail.com"
}
],
"require": {
"php": ">=8.2",
"php-llm/llm-chain": "^0.22",
"symfony/config": "^6.4 || ^7.0",
"symfony/dependency-injection": "^6.4 || ^7.0",
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/string": "^6.4 || ^7.0"
},
"require-dev": {
"php-cs-fixer/shim": "^3.69",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^11.5",
"rector/rector": "^2.0"
},
"config": {
"sort-packages": true
},
"autoload": {
"psr-4": {
"PhpLlm\\LlmChainBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PhpLlm\\LlmChainBundle\\Tests\\": "tests/"
}
}
}

7
phpstan.dist.neon Normal file
View File

@@ -0,0 +1,7 @@
parameters:
level: 6
paths:
- src/
excludePaths:
analyse:
- src/DependencyInjection/Configuration.php

24
phpunit.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
colors="true"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

BIN
profiler.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

30
rector.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitSelfCallRector;
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
return RectorConfig::configure()
->withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
->withPhpSets(php82: true)
->withSets([
PHPUnitSetList::PHPUNIT_110,
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
])
->withRules([
PreferPHPUnitSelfCallRector::class,
])
->withImportNames(importShortClasses: false)
->withSkip([
ClosureToArrowFunctionRector::class,
PreferPHPUnitThisCallRector::class,
])
->withTypeCoverageLevel(0);

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\DependencyInjection;
use PhpLlm\LlmChain\Platform\PlatformInterface;
use PhpLlm\LlmChain\Store\StoreInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('llm_chain');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
->arrayNode('platform')
->children()
->arrayNode('anthropic')
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('version')->defaultNull()->end()
->end()
->end()
->arrayNode('azure')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('base_url')->isRequired()->end()
->scalarNode('deployment')->isRequired()->end()
->scalarNode('api_version')->info('The used API version')->end()
->end()
->end()
->end()
->arrayNode('google')
->children()
->scalarNode('api_key')->isRequired()->end()
->end()
->end()
->arrayNode('openai')
->children()
->scalarNode('api_key')->isRequired()->end()
->end()
->end()
->end()
->end()
->arrayNode('chain')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('platform')
->info('Service name of platform')
->defaultValue(PlatformInterface::class)
->end()
->arrayNode('model')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->defaultNull()->end()
->arrayNode('options')
->scalarPrototype()->end()
->end()
->end()
->end()
->booleanNode('structured_output')->defaultTrue()->end()
->scalarNode('system_prompt')
->validate()
->ifTrue(fn ($v) => null !== $v && '' === trim($v))
->thenInvalid('The default system prompt must not be an empty string')
->end()
->defaultNull()
->info('The default system prompt of the chain')
->end()
->booleanNode('include_tools')
->info('Include tool definitions at the end of the system prompt')
->defaultFalse()
->end()
->arrayNode('tools')
->addDefaultsIfNotSet()
->treatFalseLike(['enabled' => false])
->treatTrueLike(['enabled' => true])
->treatNullLike(['enabled' => true])
->beforeNormalization()
->ifArray()
->then(function (array $v) {
return [
'enabled' => $v['enabled'] ?? true,
'services' => $v['services'] ?? $v,
];
})
->end()
->children()
->booleanNode('enabled')->defaultTrue()->end()
->arrayNode('services')
->arrayPrototype()
->children()
->scalarNode('service')->isRequired()->end()
->scalarNode('name')->end()
->scalarNode('description')->end()
->scalarNode('method')->end()
->booleanNode('is_chain')->defaultFalse()->end()
->end()
->beforeNormalization()
->ifString()
->then(function (string $v) {
return ['service' => $v];
})
->end()
->end()
->end()
->end()
->end()
->booleanNode('fault_tolerant_toolbox')->defaultTrue()->end()
->end()
->end()
->end()
->arrayNode('store')
->children()
->arrayNode('azure_search')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('endpoint')->isRequired()->end()
->scalarNode('api_key')->isRequired()->end()
->scalarNode('index_name')->isRequired()->end()
->scalarNode('api_version')->isRequired()->end()
->scalarNode('vector_field')->end()
->end()
->end()
->end()
->arrayNode('chroma_db')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('collection')->isRequired()->end()
->end()
->end()
->end()
->arrayNode('mongodb')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('database')->isRequired()->end()
->scalarNode('collection')->isRequired()->end()
->scalarNode('index_name')->isRequired()->end()
->scalarNode('vector_field')->end()
->booleanNode('bulk_write')->end()
->end()
->end()
->end()
->arrayNode('pinecone')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('namespace')->end()
->arrayNode('filter')
->scalarPrototype()->end()
->end()
->integerNode('top_k')->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('embedder')
->normalizeKeys(false)
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('store')
->info('Service name of store')
->defaultValue(StoreInterface::class)
->end()
->scalarNode('platform')
->info('Service name of platform')
->defaultValue(PlatformInterface::class)
->end()
->arrayNode('model')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->defaultNull()->end()
->arrayNode('options')
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,449 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\DependencyInjection;
use PhpLlm\LlmChain\Chain\Chain;
use PhpLlm\LlmChain\Chain\ChainInterface;
use PhpLlm\LlmChain\Chain\InputProcessor\SystemPromptInputProcessor;
use PhpLlm\LlmChain\Chain\InputProcessorInterface;
use PhpLlm\LlmChain\Chain\OutputProcessorInterface;
use PhpLlm\LlmChain\Chain\StructuredOutput\ChainProcessor as StructureOutputProcessor;
use PhpLlm\LlmChain\Chain\Toolbox\Attribute\AsTool;
use PhpLlm\LlmChain\Chain\Toolbox\ChainProcessor as ToolProcessor;
use PhpLlm\LlmChain\Chain\Toolbox\FaultTolerantToolbox;
use PhpLlm\LlmChain\Chain\Toolbox\Tool\Chain as ChainTool;
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactory\ChainFactory;
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactory\MemoryToolFactory;
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactory\ReflectionToolFactory;
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\Claude;
use PhpLlm\LlmChain\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
use PhpLlm\LlmChain\Platform\Bridge\Azure\OpenAI\PlatformFactory as AzureOpenAIPlatformFactory;
use PhpLlm\LlmChain\Platform\Bridge\Google\Gemini;
use PhpLlm\LlmChain\Platform\Bridge\Google\PlatformFactory as GooglePlatformFactory;
use PhpLlm\LlmChain\Platform\Bridge\Meta\Llama;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\Embeddings;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\GPT;
use PhpLlm\LlmChain\Platform\Bridge\OpenAI\PlatformFactory as OpenAIPlatformFactory;
use PhpLlm\LlmChain\Platform\Bridge\Voyage\Voyage;
use PhpLlm\LlmChain\Platform\ModelClientInterface;
use PhpLlm\LlmChain\Platform\Platform;
use PhpLlm\LlmChain\Platform\PlatformInterface;
use PhpLlm\LlmChain\Platform\ResponseConverterInterface;
use PhpLlm\LlmChain\Store\Bridge\Azure\SearchStore as AzureSearchStore;
use PhpLlm\LlmChain\Store\Bridge\ChromaDB\Store as ChromaDBStore;
use PhpLlm\LlmChain\Store\Bridge\MongoDB\Store as MongoDBStore;
use PhpLlm\LlmChain\Store\Bridge\Pinecone\Store as PineconeStore;
use PhpLlm\LlmChain\Store\Embedder;
use PhpLlm\LlmChain\Store\StoreInterface;
use PhpLlm\LlmChain\Store\VectorStoreInterface;
use PhpLlm\LlmChainBundle\Profiler\DataCollector;
use PhpLlm\LlmChainBundle\Profiler\TraceablePlatform;
use PhpLlm\LlmChainBundle\Profiler\TraceableToolbox;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use function Symfony\Component\String\u;
final class LlmChainExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
$loader->load('services.php');
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
foreach ($config['platform'] ?? [] as $type => $platform) {
$this->processPlatformConfig($type, $platform, $container);
}
$platforms = array_keys($container->findTaggedServiceIds('llm_chain.platform'));
if (1 === count($platforms)) {
$container->setAlias(PlatformInterface::class, reset($platforms));
}
if ($container->getParameter('kernel.debug')) {
foreach ($platforms as $platform) {
$traceablePlatformDefinition = (new Definition(TraceablePlatform::class))
->setDecoratedService($platform)
->setAutowired(true)
->addTag('llm_chain.traceable_platform');
$suffix = u($platform)->afterLast('.')->toString();
$container->setDefinition('llm_chain.traceable_platform.'.$suffix, $traceablePlatformDefinition);
}
}
foreach ($config['chain'] as $chainName => $chain) {
$this->processChainConfig($chainName, $chain, $container);
}
if (1 === count($config['chain']) && isset($chainName)) {
$container->setAlias(ChainInterface::class, 'llm_chain.chain.'.$chainName);
}
foreach ($config['store'] ?? [] as $type => $store) {
$this->processStoreConfig($type, $store, $container);
}
$stores = array_keys($container->findTaggedServiceIds('llm_chain.store'));
if (1 === count($stores)) {
$container->setAlias(VectorStoreInterface::class, reset($stores));
$container->setAlias(StoreInterface::class, reset($stores));
}
foreach ($config['embedder'] as $embedderName => $embedder) {
$this->processEmbedderConfig($embedderName, $embedder, $container);
}
if (1 === count($config['embedder']) && isset($embedderName)) {
$container->setAlias(Embedder::class, 'llm_chain.embedder.'.$embedderName);
}
$container->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void {
$definition->addTag('llm_chain.tool', [
'name' => $attribute->name,
'description' => $attribute->description,
'method' => $attribute->method,
]);
});
$container->registerForAutoconfiguration(InputProcessorInterface::class)
->addTag('llm_chain.chain.input_processor');
$container->registerForAutoconfiguration(OutputProcessorInterface::class)
->addTag('llm_chain.chain.output_processor');
$container->registerForAutoconfiguration(ModelClientInterface::class)
->addTag('llm_chain.platform.model_client');
$container->registerForAutoconfiguration(ResponseConverterInterface::class)
->addTag('llm_chain.platform.response_converter');
if (false === $container->getParameter('kernel.debug')) {
$container->removeDefinition(DataCollector::class);
$container->removeDefinition(TraceableToolbox::class);
}
}
/**
* @param array<string, mixed> $platform
*/
private function processPlatformConfig(string $type, array $platform, ContainerBuilder $container): void
{
if ('anthropic' === $type) {
$platformId = 'llm_chain.platform.anthropic';
$definition = (new Definition(Platform::class))
->setFactory(AnthropicPlatformFactory::class.'::create')
->setAutowired(true)
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments([
'$apiKey' => $platform['api_key'],
])
->addTag('llm_chain.platform');
if (isset($platform['version'])) {
$definition->replaceArgument('$version', $platform['version']);
}
$container->setDefinition($platformId, $definition);
return;
}
if ('azure' === $type) {
foreach ($platform as $name => $config) {
$platformId = 'llm_chain.platform.azure.'.$name;
$definition = (new Definition(Platform::class))
->setFactory(AzureOpenAIPlatformFactory::class.'::create')
->setAutowired(true)
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments([
'$baseUrl' => $config['base_url'],
'$deployment' => $config['deployment'],
'$apiVersion' => $config['api_version'],
'$apiKey' => $config['api_key'],
])
->addTag('llm_chain.platform');
$container->setDefinition($platformId, $definition);
}
return;
}
if ('google' === $type) {
$platformId = 'llm_chain.platform.google';
$definition = (new Definition(Platform::class))
->setFactory(GooglePlatformFactory::class.'::create')
->setAutowired(true)
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');
$container->setDefinition($platformId, $definition);
return;
}
if ('openai' === $type) {
$platformId = 'llm_chain.platform.openai';
$definition = (new Definition(Platform::class))
->setFactory(OpenAIPlatformFactory::class.'::create')
->setAutowired(true)
->setLazy(true)
->addTag('proxy', ['interface' => PlatformInterface::class])
->setArguments(['$apiKey' => $platform['api_key']])
->addTag('llm_chain.platform');
$container->setDefinition($platformId, $definition);
return;
}
throw new \InvalidArgumentException(sprintf('Platform "%s" is not supported for configuration via bundle at this point.', $type));
}
/**
* @param array<string, mixed> $config
*/
private function processChainConfig(string $name, array $config, ContainerBuilder $container): void
{
// MODEL
['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model'];
$modelClass = match (strtolower((string) $modelName)) {
'gpt' => GPT::class,
'claude' => Claude::class,
'llama' => Llama::class,
'gemini' => Gemini::class,
default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)),
};
$modelDefinition = new Definition($modelClass);
if (null !== $version) {
$modelDefinition->setArgument('$name', $version);
}
if (0 !== count($options)) {
$modelDefinition->setArgument('$options', $options);
}
$modelDefinition->addTag('llm_chain.model.language_model');
$container->setDefinition('llm_chain.chain.'.$name.'.model', $modelDefinition);
// CHAIN
$chainDefinition = (new Definition(Chain::class))
->setAutowired(true)
->setArgument('$platform', new Reference($config['platform']))
->setArgument('$model', new Reference('llm_chain.chain.'.$name.'.model'));
$inputProcessors = [];
$outputProcessors = [];
// TOOL & PROCESSOR
if ($config['tools']['enabled']) {
// Create specific toolbox and process if tools are explicitly defined
if (0 !== count($config['tools']['services'])) {
$memoryFactoryDefinition = new Definition(MemoryToolFactory::class);
$container->setDefinition('llm_chain.toolbox.'.$name.'.memory_factory', $memoryFactoryDefinition);
$chainFactoryDefinition = new Definition(ChainFactory::class, [
'$factories' => [new Reference('llm_chain.toolbox.'.$name.'.memory_factory'), new Reference(ReflectionToolFactory::class)],
]);
$container->setDefinition('llm_chain.toolbox.'.$name.'.chain_factory', $chainFactoryDefinition);
$tools = [];
foreach ($config['tools']['services'] as $tool) {
$reference = new Reference($tool['service']);
// We use the memory factory in case method, description and name are set
if (isset($tool['name'], $tool['description'])) {
if ($tool['is_chain']) {
$chainWrapperDefinition = new Definition(ChainTool::class, ['$chain' => $reference]);
$container->setDefinition('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name'], $chainWrapperDefinition);
$reference = new Reference('llm_chain.toolbox.'.$name.'.chain_wrapper.'.$tool['name']);
}
$memoryFactoryDefinition->addMethodCall('addTool', [$reference, $tool['name'], $tool['description'], $tool['method'] ?? '__invoke']);
}
$tools[] = $reference;
}
$toolboxDefinition = (new ChildDefinition('llm_chain.toolbox.abstract'))
->replaceArgument('$toolFactory', new Reference('llm_chain.toolbox.'.$name.'.chain_factory'))
->replaceArgument('$tools', $tools);
$container->setDefinition('llm_chain.toolbox.'.$name, $toolboxDefinition);
if ($config['fault_tolerant_toolbox']) {
$faultTolerantToolboxDefinition = (new Definition('llm_chain.fault_tolerant_toolbox.'.$name))
->setClass(FaultTolerantToolbox::class)
->setAutowired(true)
->setDecoratedService('llm_chain.toolbox.'.$name);
$container->setDefinition('llm_chain.fault_tolerant_toolbox.'.$name, $faultTolerantToolboxDefinition);
}
if ($container->getParameter('kernel.debug')) {
$traceableToolboxDefinition = (new Definition('llm_chain.traceable_toolbox.'.$name))
->setClass(TraceableToolbox::class)
->setAutowired(true)
->setDecoratedService('llm_chain.toolbox.'.$name)
->addTag('llm_chain.traceable_toolbox');
$container->setDefinition('llm_chain.traceable_toolbox.'.$name, $traceableToolboxDefinition);
}
$toolProcessorDefinition = (new ChildDefinition('llm_chain.tool.chain_processor.abstract'))
->replaceArgument('$toolbox', new Reference('llm_chain.toolbox.'.$name));
$container->setDefinition('llm_chain.tool.chain_processor.'.$name, $toolProcessorDefinition);
$inputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name);
$outputProcessors[] = new Reference('llm_chain.tool.chain_processor.'.$name);
} else {
$inputProcessors[] = new Reference(ToolProcessor::class);
$outputProcessors[] = new Reference(ToolProcessor::class);
}
}
// STRUCTURED OUTPUT
if ($config['structured_output']) {
$inputProcessors[] = new Reference(StructureOutputProcessor::class);
$outputProcessors[] = new Reference(StructureOutputProcessor::class);
}
// SYSTEM PROMPT
if (is_string($config['system_prompt'])) {
$systemPromptInputProcessorDefinition = new Definition(SystemPromptInputProcessor::class);
$systemPromptInputProcessorDefinition
->setAutowired(true)
->setArguments([
'$systemPrompt' => $config['system_prompt'],
'$toolbox' => $config['include_tools'] ? new Reference('llm_chain.toolbox.'.$name) : null,
]);
$inputProcessors[] = $systemPromptInputProcessorDefinition;
}
$chainDefinition
->setArgument('$inputProcessors', $inputProcessors)
->setArgument('$outputProcessors', $outputProcessors);
$container->setDefinition('llm_chain.chain.'.$name, $chainDefinition);
}
/**
* @param array<string, mixed> $stores
*/
private function processStoreConfig(string $type, array $stores, ContainerBuilder $container): void
{
if ('azure_search' === $type) {
foreach ($stores as $name => $store) {
$arguments = [
'$endpointUrl' => $store['endpoint'],
'$apiKey' => $store['api_key'],
'$indexName' => $store['index_name'],
'$apiVersion' => $store['api_version'],
];
if (array_key_exists('vector_field', $store)) {
$arguments['$vectorFieldName'] = $store['vector_field'];
}
$definition = new Definition(AzureSearchStore::class);
$definition
->setAutowired(true)
->addTag('llm_chain.store')
->setArguments($arguments);
$container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition);
}
}
if ('chroma_db' === $type) {
foreach ($stores as $name => $store) {
$definition = new Definition(ChromaDBStore::class);
$definition
->setAutowired(true)
->setArgument('$collectionName', $store['collection'])
->addTag('llm_chain.store');
$container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition);
}
}
if ('mongodb' === $type) {
foreach ($stores as $name => $store) {
$arguments = [
'$databaseName' => $store['database'],
'$collectionName' => $store['collection'],
'$indexName' => $store['index_name'],
];
if (array_key_exists('vector_field', $store)) {
$arguments['$vectorFieldName'] = $store['vector_field'];
}
if (array_key_exists('bulk_write', $store)) {
$arguments['$bulkWrite'] = $store['bulk_write'];
}
$definition = new Definition(MongoDBStore::class);
$definition
->setAutowired(true)
->addTag('llm_chain.store')
->setArguments($arguments);
$container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition);
}
}
if ('pinecone' === $type) {
foreach ($stores as $name => $store) {
$arguments = [
'$namespace' => $store['namespace'],
];
if (array_key_exists('filter', $store)) {
$arguments['$filter'] = $store['filter'];
}
if (array_key_exists('top_k', $store)) {
$arguments['$topK'] = $store['top_k'];
}
$definition = new Definition(PineconeStore::class);
$definition
->setAutowired(true)
->addTag('llm_chain.store')
->setArguments($arguments);
$container->setDefinition('llm_chain.store.'.$type.'.'.$name, $definition);
}
}
}
/**
* @param array<string, mixed> $config
*/
private function processEmbedderConfig(int|string $name, array $config, ContainerBuilder $container): void
{
['name' => $modelName, 'version' => $version, 'options' => $options] = $config['model'];
$modelClass = match (strtolower((string) $modelName)) {
'embeddings' => Embeddings::class,
'voyage' => Voyage::class,
default => throw new \InvalidArgumentException(sprintf('Model "%s" is not supported.', $modelName)),
};
$modelDefinition = (new Definition($modelClass));
if (null !== $version) {
$modelDefinition->setArgument('$name', $version);
}
if (0 !== count($options)) {
$modelDefinition->setArgument('$options', $options);
}
$modelDefinition->addTag('llm_chain.model.embeddings_model');
$container->setDefinition('llm_chain.embedder.'.$name.'.model', $modelDefinition);
$definition = new Definition(Embedder::class, [
'$model' => new Reference('llm_chain.embedder.'.$name.'.model'),
'$platform' => new Reference($config['platform']),
'$store' => new Reference($config['store']),
]);
$container->setDefinition('llm_chain.embedder.'.$name, $definition);
}
}

11
src/LlmChainBundle.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class LlmChainBundle extends Bundle
{
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\Profiler;
use PhpLlm\LlmChain\Chain\Toolbox\ToolboxInterface;
use PhpLlm\LlmChain\Platform\Tool\Tool;
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @phpstan-import-type PlatformCallData from TraceablePlatform
* @phpstan-import-type ToolCallData from TraceableToolbox
*/
final class DataCollector extends AbstractDataCollector
{
/**
* @var TraceablePlatform[]
*/
private readonly array $platforms;
/**
* @var TraceableToolbox[]
*/
private readonly array $toolboxes;
/**
* @param TraceablePlatform[] $platforms
* @param TraceableToolbox[] $toolboxes
*/
public function __construct(
#[TaggedIterator('llm_chain.traceable_platform')]
iterable $platforms,
private readonly ToolboxInterface $defaultToolBox,
#[TaggedIterator('llm_chain.traceable_toolbox')]
iterable $toolboxes,
) {
$this->platforms = $platforms instanceof \Traversable ? iterator_to_array($platforms) : $platforms;
$this->toolboxes = $toolboxes instanceof \Traversable ? iterator_to_array($toolboxes) : $toolboxes;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data = [
'tools' => $this->defaultToolBox->getTools(),
'platform_calls' => array_merge(...array_map(fn (TraceablePlatform $platform) => $platform->calls, $this->platforms)),
'tool_calls' => array_merge(...array_map(fn (TraceableToolbox $toolbox) => $toolbox->calls, $this->toolboxes)),
];
}
public static function getTemplate(): string
{
return '@LlmChain/data_collector.html.twig';
}
/**
* @return PlatformCallData[]
*/
public function getPlatformCalls(): array
{
return $this->data['platform_calls'] ?? [];
}
/**
* @return Tool[]
*/
public function getTools(): array
{
return $this->data['tools'] ?? [];
}
/**
* @return ToolCallData[]
*/
public function getToolCalls(): array
{
return $this->data['tool_calls'] ?? [];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\Profiler;
use PhpLlm\LlmChain\Platform\Message\Content\File;
use PhpLlm\LlmChain\Platform\Model;
use PhpLlm\LlmChain\Platform\PlatformInterface;
use PhpLlm\LlmChain\Platform\Response\ResponseInterface;
/**
* @phpstan-type PlatformCallData array{
* model: Model,
* input: array<mixed>|string|object,
* options: array<string, mixed>,
* response: ResponseInterface,
* }
*/
final class TraceablePlatform implements PlatformInterface
{
/**
* @var PlatformCallData[]
*/
public array $calls = [];
public function __construct(
private readonly PlatformInterface $platform,
) {
}
public function request(Model $model, array|string|object $input, array $options = []): ResponseInterface
{
$response = $this->platform->request($model, $input, $options);
if ($input instanceof File) {
$input = $input::class.': '.$input->getFormat();
}
$this->calls[] = [
'model' => $model,
'input' => is_object($input) ? clone $input : $input,
'options' => $options,
'response' => $response->getContent(),
];
return $response;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\Profiler;
use PhpLlm\LlmChain\Chain\Toolbox\ToolboxInterface;
use PhpLlm\LlmChain\Platform\Response\ToolCall;
/**
* @phpstan-type ToolCallData array{
* call: ToolCall,
* result: string,
* }
*/
final class TraceableToolbox implements ToolboxInterface
{
/**
* @var ToolCallData[]
*/
public array $calls = [];
public function __construct(
private readonly ToolboxInterface $toolbox,
) {
}
public function getTools(): array
{
return $this->toolbox->getTools();
}
public function execute(ToolCall $toolCall): mixed
{
$result = $this->toolbox->execute($toolCall);
$this->calls[] = [
'call' => $toolCall,
'result' => $result,
];
return $result;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use PhpLlm\LlmChain\Chain\StructuredOutput\ChainProcessor as StructureOutputProcessor;
use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactory;
use PhpLlm\LlmChain\Chain\StructuredOutput\ResponseFormatFactoryInterface;
use PhpLlm\LlmChain\Chain\Toolbox\ChainProcessor as ToolProcessor;
use PhpLlm\LlmChain\Chain\Toolbox\Toolbox;
use PhpLlm\LlmChain\Chain\Toolbox\ToolboxInterface;
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactory\ReflectionToolFactory;
use PhpLlm\LlmChain\Chain\Toolbox\ToolFactoryInterface;
use PhpLlm\LlmChainBundle\Profiler\DataCollector;
use PhpLlm\LlmChainBundle\Profiler\TraceableToolbox;
return static function (ContainerConfigurator $container): void {
$container->services()
->defaults()
->autowire()
// structured output
->set(ResponseFormatFactory::class)
->alias(ResponseFormatFactoryInterface::class, ResponseFormatFactory::class)
->set(StructureOutputProcessor::class)
->tag('llm_chain.chain.input_processor')
->tag('llm_chain.chain.output_processor')
// tools
->set('llm_chain.toolbox.abstract')
->class(Toolbox::class)
->autowire()
->abstract()
->args([
'$toolFactory' => service(ToolFactoryInterface::class),
'$tools' => abstract_arg('Collection of tools'),
])
->set(Toolbox::class)
->parent('llm_chain.toolbox.abstract')
->args([
'$tools' => tagged_iterator('llm_chain.tool'),
])
->alias(ToolboxInterface::class, Toolbox::class)
->set(ReflectionToolFactory::class)
->alias(ToolFactoryInterface::class, ReflectionToolFactory::class)
->set('llm_chain.tool.chain_processor.abstract')
->class(ToolProcessor::class)
->abstract()
->args([
'$toolbox' => abstract_arg('Toolbox'),
])
->set(ToolProcessor::class)
->parent('llm_chain.tool.chain_processor.abstract')
->tag('llm_chain.chain.input_processor')
->tag('llm_chain.chain.output_processor')
->args([
'$toolbox' => service(ToolboxInterface::class),
'$eventDispatcher' => service('event_dispatcher')->nullOnInvalid(),
])
// profiler
->set(DataCollector::class)
->tag('data_collector')
->set(TraceableToolbox::class)
->decorate(ToolboxInterface::class)
->tag('llm_chain.traceable_toolbox')
;
};

View File

@@ -0,0 +1,252 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.platformCalls|length > 0 %}
{% set icon %}
{{ include('@LlmChain/icon.svg', { y: 18 }) }}
<span class="sf-toolbar-value">{{ collector.platformCalls|length }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">calls</span>
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<div class="sf-toolbar-info-piece">
<b class="label">Configured Platforms</b>
<span class="sf-toolbar-status">1</span>
</div>
<div class="sf-toolbar-info-piece">
<b class="label">Platform Calls</b>
<span class="sf-toolbar-status">{{ collector.platformCalls|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b class="label">Registered Tools</b>
<span class="sf-toolbar-status">{{ collector.tools|length }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b class="label">Tool Calls</b>
<span class="sf-toolbar-status">{{ collector.toolCalls|length }}</span>
</div>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon">{{ include('@LlmChain/icon.svg', { y: 16 }) }}</span>
<strong>LLM Chain</strong>
<span class="count">{{ collector.platformCalls|length }}</span>
</span>
{% endblock %}
{% macro tool_calls(toolCalls) %}
Tool call{{ toolCalls|length > 1 ? 's' }}:
<ol>
{% for toolCall in toolCalls %}
<li>
<strong>{{ toolCall.name }}({{ toolCall.arguments|map((value, key) => "#{key}: #{value}")|join(', ') }})</strong>
<i>(ID: {{ toolCall.id }})</i>
</li>
{% endfor %}
</ol>
{% endmacro %}
{% block panel %}
<h2>LLM Chain</h2>
<section class="metrics">
<div class="metric-group">
<div class="metric">
<span class="value">1</span>
<span class="label">Platforms</span>
</div>
<div class="metric">
<span class="value">{{ collector.platformCalls|length }}</span>
<span class="label">Platform Calls</span>
</div>
</div>
<div class="metric-divider"></div>
<div class="metric-group">
<div class="metric">
<span class="value">{{ collector.tools|length }}</span>
<span class="label">Tools</span>
</div>
<div class="metric">
<span class="value">{{ collector.toolCalls|length }}</span>
<span class="label">Tool Calls</span>
</div>
</div>
</section>
<h3>Platform Calls</h3>
{% if collector.platformCalls|length %}
<div class="sf-tabs">
<div class="tab {{ collector.platformCalls|length == 0 ? 'disabled' }}">
<h3 class="tab-title">Platform Calls <span class="badge">{{ collector.platformCalls|length }}</span></h3>
<div class="tab-content">
{% for call in collector.platformCalls %}
<table class="table">
<thead>
<tr>
<th colspan="2">Call {{ loop.index }}</th>
</tr>
</thead>
<tbody>
<tr>
<th>Model</th>
<td><strong>{{ constant('class', call.model) }}</strong> (Version: {{ call.model.name }})</td>
</tr>
<tr>
<th>Input</th>
<td>
{% if call.input.messages is defined %}{# expect MessageBag #}
<ol>
{% for message in call.input.messages %}
<li>
<strong>{{ message.role.value|title }}:</strong>
{% if 'assistant' == message.role.value and message.hasToolCalls%}
{{ _self.tool_calls(message.toolCalls) }}
{% elseif 'tool' == message.role.value %}
<i>Result of tool call with ID {{ message.toolCall.id }}</i><br />
{{ message.content|nl2br }}
{% elseif 'user' == message.role.value %}
{% for item in message.content %}
{% if item.text is defined %}
{{ item.text|nl2br }}
{% else %}
<img src="{{ item.url }}" />
{% endif %}
{% endfor %}
{% else %}
{{ message.content|nl2br }}
{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
{{ dump(call.input) }}
{% endif %}
</td>
</tr>
<tr>
<th>Options</th>
<td>
<ul>
{% for key, value in call.options %}
{% if key == 'tools' %}
<li>{{ key }}:
<ul>
{% for tool in value %}
<li>{{ tool.name }}</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>{{ key }}: {{ dump(value) }}</li>
{% endif %}
{% endfor %}
</ul>
</td>
</tr>
<tr>
<th>Response</th>
<td>
{% if call.input.messages is defined and call.response is iterable %}{# expect array of ToolCall #}
{{ _self.tool_calls(call.response) }}
{% elseif call.response is iterable %}{# expect array of Vectors #}
<ol>
{% for vector in call.response %}
<li>Vector with <strong>{{ vector.dimensions }}</strong> dimensions</li>
{% endfor %}
</ol>
{% else %}
{{ call.response }}
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endfor %}
</div>
</div>
</div>
{% else %}
<div class="empty">
<p>No platform calls were made.</p>
</div>
{% endif %}
<h3>Tools</h3>
{% if collector.tools|length %}
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Class & Method</th>
<th>Parameters</th>
</tr>
</thead>
<tbody>
{% for tool in collector.tools %}
<tr>
<th>{{ tool.name }}</th>
<td>{{ tool.description }}</td>
<td>{{ tool.reference.class }}::{{ tool.reference.method }}</td>
<td>
{% if tool.parameters %}
<ul>
{% for name, parameter in tool.parameters.properties %}
<li>
<strong>{{ name }} ({{ parameter.type }})</strong><br />
<i>{{ parameter.description }}</i>
</li>
{% endfor %}
</ul>
{% else %}
<i>none</i>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">
<p>No tools were registered.</p>
</div>
{% endif %}
<h3>Tool Calls</h3>
{% if collector.toolCalls|length %}
{% for call in collector.toolCalls %}
<table class="table">
<thead>
<tr>
<th colspan="2">{{ call.call.name }}</th>
</tr>
</thead>
<tbody>
<tr>
<th>ID</th>
<td>{{ call.call.id }}</td>
</tr>
<tr>
<th>Arguments</th>
<td>{{ dump(call.call.arguments) }}</td>
</tr>
<tr>
<th>Response</th>
<td>{{ call.result|nl2br }}</td>
</tr>
</tbody>
</table>
{% endfor %}
{% else %}
<div class="empty">
<p>No tool calls were made.</p>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,16 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<!-- svg-source:excalidraw -->
<defs>
<style>
@font-face {
font-family: Lilita One;
src: url(data:font/woff2;base64,d09GMgABAAAAAAG8AA0AAAAAA7QAAAFuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGwwcGAZgAAQRCAoAKgsEAAE2AiQDBAQgBYMkByAb+wLIrgpsY+FkU2dFDWvuKOXBh9p0iy/i4f/3+7XPu7inv0gmWZN5gghNE15peOavTiljTUOd6ZOmib+zbYiVeKP5Oskyo1HfONwCIGtzlROtFDZJSp4td/omBwHoa/tlEAX2W5ZKFGiUJpa9tL1glG4Oi9Y1w5IM4eOYCVSl4Iq2tjhgx0csQBbP8aiU7EV2jds+KD4b+51BuLa1sSnOH5ye8khFnxAQv7M3KGQgRJ6rBwEvdQlnAuHqRSP149+s6vQFPg5W3t38OpdKRVT8KKVijvRV0C0RTsJbBMIyAwSMym1TRUklXrgnQsslMnUOkEzzdYlGqa6m/5ZpYkln1DkxhRkqod2hs5jB6NDIyNiCCzPLLWanxQ4xs0VU8AgUT77HRkZHN6DGZcTsy/uZxeqz6zRaJ+jZ5RhS4UIditeyJ2M87ZssRozQYsIdnZ8zUDjTObVgAzqg3Q1VYMZsDSuYCTpA92kjOwE5YwAA);
}
@font-face {
font-family: Lilita One;
src: url(data:font/woff2;base64,d09GMgABAAAAAAJkAA4AAAAABMAAAAISAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbDBwYBmAANBEICoEogTELCAABNgIkAwwEIAWDJAcgG9ADyB6JcSzFkhILF/EAvz/ec9/7kEcmwMlOVLI64Ma6ta3PDX5n2xAv6UbzdZJVa4z2/4hYkpQ8rzs9WEIHkWv7Es7igNoCK+TP2zXhlidtYUIBYDq3hCTdm4GEH+FzVwnIAQRBJAgCsharHmXKhKVkxXISIApnwn6pRA+jndh9J8M7Xf0mAhP8k3GTJkzkCkXxySTiU94iQPgdPUWDCBFilIgFlEjRgBQpAkYnCUYlSXJLAEFIEvkgBmmI0AX9UKAEEITChoGpW3eJsnfVnthScTorZ+flZTvD5OzwzrITvt7Z8nb1jK0P/6Z00pRTe0jTtrxet7z0VenHk0XLW3Zuvbmoe9khX0wqKdm/9kjPLVUbT+buzRuudJn9PZTtqFjXfEHWjKXHrJ47traYn5VXsy97tGwmitaE89FjXZMknsw8EhBsn/41dT7K7/UtIzt+D686ZL6MxmRnIr4Xn0KGCATiUwnirwQuCHBJ7BnhhtbxLl0FxEYTQAAp3WSskJKJh/YWBiXWYSTPUoz1Y6YpHFNVKXpvmiKM0hl1TkRiVklVdofOYga66Kizzroa4ULMIovZabGrELOFo0HDkNSd3VVnXXQxQaVxGRH7cH9gsfrsOo3WCbQ2hyZwDQ55SBrzYmrdTjdZjIhCi7DN1vlp2muw+ju0ZCY6VHa3SgkMEUdgDGLC6qDJepEqBpJbugIA);
}
</style>
</defs>
<text x="12" y="{{ y }}" font-family="Lilita One, Segoe UI Emoji" font-size="14px" fill="currentColor" text-anchor="middle">LLM</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace PhpLlm\LlmChainBundle\Tests\Profiler;
use PhpLlm\LlmChain\Chain\Toolbox\ToolboxInterface;
use PhpLlm\LlmChain\Platform\Response\ToolCall;
use PhpLlm\LlmChain\Platform\Tool\ExecutionReference;
use PhpLlm\LlmChain\Platform\Tool\Tool;
use PhpLlm\LlmChainBundle\Profiler\TraceableToolbox;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(TraceableToolbox::class)]
#[Small]
final class TraceableToolboxTest extends TestCase
{
#[Test]
public function getMap(): void
{
$metadata = new Tool(new ExecutionReference('Foo\Bar'), 'bar', 'description', null);
$toolbox = $this->createToolbox(['tool' => $metadata]);
$traceableToolbox = new TraceableToolbox($toolbox);
$map = $traceableToolbox->getTools();
self::assertSame(['tool' => $metadata], $map);
}
#[Test]
public function execute(): void
{
$metadata = new Tool(new ExecutionReference('Foo\Bar'), 'bar', 'description', null);
$toolbox = $this->createToolbox(['tool' => $metadata]);
$traceableToolbox = new TraceableToolbox($toolbox);
$toolCall = new ToolCall('foo', '__invoke');
$result = $traceableToolbox->execute($toolCall);
self::assertSame('tool_result', $result);
self::assertCount(1, $traceableToolbox->calls);
self::assertSame($toolCall, $traceableToolbox->calls[0]['call']);
self::assertSame('tool_result', $traceableToolbox->calls[0]['result']);
}
/**
* @param Tool[] $tools
*/
private function createToolbox(array $tools): ToolboxInterface
{
return new class($tools) implements ToolboxInterface {
public function __construct(
private readonly array $tools,
) {
}
public function getTools(): array
{
return $this->tools;
}
public function execute(ToolCall $toolCall): string
{
return 'tool_result';
}
};
}
}