mirror of
https://github.com/symfony/ai-bundle.git
synced 2026-03-23 23:12:08 +01:00
refactor: restructure
This commit is contained in:
72
.github/workflows/pipeline.yml
vendored
Normal file
72
.github/workflows/pipeline.yml
vendored
Normal 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
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
vendor
|
||||
composer.lock
|
||||
.php-cs-fixer.cache
|
||||
.phpunit.cache
|
||||
coverage
|
||||
12
.php-cs-fixer.dist.php
Normal file
12
.php-cs-fixer.dist.php
Normal 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
19
LICENSE
Normal 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
37
Makefile
Normal 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
176
README.md
Normal 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:
|
||||
|
||||

|
||||
43
composer.json
Normal file
43
composer.json
Normal 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
7
phpstan.dist.neon
Normal file
@@ -0,0 +1,7 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src/
|
||||
excludePaths:
|
||||
analyse:
|
||||
- src/DependencyInjection/Configuration.php
|
||||
24
phpunit.xml
Normal file
24
phpunit.xml
Normal 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
BIN
profiler.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
30
rector.php
Normal file
30
rector.php
Normal 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);
|
||||
205
src/DependencyInjection/Configuration.php
Normal file
205
src/DependencyInjection/Configuration.php
Normal 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;
|
||||
}
|
||||
}
|
||||
449
src/DependencyInjection/LlmChainExtension.php
Normal file
449
src/DependencyInjection/LlmChainExtension.php
Normal 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
11
src/LlmChainBundle.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpLlm\LlmChainBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
final class LlmChainBundle extends Bundle
|
||||
{
|
||||
}
|
||||
82
src/Profiler/DataCollector.php
Normal file
82
src/Profiler/DataCollector.php
Normal 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'] ?? [];
|
||||
}
|
||||
}
|
||||
49
src/Profiler/TraceablePlatform.php
Normal file
49
src/Profiler/TraceablePlatform.php
Normal 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;
|
||||
}
|
||||
}
|
||||
44
src/Profiler/TraceableToolbox.php
Normal file
44
src/Profiler/TraceableToolbox.php
Normal 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;
|
||||
}
|
||||
}
|
||||
69
src/Resources/config/services.php
Normal file
69
src/Resources/config/services.php
Normal 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')
|
||||
;
|
||||
};
|
||||
252
src/Resources/views/data_collector.html.twig
Normal file
252
src/Resources/views/data_collector.html.twig
Normal 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 %}
|
||||
16
src/Resources/views/icon.svg
Normal file
16
src/Resources/views/icon.svg
Normal 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 |
71
tests/Profiler/TraceableToolboxTest.php
Normal file
71
tests/Profiler/TraceableToolboxTest.php
Normal 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';
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user