diff --git a/assets/controllers/elements/ai_model_autocomplete_controller.js b/assets/controllers/elements/ai_model_autocomplete_controller.js deleted file mode 100644 index e36e6b1f..00000000 --- a/assets/controllers/elements/ai_model_autocomplete_controller.js +++ /dev/null @@ -1,152 +0,0 @@ -/* - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import {Controller} from "@hotwired/stimulus"; - -import "tom-select/dist/css/tom-select.bootstrap5.css"; -import '../../css/components/tom-select_extensions.css'; -import TomSelect from "tom-select"; - -import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit' -import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed' - -TomSelect.define('click_to_edit', TomSelect_click_to_edit) -TomSelect.define('autoselect_typed', TomSelect_autoselect_typed) - -export default class extends Controller { - _tomSelect; - - _platformSelector; - - connect() { - - let dropdownParent = "body"; - if (this.element.closest('.modal')) { - dropdownParent = null - } - - //Try to find the platform selector - const platformSelector = document.querySelector("select[data-platform-selector-label='" + this.element.dataset.platformSelector + "']"); - //Clear tomselect options, if the platform selector changes - if (platformSelector) { - this.platformSelector = platformSelector; - platformSelector.addEventListener('change', () => { - //Force reload of options by clearing the cache and options of TomSelect and triggering a search with an empty string - this._tomSelect.clearOptions(); - this._tomSelect.clearCache(); - this._tomSelect.load(''); - }); - } - - let settings = { - persistent: false, - create: true, - maxItems: 1, - preload: 'focus', - createOnBlur: true, - selectOnTab: true, - clearAfterSelect: true, - shouldLoad: ((query) => true), - maxOptions: null, - //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin - delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', - dropdownParent: dropdownParent, - render: { - item: (data, escape) => { - return '' + escape(data.label) + ''; - }, - option: (data, escape) => { - if (data.image) { - return "
" + data.label + "
" - } - return '
' + escape(data.label) + '
'; - } - }, - plugins: { - 'autoselect_typed': {}, - 'click_to_edit': {}, - 'clear_button': {}, - "restore_on_backspace": {} - } - }; - - if(this.element.dataset.urlTemplate) { - const base_url = this.element.dataset.urlTemplate; - settings.searchField = "label"; - settings.sortField = "label"; - settings.valueField = "label"; - settings.load = (query, callback) => { - - - if (!this.platformSelector) { - console.error("Platform selector not found for AI model autocomplete"); - callback(); - return; - } - - //Platform is the selected option - const platform = this.platformSelector.value; - if (!platform) { - callback(); - return; - } - - const self = this; - - //Only fetch each platform once - if(self.platformLoaded === platform) { - callback(); - } - - - const url = base_url.replace('__PLATFORM__', encodeURIComponent(platform)); - - fetch(url) - .then(response => response.json()) - .then(json => { - - self.platformLoaded = platform; - - var data = []; - - for (const name in json) { - data.push({ - "label": name, - "capabilities": json[name].capabilities, - }); - } - - callback(data); - }).catch(()=>{ - callback(); - }); - }; - } - this._tomSelect = new TomSelect(this.element, settings); - } - - disconnect() { - super.disconnect(); - //Destroy the TomSelect instance - this._tomSelect.destroy(); - } - -} - - diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index c4cd5607..95480329 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,43 +22,38 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Attachments\Attachment; use App\Entity\Parameters\AbstractParameter; +use App\Settings\MiscSettings\IpnSuggestSettings; +use Symfony\Component\HttpFoundation\Response; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; 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; use Symfony\Component\Serializer\Serializer; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; /** * In this controller the endpoints for the typeaheads are collected. @@ -126,12 +121,9 @@ 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); @@ -142,7 +134,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 = ''; @@ -156,7 +148,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); @@ -227,36 +219,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); } - - #[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'); - - $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 deleted file mode 100644 index 5228bb47..00000000 --- a/src/Form/Settings/AiModelsType.php +++ /dev/null @@ -1,72 +0,0 @@ -. - */ - -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; -use Symfony\Component\Form\FormView; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -/** - * An text input with autocomplete for AI models from the given platform. - * The platform is determined by the value of another form field, which is specified by the "platform_selector" option. This allows to filter the available models based on the selected platform. - */ -final class AiModelsType extends AbstractType -{ - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) - { - } - - public function getParent(): string - { - return TextType::class; - } - - public function configureOptions(OptionsResolver $resolver): void - { - //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 - { - $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/Form/Settings/AiPlatformChoiceType.php b/src/Form/Settings/AiPlatformChoiceType.php index eb48d933..b28f217d 100644 --- a/src/Form/Settings/AiPlatformChoiceType.php +++ b/src/Form/Settings/AiPlatformChoiceType.php @@ -28,13 +28,8 @@ use App\Services\AI\AIPlatforms; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EnumType; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * Allow to choose an AI platform from the enabled platforms in the system. This is used in the settings to choose the default platform for AI features. - */ final class AiPlatformChoiceType extends AbstractType { public function __construct(private readonly AIPlatformRegistry $platformRegistry) @@ -54,12 +49,6 @@ final class AiPlatformChoiceType extends AbstractType 'class' => AIPlatforms::class, 'choices' => $choices, 'required' => false, - 'platform_selector_label' => null ]); } - - public function finishView(FormView $view, FormInterface $form, array $options): void - { - $view->vars['attr']['data-platform-selector-label'] = $options['platform_selector_label'] ?? $view->vars['id'].'_label'; - } } diff --git a/src/Services/InfoProviderSystem/Providers/AIInfoExtractor.php b/src/Services/InfoProviderSystem/Providers/AIInfoExtractor.php index c8eff0a4..d5be0267 100644 --- a/src/Services/InfoProviderSystem/Providers/AIInfoExtractor.php +++ b/src/Services/InfoProviderSystem/Providers/AIInfoExtractor.php @@ -60,7 +60,7 @@ final class AIInfoExtractor implements InfoProviderInterface return [ 'name' => 'AI Information Extractor', 'description' => 'Extract part info from any URL using OpenRouter LLM', - //'url' => 'https://openrouter.ai', + 'url' => 'https://openrouter.ai', 'disabled_help' => 'Configure OpenRouter API key in settings', 'settings_class' => AIExtractorSettings::class, ]; @@ -73,7 +73,7 @@ final class AIInfoExtractor implements InfoProviderInterface public function isActive(): bool { - return $this->settings->platform !== null && $this->settings->model !== null && $this->settings->model !== ''; + return $this->settings->platform !== null && $this->settings->model !== ''; } public function searchByKeyword(string $keyword): array @@ -171,7 +171,7 @@ final class AIInfoExtractor implements InfoProviderInterface $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); //'openai/gpt-5-mini' - $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ + $result = $aiPlatform->invoke($this->settings->model, $input, [ 'response_format' => [ 'type' => 'json_schema', 'json_schema' => $this->jsonSchemaConverter->getJSONSchema(), diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php index 732eb597..0e99d6fc 100644 --- a/src/Settings/AISettings/AISettings.php +++ b/src/Settings/AISettings/AISettings.php @@ -29,7 +29,7 @@ use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Translation\TranslatableMessage as TM; -#[Settings(label: new TM("settings.ai"))] +#[Settings(label: new TM("settings.ai"), description: "settings.ai.help")] #[SettingsIcon("fa-brain")] class AISettings { diff --git a/src/Settings/AISettings/LMStudioSettings.php b/src/Settings/AISettings/LMStudioSettings.php index 627961a9..ea6b9681 100644 --- a/src/Settings/AISettings/LMStudioSettings.php +++ b/src/Settings/AISettings/LMStudioSettings.php @@ -33,8 +33,8 @@ use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Translation\TranslatableMessage as TM; -#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] -#[SettingsIcon("fa-robot")] +#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.openrouter"), description: "settings.ai.lmstudio.help")] +#[SettingsIcon("fa-brain")] class LMStudioSettings implements AIPlatformSettingsInterface { use SettingsTrait; diff --git a/src/Settings/AISettings/OpenRouterSettings.php b/src/Settings/AISettings/OpenRouterSettings.php index e083513a..7b96c1d9 100644 --- a/src/Settings/AISettings/OpenRouterSettings.php +++ b/src/Settings/AISettings/OpenRouterSettings.php @@ -33,7 +33,7 @@ use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Translation\TranslatableMessage as TM; #[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")] -#[SettingsIcon("fa-robot")] +#[SettingsIcon("fa-brain")] class OpenRouterSettings implements AIPlatformSettingsInterface { use SettingsTrait; diff --git a/src/Settings/InfoProviderSystem/AIExtractorSettings.php b/src/Settings/InfoProviderSystem/AIExtractorSettings.php index 876c687f..69b02637 100644 --- a/src/Settings/InfoProviderSystem/AIExtractorSettings.php +++ b/src/Settings/InfoProviderSystem/AIExtractorSettings.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace App\Settings\InfoProviderSystem; -use App\Form\Settings\AiModelsType; use App\Form\Settings\AiPlatformChoiceType; use App\Services\AI\AIPlatforms; use App\Settings\SettingsIcon; @@ -31,29 +30,32 @@ 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"))] -#[SettingsIcon("fa-plug")] +#[SettingsIcon("fa-robot")] class AIExtractorSettings { - private const MODEL_SELECTOR_LABEL = 'ai_extractor'; - use SettingsTrait; - #[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), - formType: AiPlatformChoiceType::class, formOptions: ['platform_selector_label' => self::MODEL_SELECTOR_LABEL], + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), description: new TM("settings.ips.ai_extractor.ai_platform.help"), + formType: AiPlatformChoiceType::class, + envVar: "string:PROVIDER_AI_EXTRACTOR_API_KEY", envVarMode: EnvVarMode::OVERWRITE )] public ?AIPlatforms $platform = null; - #[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.help"), - formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL, 'filter_capability' => Capability::OUTPUT_STRUCTURED], + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.description"), + envVar: "string:PROVIDER_AI_EXTRACTOR_MODEL", envVarMode: EnvVarMode::OVERWRITE )] - public ?string $model = null; + public string $model = 'z-ai/glm-4.7'; - #[SettingsParameter(label: new TM("settings.ips.ai_extractor.max_content_length"), - description: new TM("settings.ips.ai_extractor.max_content_length.description"), + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.enabled"), description: new TM("settings.ips.ai_extractor.enabled.description"), + envVar: "bool:PROVIDER_AI_EXTRACTOR_ENABLED", envVarMode: EnvVarMode::OVERWRITE + )] + public bool $enabled = false; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.max_content_length"), description: new TM("settings.ips.ai_extractor.max_content_length.description"), + envVar: "int:PROVIDER_AI_EXTRACTOR_MAX_CONTENT_LENGTH", envVarMode: EnvVarMode::OVERWRITE )] public int $maxContentLength = 50000; } diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index e16a6d69..4da88512 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -2780,7 +2780,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name - + part.table.si_value SI Value @@ -7218,13 +7218,13 @@ Element 1 -> Element 1.2 Subprojects - + project.info.total_build_price Total build price - + project.info.per_unit_price per unit @@ -7254,7 +7254,7 @@ Element 1 -> Element 1.2 Price - + project.bom.ext_price Extended Price @@ -10053,85 +10053,85 @@ Please note, that you can not impersonate a disabled user. If you try you will g When enabled, the datasheet field in KiCad will link to the actual PDF file (if found). When disabled, it will link to the Part-DB page instead. The Part-DB page link is always available as a separate "Part-DB URL" field. - + settings.misc.kicad_eda.editor.title KiCad autocomplete lists - + settings.misc.kicad_eda.editor.link Autocomplete settings - + settings.misc.kicad_eda.editor.description Configure whether KiCad autocomplete uses the autogenerated default lists or your custom override files. The custom files are editable here, while the default files are shown read-only for reference. - + settings.misc.kicad_eda.editor.footprints Footprints list - + settings.misc.kicad_eda.editor.footprints.help One entry per line. Used as autocomplete suggestions for KiCad footprint fields. - + settings.misc.kicad_eda.editor.symbols Symbols list - + settings.misc.kicad_eda.editor.symbols.help One entry per line. Used as autocomplete suggestions for KiCad symbol fields. - + settings.misc.kicad_eda.use_custom_list Use custom autocomplete lists - + settings.misc.kicad_eda.use_custom_list.help When enabled, KiCad autocomplete uses public/kicad/footprints_custom.txt and public/kicad/symbols_custom.txt instead of the autogenerated default files. - + settings.misc.kicad_eda.editor.custom_footprints Custom footprints list - + settings.misc.kicad_eda.editor.custom_symbols Custom symbols list - + settings.misc.kicad_eda.editor.default_footprints Default footprints list - + settings.misc.kicad_eda.editor.default_symbols Default symbols list - + settings.misc.kicad_eda.editor.default_files_help Autogenerated file shown for reference only. Changes must be made in the custom list. @@ -13067,41 +13067,5 @@ Buerklin-API Authentication server: Mapping error: Check if you have selected the right delimiter! - - - settings.ai - AI - - - - - settings.ai.openrouter - OpenRouter - - - - - settings.ai.lmstudio - LMStudio - - - - - settings.ips.ai_extractor.model - AI Model - - - - - settings.ips.ai_extractor.ai_platform - AI Platform - - - - - settings.ips.ai_extractor.model.help - The AI model that should be used for extraction. Must support structured output. - -