From e61d72dedef41453b7bec9b4fe99b30338f03387 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Sat, 12 Jul 2025 15:40:56 -0400 Subject: [PATCH] [AiBundle][McpBundle] Upgrade bundle structure --- src/ai-bundle/config/options.php | 211 ++++++++ .../{src/Resources => }/config/services.php | 0 src/ai-bundle/phpstan.dist.neon | 3 - src/ai-bundle/src/AIBundle.php | 489 ++++++++++++++++- .../src/DependencyInjection/AIExtension.php | 501 ------------------ .../src/DependencyInjection/Configuration.php | 225 -------- .../data_collector.html.twig | 0 .../Resources/views => templates}/icon.svg | 0 src/mcp-bundle/config/options.php | 65 +++ .../{src/Resources => }/config/routes.php | 0 .../{src/Resources => }/config/services.php | 0 src/mcp-bundle/phpstan.dist.neon | 3 - .../src/DependencyInjection/Configuration.php | 77 --- .../src/DependencyInjection/McpExtension.php | 66 --- src/mcp-bundle/src/McpBundle.php | 56 +- 15 files changed, 817 insertions(+), 879 deletions(-) create mode 100644 src/ai-bundle/config/options.php rename src/ai-bundle/{src/Resources => }/config/services.php (100%) delete mode 100644 src/ai-bundle/src/DependencyInjection/AIExtension.php delete mode 100644 src/ai-bundle/src/DependencyInjection/Configuration.php rename src/ai-bundle/{src/Resources/views => templates}/data_collector.html.twig (100%) rename src/ai-bundle/{src/Resources/views => templates}/icon.svg (100%) create mode 100644 src/mcp-bundle/config/options.php rename src/mcp-bundle/{src/Resources => }/config/routes.php (100%) rename src/mcp-bundle/{src/Resources => }/config/services.php (100%) delete mode 100644 src/mcp-bundle/src/DependencyInjection/Configuration.php delete mode 100644 src/mcp-bundle/src/DependencyInjection/McpExtension.php diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php new file mode 100644 index 000000000..899ac21b5 --- /dev/null +++ b/src/ai-bundle/config/options.php @@ -0,0 +1,211 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Configurator; + +use Symfony\AI\Platform\PlatformInterface; +use Symfony\AI\Store\StoreInterface; + +return static function (DefinitionConfigurator $configurator): void { + $configurator->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() + ->arrayNode('mistral') + ->children() + ->scalarNode('api_key')->isRequired()->end() + ->end() + ->end() + ->arrayNode('openrouter') + ->children() + ->scalarNode('api_key')->isRequired()->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('agent') + ->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 agent') + ->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_agent')->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('indexer') + ->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() + ; +}; diff --git a/src/ai-bundle/src/Resources/config/services.php b/src/ai-bundle/config/services.php similarity index 100% rename from src/ai-bundle/src/Resources/config/services.php rename to src/ai-bundle/config/services.php diff --git a/src/ai-bundle/phpstan.dist.neon b/src/ai-bundle/phpstan.dist.neon index f2f24685d..41e004887 100644 --- a/src/ai-bundle/phpstan.dist.neon +++ b/src/ai-bundle/phpstan.dist.neon @@ -2,6 +2,3 @@ parameters: level: 6 paths: - src/ - excludePaths: - analyse: - - src/DependencyInjection/Configuration.php diff --git a/src/ai-bundle/src/AIBundle.php b/src/ai-bundle/src/AIBundle.php index ed3ad72e3..cc125e44b 100644 --- a/src/ai-bundle/src/AIBundle.php +++ b/src/ai-bundle/src/AIBundle.php @@ -11,11 +11,496 @@ namespace Symfony\AI\AIBundle; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\AI\Agent\Agent; +use Symfony\AI\Agent\AgentInterface; +use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor; +use Symfony\AI\Agent\InputProcessorInterface; +use Symfony\AI\Agent\OutputProcessorInterface; +use Symfony\AI\Agent\StructuredOutput\AgentProcessor as StructureOutputProcessor; +use Symfony\AI\Agent\Toolbox\AgentProcessor as ToolProcessor; +use Symfony\AI\Agent\Toolbox\Attribute\AsTool; +use Symfony\AI\Agent\Toolbox\FaultTolerantToolbox; +use Symfony\AI\Agent\Toolbox\Tool\Agent as AgentTool; +use Symfony\AI\Agent\Toolbox\ToolFactory\ChainFactory; +use Symfony\AI\Agent\Toolbox\ToolFactory\MemoryToolFactory; +use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory; +use Symfony\AI\AIBundle\Profiler\DataCollector; +use Symfony\AI\AIBundle\Profiler\TraceablePlatform; +use Symfony\AI\AIBundle\Profiler\TraceableToolbox; +use Symfony\AI\Platform\Bridge\Anthropic\Claude; +use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory; +use Symfony\AI\Platform\Bridge\Azure\OpenAI\PlatformFactory as AzureOpenAIPlatformFactory; +use Symfony\AI\Platform\Bridge\Google\Gemini; +use Symfony\AI\Platform\Bridge\Google\PlatformFactory as GooglePlatformFactory; +use Symfony\AI\Platform\Bridge\Meta\Llama; +use Symfony\AI\Platform\Bridge\Mistral\Mistral; +use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory; +use Symfony\AI\Platform\Bridge\OpenAI\Embeddings; +use Symfony\AI\Platform\Bridge\OpenAI\GPT; +use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory as OpenAIPlatformFactory; +use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory; +use Symfony\AI\Platform\Bridge\Voyage\Voyage; +use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\ModelClientInterface; +use Symfony\AI\Platform\Platform; +use Symfony\AI\Platform\PlatformInterface; +use Symfony\AI\Platform\ResponseConverterInterface; +use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore; +use Symfony\AI\Store\Bridge\ChromaDB\Store as ChromaDBStore; +use Symfony\AI\Store\Bridge\MongoDB\Store as MongoDBStore; +use Symfony\AI\Store\Bridge\Pinecone\Store as PineconeStore; +use Symfony\AI\Store\Document\Vectorizer; +use Symfony\AI\Store\Indexer; +use Symfony\AI\Store\StoreInterface; +use Symfony\AI\Store\VectorStoreInterface; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + +use function Symfony\Component\String\u; /** * @author Christopher Hertel */ -final class AIBundle extends Bundle +final class AIBundle extends AbstractBundle { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/options.php'); + } + + /** + * @param array $config + */ + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->import('../config/services.php'); + + foreach ($config['platform'] ?? [] as $type => $platform) { + $this->processPlatformConfig($type, $platform, $builder); + } + $platforms = array_keys($builder->findTaggedServiceIds('symfony_ai.platform')); + if (1 === \count($platforms)) { + $builder->setAlias(PlatformInterface::class, reset($platforms)); + } + if ($builder->getParameter('kernel.debug')) { + foreach ($platforms as $platform) { + $traceablePlatformDefinition = (new Definition(TraceablePlatform::class)) + ->setDecoratedService($platform) + ->setAutowired(true) + ->addTag('symfony_ai.traceable_platform'); + $suffix = u($platform)->afterLast('.')->toString(); + $builder->setDefinition('symfony_ai.traceable_platform.'.$suffix, $traceablePlatformDefinition); + } + } + + foreach ($config['agent'] as $agentName => $agent) { + $this->processAgentConfig($agentName, $agent, $builder); + } + if (1 === \count($config['agent']) && isset($agentName)) { + $builder->setAlias(AgentInterface::class, 'symfony_ai.agent.'.$agentName); + } + + foreach ($config['store'] ?? [] as $type => $store) { + $this->processStoreConfig($type, $store, $builder); + } + $stores = array_keys($builder->findTaggedServiceIds('symfony_ai.store')); + if (1 === \count($stores)) { + $builder->setAlias(VectorStoreInterface::class, reset($stores)); + $builder->setAlias(StoreInterface::class, reset($stores)); + } + + foreach ($config['indexer'] as $indexerName => $indexer) { + $this->processIndexerConfig($indexerName, $indexer, $builder); + } + if (1 === \count($config['indexer']) && isset($indexerName)) { + $builder->setAlias(Indexer::class, 'symfony_ai.indexer.'.$indexerName); + } + + $builder->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void { + $definition->addTag('symfony_ai.tool', [ + 'name' => $attribute->name, + 'description' => $attribute->description, + 'method' => $attribute->method, + ]); + }); + + $builder->registerForAutoconfiguration(InputProcessorInterface::class) + ->addTag('symfony_ai.agent.input_processor'); + $builder->registerForAutoconfiguration(OutputProcessorInterface::class) + ->addTag('symfony_ai.agent.output_processor'); + $builder->registerForAutoconfiguration(ModelClientInterface::class) + ->addTag('symfony_ai.platform.model_client'); + $builder->registerForAutoconfiguration(ResponseConverterInterface::class) + ->addTag('symfony_ai.platform.response_converter'); + + if (false === $builder->getParameter('kernel.debug')) { + $builder->removeDefinition(DataCollector::class); + $builder->removeDefinition(TraceableToolbox::class); + } + } + + /** + * @param array $platform + */ + private function processPlatformConfig(string $type, array $platform, ContainerBuilder $container): void + { + if ('anthropic' === $type) { + $platformId = 'symfony_ai.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('symfony_ai.platform'); + + if (isset($platform['version'])) { + $definition->replaceArgument('$version', $platform['version']); + } + + $container->setDefinition($platformId, $definition); + + return; + } + + if ('azure' === $type) { + foreach ($platform as $name => $config) { + $platformId = 'symfony_ai.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('symfony_ai.platform'); + + $container->setDefinition($platformId, $definition); + } + + return; + } + + if ('google' === $type) { + $platformId = 'symfony_ai.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('symfony_ai.platform'); + + $container->setDefinition($platformId, $definition); + + return; + } + + if ('openai' === $type) { + $platformId = 'symfony_ai.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('symfony_ai.platform'); + + $container->setDefinition($platformId, $definition); + + return; + } + + if ('openrouter' === $type) { + $platformId = 'symfony_ai.platform.openrouter'; + $definition = (new Definition(Platform::class)) + ->setFactory(OpenRouterPlatformFactory::class.'::create') + ->setAutowired(true) + ->setLazy(true) + ->addTag('proxy', ['interface' => PlatformInterface::class]) + ->setArguments(['$apiKey' => $platform['api_key']]) + ->addTag('symfony_ai.platform'); + + $container->setDefinition($platformId, $definition); + + return; + } + + if ('mistral' === $type) { + $platformId = 'symfony_ai.platform.mistral'; + $definition = (new Definition(Platform::class)) + ->setFactory(MistralPlatformFactory::class.'::create') + ->setAutowired(true) + ->setLazy(true) + ->addTag('proxy', ['interface' => PlatformInterface::class]) + ->setArguments(['$apiKey' => $platform['api_key']]) + ->addTag('symfony_ai.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 $config + */ + private function processAgentConfig(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, + 'mistral' => Mistral::class, + 'openrouter' => Model::class, + default => throw new \InvalidArgumentException(\sprintf('Model "%s" is not supported.', $modelName)), + }; + $modelDefinition = new Definition($modelClass); + if (null !== $version) { + $modelDefinition->setArgument('$name', $version); + } + if ([] !== $options) { + $modelDefinition->setArgument('$options', $options); + } + $modelDefinition->addTag('symfony_ai.model.language_model'); + $container->setDefinition('symfony_ai.agent.'.$name.'.model', $modelDefinition); + + // AGENT + $agentDefinition = (new Definition(Agent::class)) + ->setAutowired(true) + ->setArgument('$platform', new Reference($config['platform'])) + ->setArgument('$model', new Reference('symfony_ai.agent.'.$name.'.model')); + + $inputProcessors = []; + $outputProcessors = []; + + // TOOL & PROCESSOR + if ($config['tools']['enabled']) { + // Create specific toolbox and process if tools are explicitly defined + if ([] !== $config['tools']['services']) { + $memoryFactoryDefinition = new Definition(MemoryToolFactory::class); + $container->setDefinition('symfony_ai.toolbox.'.$name.'.memory_factory', $memoryFactoryDefinition); + $chainFactoryDefinition = new Definition(ChainFactory::class, [ + '$factories' => [new Reference('symfony_ai.toolbox.'.$name.'.memory_factory'), new Reference(ReflectionToolFactory::class)], + ]); + $container->setDefinition('symfony_ai.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_agent']) { + $agentWrapperDefinition = new Definition(AgentTool::class, ['$agent' => $reference]); + $container->setDefinition('symfony_ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name'], $agentWrapperDefinition); + $reference = new Reference('symfony_ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name']); + } + $memoryFactoryDefinition->addMethodCall('addTool', [$reference, $tool['name'], $tool['description'], $tool['method'] ?? '__invoke']); + } + $tools[] = $reference; + } + + $toolboxDefinition = (new ChildDefinition('symfony_ai.toolbox.abstract')) + ->replaceArgument('$toolFactory', new Reference('symfony_ai.toolbox.'.$name.'.chain_factory')) + ->replaceArgument('$tools', $tools); + $container->setDefinition('symfony_ai.toolbox.'.$name, $toolboxDefinition); + + if ($config['fault_tolerant_toolbox']) { + $faultTolerantToolboxDefinition = (new Definition('symfony_ai.fault_tolerant_toolbox.'.$name)) + ->setClass(FaultTolerantToolbox::class) + ->setAutowired(true) + ->setDecoratedService('symfony_ai.toolbox.'.$name); + $container->setDefinition('symfony_ai.fault_tolerant_toolbox.'.$name, $faultTolerantToolboxDefinition); + } + + if ($container->getParameter('kernel.debug')) { + $traceableToolboxDefinition = (new Definition('symfony_ai.traceable_toolbox.'.$name)) + ->setClass(TraceableToolbox::class) + ->setAutowired(true) + ->setDecoratedService('symfony_ai.toolbox.'.$name) + ->addTag('symfony_ai.traceable_toolbox'); + $container->setDefinition('symfony_ai.traceable_toolbox.'.$name, $traceableToolboxDefinition); + } + + $toolProcessorDefinition = (new ChildDefinition('symfony_ai.tool.agent_processor.abstract')) + ->replaceArgument('$toolbox', new Reference('symfony_ai.toolbox.'.$name)); + $container->setDefinition('symfony_ai.tool.agent_processor.'.$name, $toolProcessorDefinition); + + $inputProcessors[] = new Reference('symfony_ai.tool.agent_processor.'.$name); + $outputProcessors[] = new Reference('symfony_ai.tool.agent_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('symfony_ai.toolbox.'.$name) : null, + ]); + + $inputProcessors[] = $systemPromptInputProcessorDefinition; + } + + $agentDefinition + ->setArgument('$inputProcessors', $inputProcessors) + ->setArgument('$outputProcessors', $outputProcessors); + + $container->setDefinition('symfony_ai.agent.'.$name, $agentDefinition); + } + + /** + * @param array $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('symfony_ai.store') + ->setArguments($arguments); + + $container->setDefinition('symfony_ai.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('symfony_ai.store'); + + $container->setDefinition('symfony_ai.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('symfony_ai.store') + ->setArguments($arguments); + + $container->setDefinition('symfony_ai.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('symfony_ai.store') + ->setArguments($arguments); + + $container->setDefinition('symfony_ai.store.'.$type.'.'.$name, $definition); + } + } + } + + /** + * @param array $config + */ + private function processIndexerConfig(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 ([] !== $options) { + $modelDefinition->setArgument('$options', $options); + } + $modelDefinition->addTag('symfony_ai.model.embeddings_model'); + $container->setDefinition('symfony_ai.indexer.'.$name.'.model', $modelDefinition); + + $vectorizerDefinition = new Definition(Vectorizer::class, [ + '$platform' => new Reference($config['platform']), + '$model' => new Reference('symfony_ai.indexer.'.$name.'.model'), + ]); + $container->setDefinition('symfony_ai.indexer.'.$name.'.vectorizer', $vectorizerDefinition); + + $definition = new Definition(Indexer::class, [ + '$vectorizer' => new Reference('symfony_ai.indexer.'.$name.'.vectorizer'), + '$store' => new Reference($config['store']), + ]); + + $container->setDefinition('symfony_ai.indexer.'.$name, $definition); + } } diff --git a/src/ai-bundle/src/DependencyInjection/AIExtension.php b/src/ai-bundle/src/DependencyInjection/AIExtension.php deleted file mode 100644 index 0d06dc4a6..000000000 --- a/src/ai-bundle/src/DependencyInjection/AIExtension.php +++ /dev/null @@ -1,501 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\AIBundle\DependencyInjection; - -use Symfony\AI\Agent\Agent; -use Symfony\AI\Agent\AgentInterface; -use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor; -use Symfony\AI\Agent\InputProcessorInterface; -use Symfony\AI\Agent\OutputProcessorInterface; -use Symfony\AI\Agent\StructuredOutput\AgentProcessor as StructureOutputProcessor; -use Symfony\AI\Agent\Toolbox\AgentProcessor as ToolProcessor; -use Symfony\AI\Agent\Toolbox\Attribute\AsTool; -use Symfony\AI\Agent\Toolbox\FaultTolerantToolbox; -use Symfony\AI\Agent\Toolbox\Tool\Agent as AgentTool; -use Symfony\AI\Agent\Toolbox\ToolFactory\ChainFactory; -use Symfony\AI\Agent\Toolbox\ToolFactory\MemoryToolFactory; -use Symfony\AI\Agent\Toolbox\ToolFactory\ReflectionToolFactory; -use Symfony\AI\AIBundle\Profiler\DataCollector; -use Symfony\AI\AIBundle\Profiler\TraceablePlatform; -use Symfony\AI\AIBundle\Profiler\TraceableToolbox; -use Symfony\AI\Platform\Bridge\Anthropic\Claude; -use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory; -use Symfony\AI\Platform\Bridge\Azure\OpenAI\PlatformFactory as AzureOpenAIPlatformFactory; -use Symfony\AI\Platform\Bridge\Google\Gemini; -use Symfony\AI\Platform\Bridge\Google\PlatformFactory as GooglePlatformFactory; -use Symfony\AI\Platform\Bridge\Meta\Llama; -use Symfony\AI\Platform\Bridge\Mistral\Mistral; -use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory; -use Symfony\AI\Platform\Bridge\OpenAI\Embeddings; -use Symfony\AI\Platform\Bridge\OpenAI\GPT; -use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory as OpenAIPlatformFactory; -use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory; -use Symfony\AI\Platform\Bridge\Voyage\Voyage; -use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface; -use Symfony\AI\Platform\Platform; -use Symfony\AI\Platform\PlatformInterface; -use Symfony\AI\Platform\ResponseConverterInterface; -use Symfony\AI\Store\Bridge\Azure\SearchStore as AzureSearchStore; -use Symfony\AI\Store\Bridge\ChromaDB\Store as ChromaDBStore; -use Symfony\AI\Store\Bridge\MongoDB\Store as MongoDBStore; -use Symfony\AI\Store\Bridge\Pinecone\Store as PineconeStore; -use Symfony\AI\Store\Document\Vectorizer; -use Symfony\AI\Store\Indexer; -use Symfony\AI\Store\StoreInterface; -use Symfony\AI\Store\VectorStoreInterface; -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; - -/** - * @author Christopher Hertel - */ -final class AIExtension 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('symfony_ai.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('symfony_ai.traceable_platform'); - $suffix = u($platform)->afterLast('.')->toString(); - $container->setDefinition('symfony_ai.traceable_platform.'.$suffix, $traceablePlatformDefinition); - } - } - - foreach ($config['agent'] as $agentName => $agent) { - $this->processAgentConfig($agentName, $agent, $container); - } - if (1 === \count($config['agent']) && isset($agentName)) { - $container->setAlias(AgentInterface::class, 'symfony_ai.agent.'.$agentName); - } - - foreach ($config['store'] ?? [] as $type => $store) { - $this->processStoreConfig($type, $store, $container); - } - $stores = array_keys($container->findTaggedServiceIds('symfony_ai.store')); - if (1 === \count($stores)) { - $container->setAlias(VectorStoreInterface::class, reset($stores)); - $container->setAlias(StoreInterface::class, reset($stores)); - } - - foreach ($config['indexer'] as $indexerName => $indexer) { - $this->processIndexerConfig($indexerName, $indexer, $container); - } - if (1 === \count($config['indexer']) && isset($indexerName)) { - $container->setAlias(Indexer::class, 'symfony_ai.indexer.'.$indexerName); - } - - $container->registerAttributeForAutoconfiguration(AsTool::class, static function (ChildDefinition $definition, AsTool $attribute): void { - $definition->addTag('symfony_ai.tool', [ - 'name' => $attribute->name, - 'description' => $attribute->description, - 'method' => $attribute->method, - ]); - }); - - $container->registerForAutoconfiguration(InputProcessorInterface::class) - ->addTag('symfony_ai.agent.input_processor'); - $container->registerForAutoconfiguration(OutputProcessorInterface::class) - ->addTag('symfony_ai.agent.output_processor'); - $container->registerForAutoconfiguration(ModelClientInterface::class) - ->addTag('symfony_ai.platform.model_client'); - $container->registerForAutoconfiguration(ResponseConverterInterface::class) - ->addTag('symfony_ai.platform.response_converter'); - - if (false === $container->getParameter('kernel.debug')) { - $container->removeDefinition(DataCollector::class); - $container->removeDefinition(TraceableToolbox::class); - } - } - - /** - * @param array $platform - */ - private function processPlatformConfig(string $type, array $platform, ContainerBuilder $container): void - { - if ('anthropic' === $type) { - $platformId = 'symfony_ai.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('symfony_ai.platform'); - - if (isset($platform['version'])) { - $definition->replaceArgument('$version', $platform['version']); - } - - $container->setDefinition($platformId, $definition); - - return; - } - - if ('azure' === $type) { - foreach ($platform as $name => $config) { - $platformId = 'symfony_ai.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('symfony_ai.platform'); - - $container->setDefinition($platformId, $definition); - } - - return; - } - - if ('google' === $type) { - $platformId = 'symfony_ai.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('symfony_ai.platform'); - - $container->setDefinition($platformId, $definition); - - return; - } - - if ('openai' === $type) { - $platformId = 'symfony_ai.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('symfony_ai.platform'); - - $container->setDefinition($platformId, $definition); - - return; - } - - if ('openrouter' === $type) { - $platformId = 'symfony_ai.platform.openrouter'; - $definition = (new Definition(Platform::class)) - ->setFactory(OpenRouterPlatformFactory::class.'::create') - ->setAutowired(true) - ->setLazy(true) - ->addTag('proxy', ['interface' => PlatformInterface::class]) - ->setArguments(['$apiKey' => $platform['api_key']]) - ->addTag('symfony_ai.platform'); - - $container->setDefinition($platformId, $definition); - - return; - } - - if ('mistral' === $type) { - $platformId = 'symfony_ai.platform.mistral'; - $definition = (new Definition(Platform::class)) - ->setFactory(MistralPlatformFactory::class.'::create') - ->setAutowired(true) - ->setLazy(true) - ->addTag('proxy', ['interface' => PlatformInterface::class]) - ->setArguments(['$apiKey' => $platform['api_key']]) - ->addTag('symfony_ai.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 $config - */ - private function processAgentConfig(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, - 'mistral' => Mistral::class, - 'openrouter' => Model::class, - default => throw new \InvalidArgumentException(\sprintf('Model "%s" is not supported.', $modelName)), - }; - $modelDefinition = new Definition($modelClass); - if (null !== $version) { - $modelDefinition->setArgument('$name', $version); - } - if ([] !== $options) { - $modelDefinition->setArgument('$options', $options); - } - $modelDefinition->addTag('symfony_ai.model.language_model'); - $container->setDefinition('symfony_ai.agent.'.$name.'.model', $modelDefinition); - - // AGENT - $agentDefinition = (new Definition(Agent::class)) - ->setAutowired(true) - ->setArgument('$platform', new Reference($config['platform'])) - ->setArgument('$model', new Reference('symfony_ai.agent.'.$name.'.model')); - - $inputProcessors = []; - $outputProcessors = []; - - // TOOL & PROCESSOR - if ($config['tools']['enabled']) { - // Create specific toolbox and process if tools are explicitly defined - if ([] !== $config['tools']['services']) { - $memoryFactoryDefinition = new Definition(MemoryToolFactory::class); - $container->setDefinition('symfony_ai.toolbox.'.$name.'.memory_factory', $memoryFactoryDefinition); - $chainFactoryDefinition = new Definition(ChainFactory::class, [ - '$factories' => [new Reference('symfony_ai.toolbox.'.$name.'.memory_factory'), new Reference(ReflectionToolFactory::class)], - ]); - $container->setDefinition('symfony_ai.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_agent']) { - $agentWrapperDefinition = new Definition(AgentTool::class, ['$agent' => $reference]); - $container->setDefinition('symfony_ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name'], $agentWrapperDefinition); - $reference = new Reference('symfony_ai.toolbox.'.$name.'.agent_wrapper.'.$tool['name']); - } - $memoryFactoryDefinition->addMethodCall('addTool', [$reference, $tool['name'], $tool['description'], $tool['method'] ?? '__invoke']); - } - $tools[] = $reference; - } - - $toolboxDefinition = (new ChildDefinition('symfony_ai.toolbox.abstract')) - ->replaceArgument('$toolFactory', new Reference('symfony_ai.toolbox.'.$name.'.chain_factory')) - ->replaceArgument('$tools', $tools); - $container->setDefinition('symfony_ai.toolbox.'.$name, $toolboxDefinition); - - if ($config['fault_tolerant_toolbox']) { - $faultTolerantToolboxDefinition = (new Definition('symfony_ai.fault_tolerant_toolbox.'.$name)) - ->setClass(FaultTolerantToolbox::class) - ->setAutowired(true) - ->setDecoratedService('symfony_ai.toolbox.'.$name); - $container->setDefinition('symfony_ai.fault_tolerant_toolbox.'.$name, $faultTolerantToolboxDefinition); - } - - if ($container->getParameter('kernel.debug')) { - $traceableToolboxDefinition = (new Definition('symfony_ai.traceable_toolbox.'.$name)) - ->setClass(TraceableToolbox::class) - ->setAutowired(true) - ->setDecoratedService('symfony_ai.toolbox.'.$name) - ->addTag('symfony_ai.traceable_toolbox'); - $container->setDefinition('symfony_ai.traceable_toolbox.'.$name, $traceableToolboxDefinition); - } - - $toolProcessorDefinition = (new ChildDefinition('symfony_ai.tool.agent_processor.abstract')) - ->replaceArgument('$toolbox', new Reference('symfony_ai.toolbox.'.$name)); - $container->setDefinition('symfony_ai.tool.agent_processor.'.$name, $toolProcessorDefinition); - - $inputProcessors[] = new Reference('symfony_ai.tool.agent_processor.'.$name); - $outputProcessors[] = new Reference('symfony_ai.tool.agent_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('symfony_ai.toolbox.'.$name) : null, - ]); - - $inputProcessors[] = $systemPromptInputProcessorDefinition; - } - - $agentDefinition - ->setArgument('$inputProcessors', $inputProcessors) - ->setArgument('$outputProcessors', $outputProcessors); - - $container->setDefinition('symfony_ai.agent.'.$name, $agentDefinition); - } - - /** - * @param array $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('symfony_ai.store') - ->setArguments($arguments); - - $container->setDefinition('symfony_ai.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('symfony_ai.store'); - - $container->setDefinition('symfony_ai.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('symfony_ai.store') - ->setArguments($arguments); - - $container->setDefinition('symfony_ai.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('symfony_ai.store') - ->setArguments($arguments); - - $container->setDefinition('symfony_ai.store.'.$type.'.'.$name, $definition); - } - } - } - - /** - * @param array $config - */ - private function processIndexerConfig(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 ([] !== $options) { - $modelDefinition->setArgument('$options', $options); - } - $modelDefinition->addTag('symfony_ai.model.embeddings_model'); - $container->setDefinition('symfony_ai.indexer.'.$name.'.model', $modelDefinition); - - $vectorizerDefinition = new Definition(Vectorizer::class, [ - '$platform' => new Reference($config['platform']), - '$model' => new Reference('symfony_ai.indexer.'.$name.'.model'), - ]); - $container->setDefinition('symfony_ai.indexer.'.$name.'.vectorizer', $vectorizerDefinition); - - $definition = new Definition(Indexer::class, [ - '$vectorizer' => new Reference('symfony_ai.indexer.'.$name.'.vectorizer'), - '$store' => new Reference($config['store']), - ]); - - $container->setDefinition('symfony_ai.indexer.'.$name, $definition); - } -} diff --git a/src/ai-bundle/src/DependencyInjection/Configuration.php b/src/ai-bundle/src/DependencyInjection/Configuration.php deleted file mode 100644 index f05f9bced..000000000 --- a/src/ai-bundle/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,225 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\AIBundle\DependencyInjection; - -use Symfony\AI\Platform\PlatformInterface; -use Symfony\AI\Store\StoreInterface; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -/** - * @author Christopher Hertel - */ -final class Configuration implements ConfigurationInterface -{ - public function getConfigTreeBuilder(): TreeBuilder - { - $treeBuilder = new TreeBuilder('ai'); - $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() - ->arrayNode('mistral') - ->children() - ->scalarNode('api_key')->isRequired()->end() - ->end() - ->end() - ->arrayNode('openrouter') - ->children() - ->scalarNode('api_key')->isRequired()->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('agent') - ->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 agent') - ->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_agent')->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('indexer') - ->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; - } -} diff --git a/src/ai-bundle/src/Resources/views/data_collector.html.twig b/src/ai-bundle/templates/data_collector.html.twig similarity index 100% rename from src/ai-bundle/src/Resources/views/data_collector.html.twig rename to src/ai-bundle/templates/data_collector.html.twig diff --git a/src/ai-bundle/src/Resources/views/icon.svg b/src/ai-bundle/templates/icon.svg similarity index 100% rename from src/ai-bundle/src/Resources/views/icon.svg rename to src/ai-bundle/templates/icon.svg diff --git a/src/mcp-bundle/config/options.php b/src/mcp-bundle/config/options.php new file mode 100644 index 000000000..6e0dce185 --- /dev/null +++ b/src/mcp-bundle/config/options.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Configurator; + +return static function (DefinitionConfigurator $configurator): void { + $configurator->rootNode() + ->children() + ->scalarNode('app')->defaultValue('app')->end() + ->scalarNode('version')->defaultValue('0.0.1')->end() + // ->arrayNode('servers') + // ->useAttributeAsKey('name') + // ->arrayPrototype() + // ->children() + // ->enumNode('transport') + // ->values(['stdio', 'sse']) + // ->isRequired() + // ->end() + // ->arrayNode('stdio') + // ->children() + // ->scalarNode('command')->isRequired()->end() + // ->arrayNode('arguments') + // ->scalarPrototype()->end() + // ->defaultValue([]) + // ->end() + // ->end() + // ->end() + // ->arrayNode('sse') + // ->children() + // ->scalarNode('url')->isRequired()->end() + // ->end() + // ->end() + // ->end() + // ->validate() + // ->ifTrue(function ($v) { + // if ('stdio' === $v['transport'] && !isset($v['stdio'])) { + // return true; + // } + // if ('sse' === $v['transport'] && !isset($v['sse'])) { + // return true; + // } + // + // return false; + // }) + // ->thenInvalid('When transport is "%s", you must configure the corresponding section.') + // ->end() + // ->end() + // ->end() + ->arrayNode('client_transports') + ->children() + ->booleanNode('stdio')->defaultFalse()->end() + ->booleanNode('sse')->defaultFalse()->end() + ->end() + ->end() + ->end() + ; +}; diff --git a/src/mcp-bundle/src/Resources/config/routes.php b/src/mcp-bundle/config/routes.php similarity index 100% rename from src/mcp-bundle/src/Resources/config/routes.php rename to src/mcp-bundle/config/routes.php diff --git a/src/mcp-bundle/src/Resources/config/services.php b/src/mcp-bundle/config/services.php similarity index 100% rename from src/mcp-bundle/src/Resources/config/services.php rename to src/mcp-bundle/config/services.php diff --git a/src/mcp-bundle/phpstan.dist.neon b/src/mcp-bundle/phpstan.dist.neon index f2f24685d..41e004887 100644 --- a/src/mcp-bundle/phpstan.dist.neon +++ b/src/mcp-bundle/phpstan.dist.neon @@ -2,6 +2,3 @@ parameters: level: 6 paths: - src/ - excludePaths: - analyse: - - src/DependencyInjection/Configuration.php diff --git a/src/mcp-bundle/src/DependencyInjection/Configuration.php b/src/mcp-bundle/src/DependencyInjection/Configuration.php deleted file mode 100644 index 8c03bec0d..000000000 --- a/src/mcp-bundle/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpBundle\DependencyInjection; - -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('mcp'); - $rootNode = $treeBuilder->getRootNode(); - - $rootNode - ->children() - ->scalarNode('app')->defaultValue('app')->end() - ->scalarNode('version')->defaultValue('0.0.1')->end() - // ->arrayNode('servers') - // ->useAttributeAsKey('name') - // ->arrayPrototype() - // ->children() - // ->enumNode('transport') - // ->values(['stdio', 'sse']) - // ->isRequired() - // ->end() - // ->arrayNode('stdio') - // ->children() - // ->scalarNode('command')->isRequired()->end() - // ->arrayNode('arguments') - // ->scalarPrototype()->end() - // ->defaultValue([]) - // ->end() - // ->end() - // ->end() - // ->arrayNode('sse') - // ->children() - // ->scalarNode('url')->isRequired()->end() - // ->end() - // ->end() - // ->end() - // ->validate() - // ->ifTrue(function ($v) { - // if ('stdio' === $v['transport'] && !isset($v['stdio'])) { - // return true; - // } - // if ('sse' === $v['transport'] && !isset($v['sse'])) { - // return true; - // } - // - // return false; - // }) - // ->thenInvalid('When transport is "%s", you must configure the corresponding section.') - // ->end() - // ->end() - // ->end() - ->arrayNode('client_transports') - ->children() - ->booleanNode('stdio')->defaultFalse()->end() - ->booleanNode('sse')->defaultFalse()->end() - ->end() - ->end() - ->end() - ; - - return $treeBuilder; - } -} diff --git a/src/mcp-bundle/src/DependencyInjection/McpExtension.php b/src/mcp-bundle/src/DependencyInjection/McpExtension.php deleted file mode 100644 index 57b4096b8..000000000 --- a/src/mcp-bundle/src/DependencyInjection/McpExtension.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\AI\McpBundle\DependencyInjection; - -use Symfony\AI\McpBundle\Command\McpCommand; -use Symfony\AI\McpBundle\Controller\McpController; -use Symfony\AI\McpBundle\Routing\RouteLoader; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; - -final class McpExtension 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); - - $container->setParameter('mcp.app', $config['app']); - $container->setParameter('mcp.version', $config['version']); - - if (isset($config['client_transports'])) { - $this->configureClient($config['client_transports'], $container); - } - } - - /** - * @param array{stdio: bool, sse: bool} $transports - */ - private function configureClient(array $transports, ContainerBuilder $container): void - { - if (!$transports['stdio'] && !$transports['sse']) { - return; - } - - if ($transports['stdio']) { - $container->register('mcp.server.command', McpCommand::class) - ->setAutowired(true) - ->addTag('console.command'); - } - - if ($transports['sse']) { - $container->register('mcp.server.controller', McpController::class) - ->setAutowired(true) - ->setPublic(true) - ->addTag('controller.service_arguments'); - } - - $container->register('mcp.server.route_loader', RouteLoader::class) - ->setArgument('$sseTransportEnabled', $transports['sse']) - ->addTag('routing.route_loader'); - } -} diff --git a/src/mcp-bundle/src/McpBundle.php b/src/mcp-bundle/src/McpBundle.php index 4c863dfce..44a11c490 100644 --- a/src/mcp-bundle/src/McpBundle.php +++ b/src/mcp-bundle/src/McpBundle.php @@ -11,8 +11,60 @@ namespace Symfony\AI\McpBundle; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\AI\McpBundle\Command\McpCommand; +use Symfony\AI\McpBundle\Controller\McpController; +use Symfony\AI\McpBundle\Routing\RouteLoader; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; -final class McpBundle extends Bundle +final class McpBundle extends AbstractBundle { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../config/options.php'); + } + + /** + * @param array $config + */ + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->import('../config/services.php'); + + $builder->setParameter('mcp.app', $config['app']); + $builder->setParameter('mcp.version', $config['version']); + + if (isset($config['client_transports'])) { + $this->configureClient($config['client_transports'], $builder); + } + } + + /** + * @param array{stdio: bool, sse: bool} $transports + */ + private function configureClient(array $transports, ContainerBuilder $container): void + { + if (!$transports['stdio'] && !$transports['sse']) { + return; + } + + if ($transports['stdio']) { + $container->register('mcp.server.command', McpCommand::class) + ->setAutowired(true) + ->addTag('console.command'); + } + + if ($transports['sse']) { + $container->register('mcp.server.controller', McpController::class) + ->setAutowired(true) + ->setPublic(true) + ->addTag('controller.service_arguments'); + } + + $container->register('mcp.server.route_loader', RouteLoader::class) + ->setArgument('$sseTransportEnabled', $transports['sse']) + ->addTag('routing.route_loader'); + } }