diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index cca7df98..c4cd5607 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,37 +22,37 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Parameters\AbstractParameter; -use App\Services\AI\AIPlatformRegistry; -use App\Services\AI\AIPlatforms; -use App\Settings\MiscSettings\IpnSuggestSettings; -use Symfony\Component\Cache\Adapter\AdapterInterface; -use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; +use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; -use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Currency; use App\Repository\ParameterRepository; +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Attachments\PartPreviewGenerator; use App\Services\Tools\TagFinder; +use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\ORM\EntityManagerInterface; +use Symfony\AI\Platform\Capability; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -126,9 +126,12 @@ class TypeaheadController extends AbstractController } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); $repo = $entityManager->getRepository(Part::class); @@ -139,7 +142,7 @@ class TypeaheadController extends AbstractController foreach ($parts as $part) { //Determine the picture to show: $preview_attachment = $previewGenerator->getTablePreviewAttachment($part); - if($preview_attachment instanceof Attachment) { + if ($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); } else { $preview_url = ''; @@ -153,7 +156,7 @@ class TypeaheadController extends AbstractController 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); @@ -224,7 +227,8 @@ class TypeaheadController extends AbstractController $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->ipnSuggestSettings->suggestPartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, + $this->ipnSuggestSettings->suggestPartDigits); return new JsonResponse($ipnSuggestions); } @@ -232,16 +236,26 @@ class TypeaheadController extends AbstractController #[Route(path: '/ai/{platform}/models', name: 'typeahead_ai_models', requirements: ['platform' => '.+'])] public function aiModels( AIPlatforms $platform, + Request $request, AIPlatformRegistry $platformRegistry, CacheInterface $cache, ): JsonResponse { - $this->denyAccessUnlessGranted('@config.change_system_settings'); - $models = $cache->get('ai_models_'.$platform->value, function(ItemInterface $item) use ($platformRegistry, $platform) { - $item->expiresAfter(3600); //Cache for 1 hour - return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(); - }); + $capability_filter = $request->query->getEnum('capability', Capability::class); + + $models = $cache->get('ai_models_'.$platform->value.'_'.($capability_filter?->value ?? 'all'), + function (ItemInterface $item) use ($platformRegistry, $platform, $capability_filter) { + $item->expiresAfter(3600); //Cache for 1 hour + if ($capability_filter === null) { + return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(); + } + + //Otherwise filter the models by the capability + return array_filter($platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(), + static fn(array $model) => in_array($capability_filter, $model['capabilities'] ?? [], true) + ); + }); return new JsonResponse($models); } diff --git a/src/Form/Settings/AiModelsType.php b/src/Form/Settings/AiModelsType.php index 78b088ed..5228bb47 100644 --- a/src/Form/Settings/AiModelsType.php +++ b/src/Form/Settings/AiModelsType.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Form\Settings; +use Symfony\AI\Platform\Capability; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormInterface; @@ -50,11 +51,20 @@ final class AiModelsType extends AbstractType //The target label of the platform select, which is used to filter the models for the selected platform. $resolver->setRequired('platform_selector'); $resolver->setAllowedTypes('platform_selector', 'string'); + + //Only show models, that have the given capability. This is used to only show models that support structured output for the AI extractor settings. + $resolver->setDefault('filter_capability', null); + $resolver->setAllowedTypes('filter_capability', ['null', Capability::class]); } public function finishView(FormView $view, FormInterface $form, array $options): void { - $view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', ['platform' => '__PLATFORM__']); + $urlOptions = ['platform' => '__PLATFORM__']; + if ($options['filter_capability'] !== null) { + $urlOptions['capability'] = $options['filter_capability']->value; + } + + $view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', $urlOptions); $view->vars['attr']['data-controller'] = 'elements--ai-model-autocomplete'; $view->vars['attr']['data-platform-selector'] = $options['platform_selector']; diff --git a/src/Settings/InfoProviderSystem/AIExtractorSettings.php b/src/Settings/InfoProviderSystem/AIExtractorSettings.php index 0852d2ec..2efd6717 100644 --- a/src/Settings/InfoProviderSystem/AIExtractorSettings.php +++ b/src/Settings/InfoProviderSystem/AIExtractorSettings.php @@ -31,6 +31,7 @@ use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\AI\Platform\Capability; use Symfony\Component\Translation\TranslatableMessage as TM; #[Settings(name: "ai_extractor", label: new TM("settings.ips.ai_extractor"), description: new TM("settings.ips.ai_extractor.description"))] @@ -48,7 +49,7 @@ class AIExtractorSettings public ?AIPlatforms $platform = null; #[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.description"), - formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL], + formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL, 'filter_capability' => Capability::OUTPUT_STRUCTURED], envVar: "string:PROVIDER_AI_EXTRACTOR_MODEL", envVarMode: EnvVarMode::OVERWRITE )] public string $model = 'z-ai/glm-4.7';