diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 9685847278..f77c8dce67 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,15 +14,12 @@ -- limitations under the License. -- -CREATE TABLE ai_settings ( +CREATE TABLE ai_model_settings ( id UUID NOT NULL PRIMARY KEY, created_time BIGINT NOT NULL, tenant_id UUID NOT NULL, version BIGINT NOT NULL DEFAULT 1, name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - provider_config JSONB NOT NULL, - model VARCHAR(255) NOT NULL, - model_config JSONB, - CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) + configuration JSONB NOT NULL, + CONSTRAINT ai_model_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 5fc7247061..a81e312bf6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiService; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -63,7 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -314,11 +314,11 @@ public class ActorSystemContext { @Autowired @Getter - private RuleEngineAiService aiService; + private RuleEngineAiModelService aiModelService; @Autowired @Getter - private AiSettingsService aiSettingsService; + private AiModelSettingsService aiModelSettingsService; @Autowired @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 9df08b7b87..5778aba4ef 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiService; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -77,7 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -1016,13 +1016,13 @@ public class DefaultTbContext implements TbContext { } @Override - public RuleEngineAiService getAiService() { - return mainCtx.getAiService(); + public RuleEngineAiModelService getAiModelService() { + return mainCtx.getAiModelService(); } @Override - public AiSettingsService getAiSettingsService() { - return mainCtx.getAiSettingsService(); + public AiModelSettingsService getAiModelSettingsService() { + return mainCtx.getAiModelSettingsService(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java similarity index 65% rename from application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java rename to application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java index 530fc1e56a..891f6b176c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiSettingsController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java @@ -26,9 +26,9 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.service.security.permission.Operation; @@ -47,70 +47,70 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERT import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; @RestController -@RequestMapping("/api/ai-settings") -public class AiSettingsController extends BaseController { +@RequestMapping("/api/ai-model-settings") +public class AiModelSettingsController extends BaseController { - private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name", "provider", "model"); + private static final Set ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name"); @ApiOperation( - value = "Create or update AI settings (saveAiSettings)", - notes = "Creates or updates an AI settings record.\n\n" + + value = "Create or update AI model settings (saveAiModelSettings)", + notes = "Creates or updates an AI model settings record.\n\n" + "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" + "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + - "Tenant ID for the AI settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + + "Tenant ID for the AI model settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @PostMapping - public AiSettings saveAiSettings(@RequestBody AiSettings aiSettings) throws ThingsboardException { + public AiModelSettings saveAiModelSettings(@RequestBody AiModelSettings settings) throws ThingsboardException { var user = getCurrentUser(); - aiSettings.setTenantId(user.getTenantId()); - checkEntity(aiSettings.getId(), aiSettings, Resource.AI_SETTINGS); - return tbAiSettingsService.save(aiSettings, user); + settings.setTenantId(user.getTenantId()); + checkEntity(settings.getId(), settings, Resource.AI_MODEL_SETTINGS); + return tbAiModelSettingsService.save(settings, user); } @ApiOperation( - value = "Get AI settings by ID (getAiSettingsById)", - notes = "Fetches an AI settings record by its `id`." + + value = "Get AI model settings by ID (getAiModelSettingsById)", + notes = "Fetches an AI model settings record by its `id`." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/{aiSettingsId}") - public AiSettings getAiSettingsById( + @GetMapping("/{aiModelSettingsId}") + public AiModelSettings getAiModelSettingsById( @Parameter( - description = "ID of the AI settings record", + description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiSettingsId") UUID aiSettingsUuid + @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid ) throws ThingsboardException { - return checkAiSettingsId(new AiSettingsId(aiSettingsUuid), Operation.READ); + return checkAiModelSettingsId(new AiModelSettingsId(aiModelSettingsUuid), Operation.READ); } @ApiOperation( - value = "Get AI settings (getAiSettings)", - notes = "Returns a page of AI settings. " + + value = "Get AI model settings (getAiModelSettings)", + notes = "Returns a page of AI model settings. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @GetMapping - public PageData getAiSettings( + public PageData getAiModelSettings( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, @Parameter(description = AI_SETTINGS_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "model"})) + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder ) throws ThingsboardException { var user = getCurrentUser(); - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.READ); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.READ); validateSortProperty(sortProperty); var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return aiSettingsService.findAiSettingsByTenantId(user.getTenantId(), pageLink); + return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink); } private static void validateSortProperty(String sortProperty) { @@ -120,31 +120,31 @@ public class AiSettingsController extends BaseController { } @ApiOperation( - value = "Delete AI settings by ID (deleteAiSettingsById)", - notes = "Deletes the AI settings record by its `id`. " + + value = "Delete AI model settings by ID (deleteAiModelSettingsById)", + notes = "Deletes the AI model settings record by its `id`. " + "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + "If no such record exists, the endpoint returns `false`." + TENANT_AUTHORITY_PARAGRAPH ) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/{aiSettingsId}") - public boolean deleteAiSettingsById( + @DeleteMapping("/{aiModelSettingsId}") + public boolean deleteAiModelSettingsById( @Parameter( - description = "ID of the AI settings record", + description = "ID of the AI model settings record", required = true, example = "de7900d4-30e2-11f0-9cd2-0242ac120002" ) - @PathVariable("aiSettingsId") UUID aiSettingsUuid + @PathVariable("aiModelSettingsId") UUID aiModelSettingsUuid ) throws ThingsboardException { var user = getCurrentUser(); - var aiSettingsId = new AiSettingsId(aiSettingsUuid); - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE); - Optional aiSettingsOpt = aiSettingsService.findAiSettingsByTenantIdAndId(user.getTenantId(), aiSettingsId); - if (aiSettingsOpt.isEmpty()) { + var settingsId = new AiModelSettingsId(aiModelSettingsUuid); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE); + Optional toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId); + if (toDelete.isEmpty()) { return false; } - accessControlService.checkPermission(user, Resource.AI_SETTINGS, Operation.DELETE, aiSettingsId, aiSettingsOpt.get()); - return tbAiSettingsService.delete(aiSettingsOpt.get(), user); + accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE, settingsId, toDelete.get()); + return tbAiModelSettingsService.delete(toDelete.get(), user); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 26ff7c1a94..179327f73b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -63,7 +63,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -78,7 +78,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -131,7 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -177,7 +177,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; -import org.thingsboard.server.service.entitiy.ai.TbAiSettingsService; +import org.thingsboard.server.service.entitiy.ai.TbAiModelSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -378,10 +378,10 @@ public abstract class BaseController { protected CalculatedFieldService calculatedFieldService; @Autowired - protected AiSettingsService aiSettingsService; + protected AiModelSettingsService aiModelSettingsService; @Autowired - protected TbAiSettingsService tbAiSettingsService; + protected TbAiModelSettingsService tbAiModelSettingsService; @Value("${server.log_controller_error_stack_trace}") @Getter @@ -691,8 +691,8 @@ public abstract class BaseController { case CALCULATED_FIELD: checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); return; - case AI_SETTINGS: - checkAiSettingsId(new AiSettingsId(entityId.getId()), operation); + case AI_MODEL_SETTINGS: + checkAiModelSettingsId(new AiModelSettingsId(entityId.getId()), operation); return; default: checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); @@ -894,8 +894,8 @@ public abstract class BaseController { return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, operation); } - AiSettings checkAiSettingsId(AiSettingsId aiSettingsId, Operation operation) throws ThingsboardException { - return checkEntityId(aiSettingsId, (tenantId, id) -> aiSettingsService.findAiSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); + AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException { + return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); } protected I emptyId(EntityType entityType) { diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java new file mode 100644 index 0000000000..728e46e922 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import dev.langchain4j.model.chat.ChatModel; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineAiModelService; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; + +@Service +@RequiredArgsConstructor +class AiModelServiceImpl implements RuleEngineAiModelService { + + private final Langchain4jChatModelConfigurer chatModelConfigurer; + + @Override + public > ChatModel configureChatModel(AiChatModel chatModel) { + return chatModel.configure(chatModelConfigurer); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java deleted file mode 100644 index eeddb5e6d2..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.service.ai; - -import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; -import dev.langchain4j.model.mistralai.MistralAiChatModel; -import dev.langchain4j.model.openai.OpenAiChatModel; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.thingsboard.rule.engine.api.RuleEngineAiService; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.model.GoogleAiGeminiChatModelConfig; -import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig; -import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ai.AiSettingsService; - -import java.time.Duration; -import java.util.NoSuchElementException; -import java.util.Optional; - -@Service -@RequiredArgsConstructor -class AiServiceImpl implements RuleEngineAiService { - - private final AiSettingsService aiSettingsService; - - @Override - public ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsService.findAiSettingsById(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { - throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); - } - var aiSettings = aiSettingsOpt.get(); - return configureChatModel(aiSettings.getProviderConfig(), aiSettings.getModelConfig()); - } - - @Override - public ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig) { - return switch (providerConfig.getProvider()) { - case OPENAI -> { - var modelBuilder = OpenAiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof OpenAiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - case MISTRAL_AI -> { - var modelBuilder = MistralAiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof MistralAiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - case GOOGLE_AI_GEMINI -> { - var modelBuilder = GoogleAiGeminiChatModel.builder() - .apiKey(providerConfig.getApiKey()) - .modelName(modelConfig.getModel()); - - if (modelConfig instanceof GoogleAiGeminiChatModelConfig config) { - modelBuilder.temperature(config.getTemperature()); - if (config.getTimeoutSeconds() != null) { - modelBuilder.timeout(Duration.ofSeconds(config.getTimeoutSeconds())); - } - modelBuilder.maxRetries(config.getMaxRetries()); - } - - yield modelBuilder.build(); - } - }; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java new file mode 100644 index 0000000000..f66f31400b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.ai; + +import dev.langchain4j.model.chat.ChatModel; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; + +import java.time.Duration; + +@Component +public class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { + + @Override + public ChatModel configureChatModel(OpenAiChatModel chatModel) { + OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.openai.OpenAiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { + GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(MistralAiChatModel chatModel) { + MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); + return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() + .apiKey(chatModel.providerConfig().apiKey()) + .modelName(chatModel.modelId()) + .temperature(modelConfig.temperature()) + .timeout(toDuration(modelConfig.timeoutSeconds())) + .maxRetries(modelConfig.maxRetries()) + .build(); + } + + private static Duration toDuration(Integer timeoutSeconds) { + return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index dc3d1a74f7..8bbed875fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -112,7 +112,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_SETTINGS == entityType) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -226,7 +226,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE, AI_SETTINGS: + case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index e0c24a13bc..5fca13380c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener( fallbackExecution = true, - condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_SETTINGS" + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index d0d9376f12..5823632b63 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -96,7 +96,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -158,7 +158,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_SETTINGS -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, AI_MODEL_SETTINGS -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java similarity index 60% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java index 08e663633d..5a41d83eed 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java @@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -30,22 +30,22 @@ import static java.util.Objects.requireNonNullElseGet; @Service @TbCoreComponent @RequiredArgsConstructor -class DefaultTbAiSettingsService extends AbstractTbEntityService implements TbAiSettingsService { +class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService { - private final AiSettingsService aiSettingsService; + private final AiModelSettingsService aiModelSettingsService; @Override - public AiSettings save(AiSettings aiSettings, User user) { - var actionType = aiSettings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + public AiModelSettings save(AiModelSettings settings, User user) { + var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; var tenantId = user.getTenantId(); - aiSettings.setTenantId(tenantId); + settings.setTenantId(tenantId); - AiSettings savedSettings; + AiModelSettings savedSettings; try { - savedSettings = aiSettingsService.save(aiSettings); + savedSettings = aiModelSettingsService.save(settings); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(aiSettings.getId(), () -> emptyId(EntityType.AI_SETTINGS)), aiSettings, actionType, user, e); + logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); throw e; } @@ -55,22 +55,22 @@ class DefaultTbAiSettingsService extends AbstractTbEntityService implements TbAi } @Override - public boolean delete(AiSettings aiSettings, User user) { + public boolean delete(AiModelSettings settings, User user) { var actionType = ActionType.DELETED; var tenantId = user.getTenantId(); - var aiSettingsId = aiSettings.getId(); + var settingsId = settings.getId(); boolean deleted; try { - deleted = aiSettingsService.deleteByTenantIdAndId(tenantId, aiSettingsId); + deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, e, aiSettingsId.toString()); + logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, e, settingsId.toString()); throw e; } if (deleted) { - logEntityActionService.logEntityAction(tenantId, aiSettingsId, aiSettings, actionType, user, aiSettingsId.toString()); + logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString()); } return deleted; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java index 399a3b2bdd..0d66c171a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java @@ -16,12 +16,12 @@ package org.thingsboard.server.service.entitiy.ai; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; -public interface TbAiSettingsService { +public interface TbAiModelSettingsService { - AiSettings save(AiSettings aiSettings, User user); + AiModelSettings save(AiModelSettings settings, User user); - boolean delete(AiSettings aiSettings, User user); + boolean delete(AiModelSettings settings, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index aa27b4592b..2a2a39c4d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -590,7 +590,7 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.ENTITY_VIEW, EntityType.NOTIFICATION_RULE, EntityType.CALCULATED_FIELD, - EntityType.AI_SETTINGS, + EntityType.AI_MODEL_SETTINGS, EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE) diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 55b7577ebe..cdbd2bccec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -52,7 +52,7 @@ public enum Resource { EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, CALCULATED_FIELD(EntityType.CALCULATED_FIELD), - AI_SETTINGS(EntityType.AI_SETTINGS); + AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS); private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 990f29798c..49c4350761 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -58,7 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker); - put(Resource.AI_SETTINGS, aiSettingsPermissionChecker); + put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -149,7 +149,7 @@ public class TenantAdminPermissions extends AbstractPermissions { }; - private static final PermissionChecker aiSettingsPermissionChecker = new PermissionChecker<>() { + private static final PermissionChecker aiModelSettingsPermissionChecker = new PermissionChecker<>() { @Override public boolean hasPermission(SecurityUser user, Operation operation) { @@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions { } @Override - public boolean hasPermission(SecurityUser user, Operation operation, AiSettingsId entityId, AiSettings entity) { + public boolean hasPermission(SecurityUser user, Operation operation, AiModelSettingsId entityId, AiModelSettings entity) { return user.getTenantId().equals(entity.getTenantId()); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index dd41b13de0..f79a387b58 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -657,9 +657,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled - aiSettings: - timeToLiveInMinutes: "${CACHE_SPECS_AI_SETTINGS_TTL:1440}" # AI settings cache TTL - maxSize: "${CACHE_SPECS_AI_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiModelSettings: + timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL + maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -875,7 +875,7 @@ audit-log: "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. - "ai_settings": "${AUDIT_LOG_MASK_AI_SETTINGS:W}" # AI settings logging levels. + "ai_model_settings": "${AUDIT_LOG_MASK_AI_MODEL_SETTINGS:W}" # AI model settings logging levels. sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java similarity index 55% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java index 83a66de876..09219f238e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java @@ -16,8 +16,8 @@ package org.thingsboard.server.dao.ai; import com.google.common.util.concurrent.FluentFuture; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Optional; -public interface AiSettingsService extends EntityDaoService { +public interface AiModelSettingsService extends EntityDaoService { - AiSettings save(AiSettings aiSettings); + AiModelSettings save(AiModelSettings settings); - Optional findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); - PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); - Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); - FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId); + FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 648f3d1086..beb67c6b19 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -112,6 +112,10 @@ leshan-core compile + + dev.langchain4j + langchain4j + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 4b103d769a..c5df7c10c0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -37,7 +37,7 @@ public final class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; - public static final String AI_SETTINGS_CACHE = "aiSettings"; + public static final String AI_MODEL_SETTINGS_CACHE = "aiModelSettings"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 89bc456fd9..868b36f73b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -64,10 +64,10 @@ public enum EntityType { MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), - AI_SETTINGS(41, "ai_settings") { + AI_MODEL_SETTINGS(41, "ai_model_settings") { @Override public String getNormalName() { - return "AI settings"; + return "AI model settings"; } }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java similarity index 52% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java index d38ac45992..df1dbe4d7e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java @@ -24,10 +24,8 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasVersion; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProvider; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.io.Serial; @@ -36,7 +34,7 @@ import java.io.Serial; @Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public final class AiSettings extends BaseData implements HasTenantId, HasVersion, HasName { +public final class AiModelSettings extends BaseData implements HasTenantId, HasVersion, HasName { @Serial private static final long serialVersionUID = 9017108678716011604L; @@ -44,7 +42,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "JSON object representing the ID of the tenant associated with these AI settings", + description = "JSON object representing the ID of the tenant associated with these AI model settings", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" ) TenantId tenantId; @@ -52,7 +50,7 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "Version of the AI settings; increments automatically whenever the settings are changed", + description = "Version of the AI model settings; increments automatically whenever the settings are changed", example = "7", defaultValue = "1" ) @@ -61,49 +59,21 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Human-readable name of the AI settings; must be unique within the scope of the tenant", + description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant", example = "Default AI Settings" ) String name; - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "OPENAI", - allowableValues = {"OPENAI", "GOOGLE_AI_GEMINI", "MISTRAL_AI"}, - type = "string" - ) - AiProvider provider; - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Configuration specific to the AI provider" - ) - AiProviderConfig providerConfig; - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - example = "gpt-4o-mini" - ) - String model; - @Schema( requiredMode = Schema.RequiredMode.NOT_REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = """ - Optional configuration specific to the AI model. - If provided, it must be one of the known `AiModelConfig` subtypes and any settings - you specify will override the model’s defaults; if omitted, the model will run with its built-in defaults.""" + description = "Configuration of the AI model" ) - AiModelConfig modelConfig; + AiModel configuration; - public AiSettings() {} + public AiModelSettings() {} - public AiSettings(AiSettingsId id) { + public AiModelSettings(AiModelSettingsId id) { super(id); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java new file mode 100644 index 0000000000..686c1f4fcf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "modelId", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "gpt-4o"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "gemini-2.5-flash"), + @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "mistral-medium-latest") +}) +public interface AiModel> { + + AiProviderConfig providerConfig(); + + AiModelType modelType(); + + String modelId(); + + C modelConfig(); + + AiModel withModelConfig(C config); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 779b75812f..b07007c301 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,42 +15,4 @@ */ package org.thingsboard.server.common.data.ai.model; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "model", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o"), - @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o-mini"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "gemini-2.0-flash"), - @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "mistral-medium-latest") -}) -public abstract class AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model" - ) - private String model; - - public abstract Integer getTimeoutSeconds(); - - public abstract void setTimeoutSeconds(Integer timeoutSeconds); - - public abstract Integer getMaxRetries(); - - public abstract void setMaxRetries(Integer timeoutSeconds); - - -} +public interface AiModelConfig> {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java new file mode 100644 index 0000000000..d6299cf5e6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model; + +public enum AiModelType { + + CHAT + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java deleted file mode 100644 index 1d85115f31..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/GoogleAiGeminiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "GoogleAiGeminiChatModelConfig", - description = "Configuration for Google AI Gemini chat models" -) -public final class GoogleAiGeminiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = "gemini-2.0-flash", - example = "gemini-2.0-flash" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for establishing HTTP connection" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java deleted file mode 100644 index d333eb6283..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/MistralAiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "MistralAiChatModelConfig", - description = "Configuration for Mistral AI chat models" -) -public final class MistralAiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = "mistral-medium-latest", - example = "mistral-medium-latest" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for the entire HTTP call: applied to connect, read, and write operations" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java deleted file mode 100644 index accf5c92ec..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/OpenAiChatModelConfig.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.common.data.ai.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@Data -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -@Schema( - name = "OpenAiChatModelConfig", - description = "Configuration for OpenAI chat models" -) -public final class OpenAiChatModelConfig extends AiModelConfig { - - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Identifier of the AI model", - allowableValues = {"gpt-4o", "gpt-4o-mini"}, - example = "gpt-4o" - ) - public String getModel() { - return super.getModel(); - } - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)", - example = "0.7" - ) - private Double temperature; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Timeout (in seconds) for both establishing HTTP connection and receiving a response" - ) - private Integer timeoutSeconds; - - @Schema( - accessMode = Schema.AccessMode.READ_WRITE, - description = "Maximum number of times to retry an LLM call upon exception (except for non-retriable ones like authentication or invalid request errors)" - ) - private Integer maxRetries; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java new file mode 100644 index 0000000000..17f79d93e2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.ai.model.AiModelType; + +public sealed interface AiChatModel> extends AiModel + permits OpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel { + + ChatModel configure(Langchain4jChatModelConfigurer configurer); + + @Override + default AiModelType modelType() { + return AiModelType.CHAT; + } + + @Override + C modelConfig(); + + @Override + AiChatModel withModelConfig(C config); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java new file mode 100644 index 0000000000..565923cf6c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import org.thingsboard.server.common.data.ai.model.AiModelConfig; + +public sealed interface AiChatModelConfig> extends AiModelConfig + permits OpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, MistralAiChatModel.Config { + + Double temperature(); + + Integer timeoutSeconds(); + + Integer maxRetries(); + + C withTemperature(Double temperature); + + C withTimeoutSeconds(Integer timeoutSeconds); + + C withMaxRetries(Integer maxRetries); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java new file mode 100644 index 0000000000..875262abb6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; + +public record GoogleAiGeminiChatModel( + GoogleAiGeminiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public GoogleAiGeminiChatModel withModelConfig(GoogleAiGeminiChatModel.Config config) { + return new GoogleAiGeminiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java new file mode 100644 index 0000000000..87d317eb0b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; + +public interface Langchain4jChatModelConfigurer { + + ChatModel configureChatModel(OpenAiChatModel chatModel); + + ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); + + ChatModel configureChatModel(MistralAiChatModel chatModel); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java new file mode 100644 index 0000000000..413d2b93a8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; + +public record MistralAiChatModel( + MistralAiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public MistralAiChatModel withModelConfig(Config config) { + return new MistralAiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java new file mode 100644 index 0000000000..0d5031d512 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; + +public record OpenAiChatModel( + OpenAiProviderConfig providerConfig, + String modelId, + Config modelConfig +) implements AiChatModel { + + public record Config( + Double temperature, + Integer timeoutSeconds, + Integer maxRetries + ) implements AiChatModelConfig { + + @Override + public OpenAiChatModel.Config withTemperature(Double temperature) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public OpenAiChatModel.Config withTimeoutSeconds(Integer timeoutSeconds) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + @Override + public OpenAiChatModel.Config withMaxRetries(Integer maxRetries) { + return new Config(temperature, timeoutSeconds, maxRetries); + } + + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public OpenAiChatModel withModelConfig(OpenAiChatModel.Config config) { + return new OpenAiChatModel(providerConfig, modelId, config); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index f350ed75ec..b7c594ba60 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -17,32 +17,22 @@ package org.thingsboard.server.common.data.ai.provider; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.NoArgsConstructor; -@Data -@NoArgsConstructor @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXISTING_PROPERTY, - property = "provider", - visible = true + include = JsonTypeInfo.As.PROPERTY, + property = "provider" ) @JsonSubTypes({ @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI") }) -public abstract class AiProviderConfig { +public sealed interface AiProviderConfig + permits OpenAiProviderConfig, GoogleAiGeminiProviderConfig, MistralAiProviderConfig { - public abstract AiProvider getProvider(); + AiProvider provider(); - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "API key for authenticating with the AI provider" - ) - private String apiKey; + String apiKey(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java index 9ca8643173..0bb9d21b52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record GoogleAiGeminiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "GoogleAiGeminiProviderConfig", - description = "Configuration for the Google AI Gemini provider" -) -public final class GoogleAiGeminiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_AI_GEMINI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "GOOGLE_AI_GEMINI", - allowableValues = "GOOGLE_AI_GEMINI", - type = "string" - ) - private AiProvider provider = AiProvider.GOOGLE_AI_GEMINI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java index bc17f34220..45e3b68800 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record MistralAiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "MistralAiProviderConfig", - description = "Configuration for the Mistral AI provider" -) -public final class MistralAiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.MISTRAL_AI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "MISTRAL_AI", - allowableValues = "MISTRAL_AI", - type = "string" - ) - private AiProvider provider = AiProvider.MISTRAL_AI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 36ac8b4b35..0536d1176e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -15,26 +15,16 @@ */ package org.thingsboard.server.common.data.ai.provider; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; +public record OpenAiProviderConfig(String apiKey) implements AiProviderConfig { -@Data -@EqualsAndHashCode(callSuper = true) -@Schema( - name = "OpenAiProviderConfig", - description = "Configuration for the OpenAI provider" -) -public final class OpenAiProviderConfig extends AiProviderConfig { + @Override + public AiProvider provider() { + return AiProvider.OPENAI; + } - @Schema( - requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.READ_WRITE, - description = "Name of the AI provider", - example = "OPENAI", - allowableValues = "OPENAI", - type = "string" - ) - private AiProvider provider = AiProvider.OPENAI; + @Override + public String apiKey() { + return apiKey; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java index 8aac27051a..83b4fefab2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java @@ -23,29 +23,29 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; -public final class AiSettingsId extends UUIDBased implements EntityId { +public final class AiModelSettingsId extends UUIDBased implements EntityId { @Serial private static final long serialVersionUID = 3021036138554389754L; @JsonCreator - public AiSettingsId(@JsonProperty("id") UUID id) { + public AiModelSettingsId(@JsonProperty("id") UUID id) { super(id); } @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - description = "Entity type of the AI settings", - example = "AI_SETTINGS", - allowableValues = "AI_SETTINGS" + description = "Entity type of the AI model settings", + example = "AI_MODEL_SETTINGS", + allowableValues = "AI_MODEL_SETTINGS" ) public EntityType getEntityType() { - return EntityType.AI_SETTINGS; + return EntityType.AI_MODEL_SETTINGS; } - public static AiSettingsId fromString(String uuid) { - return new AiSettingsId(UUID.fromString(uuid)); + public static AiModelSettingsId fromString(String uuid) { + return new AiModelSettingsId(UUID.fromString(uuid)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index fcdf1e0a1b..0738ce45ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -84,7 +84,7 @@ public class EntityIdFactory { case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); - case AI_SETTINGS -> new AiSettingsId(uuid); + case AI_MODEL_SETTINGS -> new AiModelSettingsId(uuid); default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); }; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index e5a94c21fc..40f154d276 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -63,7 +63,7 @@ enum EntityTypeProto { MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; - AI_SETTINGS = 41; + AI_MODEL_SETTINGS = 41; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java similarity index 68% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java index d6cad8bf16..16ce64256b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.util.Set; -record AiSettingsCacheEvictEvent(Set keys) { +record AiModelSettingsCacheEvictEvent(Set keys) { - static AiSettingsCacheEvictEvent of(TenantId tenantId, AiSettingsId aiSettingsId) { - return new AiSettingsCacheEvictEvent(Set.of(AiSettingsCacheKey.of(tenantId, aiSettingsId))); + static AiModelSettingsCacheEvictEvent of(TenantId tenantId, AiModelSettingsId settingsId) { + return new AiModelSettingsCacheEvictEvent(Set.of(AiModelSettingsCacheKey.of(tenantId, settingsId))); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java similarity index 70% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java index d732c08fc8..9d73289be7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java @@ -17,22 +17,22 @@ package org.thingsboard.server.dao.ai; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.server.cache.VersionedCacheKey; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; import static java.util.Objects.requireNonNull; -record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements VersionedCacheKey { +record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements VersionedCacheKey { - AiSettingsCacheKey { + AiModelSettingsCacheKey { requireNonNull(tenantId); - requireNonNull(aiSettingsId); + requireNonNull(settingsId); } - static AiSettingsCacheKey of(TenantId tenantId, AiSettingsId aiSettingsId) { - return new AiSettingsCacheKey(tenantId.getId(), aiSettingsId.getId()); + static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) { + return new AiModelSettingsCacheKey(tenantId.getId(), settingsId.getId()); } @Override @@ -43,7 +43,7 @@ record AiSettingsCacheKey(UUID tenantId, UUID aiSettingsId) implements Versioned @NonNull @Override public String toString() { - return /* cache name */ "_" + tenantId + "_" + aiSettingsId; + return /* cache name */ "_" + tenantId + "_" + settingsId; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java similarity index 75% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java index f006c0a757..d758cb21c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java @@ -20,14 +20,14 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.VersionedCaffeineTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; -@Component("AiSettingsCache") +@Component("AiModelSettingsCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -class AiSettingsCaffeineCache extends VersionedCaffeineTbCache { +class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache { - AiSettingsCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.AI_SETTINGS_CACHE); + AiModelSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java similarity index 64% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java index 94a096d679..ae47af0db2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java @@ -15,22 +15,22 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Optional; -public interface AiSettingsDao extends Dao, TenantEntityDao { +public interface AiModelSettingsDao extends Dao, TenantEntityDao { - Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); - boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); int deleteByTenantId(TenantId tenantId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java similarity index 73% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java index 0c1da26769..8674c522ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java @@ -23,14 +23,14 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbJsonRedisSerializer; import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; @Component("AiSettingsCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -class AiSettingsRedisCache extends VersionedRedisTbCache { +class AiModelSettingsRedisCache extends VersionedRedisTbCache { - AiSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.AI_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiSettings.class)); + AiModelSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_MODEL_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModelSettings.class)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java index 8950e3559f..2fa47bf60f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java @@ -22,8 +22,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -43,29 +43,29 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @RequiredArgsConstructor -class AiSettingsServiceImpl extends CachedVersionedEntityService implements AiSettingsService { +class AiModelSettingsServiceImpl extends CachedVersionedEntityService implements AiModelSettingsService { - private final DataValidator aiSettingsValidator; + private final DataValidator aiModelSettingsValidator; private final JpaExecutorService jpaExecutor; - private final AiSettingsDao aiSettingsDao; + private final AiModelSettingsDao aiModelSettingsDao; @Override @TransactionalEventListener - public void handleEvictEvent(AiSettingsCacheEvictEvent event) { + public void handleEvictEvent(AiModelSettingsCacheEvictEvent event) { cache.evict(event.keys()); } @Override @Transactional - public AiSettings save(AiSettings aiSettings) { - AiSettings oldSettings = aiSettingsValidator.validate(aiSettings, AiSettings::getTenantId); + public AiModelSettings save(AiModelSettings settings) { + AiModelSettings oldSettings = aiModelSettingsValidator.validate(settings, AiModelSettings::getTenantId); - AiSettings savedSettings; + AiModelSettings savedSettings; try { - savedSettings = aiSettingsDao.saveAndFlush(aiSettings.getTenantId(), aiSettings); + savedSettings = aiModelSettingsDao.saveAndFlush(settings.getTenantId(), settings); } catch (Exception e) { - checkConstraintViolation(e, "ai_settings_name_unq_key", "AI settings record with such name already exists!"); + checkConstraintViolation(e, "ai_model_settings_name_unq_key", "AI model settings with such name already exist!"); throw e; } @@ -78,65 +78,65 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId) { - return Optional.ofNullable(aiSettingsDao.findById(tenantId, aiSettingsId.getId())); + public Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) { + return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId())); } @Override - public PageData findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { validatePageLink(pageLink); - return aiSettingsDao.findAllByTenantId(tenantId, pageLink); + return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); } @Override - public Optional findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - var cacheKey = AiSettingsCacheKey.of(tenantId, aiSettingsId); - return Optional.ofNullable(cache.get(cacheKey, () -> aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId).orElse(null))); + public Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + var cacheKey = AiModelSettingsCacheKey.of(tenantId, settingsId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId).orElse(null))); } @Override - public FluentFuture> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId) { - return FluentFuture.from(jpaExecutor.submit(() -> findAiSettingsByTenantIdAndId(tenantId, aiSettingsId))); + public FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId) { + return FluentFuture.from(jpaExecutor.submit(() -> findAiModelSettingsByTenantIdAndId(tenantId, settingsId))); } @Override @Transactional - public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return deleteByTenantIdAndIdInternal(tenantId, aiSettingsId); + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return deleteByTenantIdAndIdInternal(tenantId, settingsId); } @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return findAiSettingsByTenantIdAndId(tenantId, (AiSettingsId) entityId) - .map(aiSettings -> aiSettings); // necessary to cast to HasId + return findAiModelSettingsByTenantIdAndId(tenantId, (AiModelSettingsId) entityId) + .map(settings -> settings); // necessary to cast to HasId } @Override public long countByTenantId(TenantId tenantId) { - return aiSettingsDao.countByTenantId(tenantId); + return aiModelSettingsDao.countByTenantId(tenantId); } @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteByTenantIdAndIdInternal(tenantId, new AiSettingsId(id.getId())); + deleteByTenantIdAndIdInternal(tenantId, new AiModelSettingsId(id.getId())); } - private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiSettingsId aiSettingsId) { - Optional aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId); - if (aiSettingsOpt.isEmpty()) { + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelSettingsId settingsId) { + Optional toDeleteOpt = aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId); + if (toDeleteOpt.isEmpty()) { return false; } - boolean deleted = aiSettingsDao.deleteByTenantIdAndId(tenantId, aiSettingsId); + boolean deleted = aiModelSettingsDao.deleteByTenantIdAndId(tenantId, settingsId); if (deleted) { - publishDeleteEvent(aiSettingsOpt.get()); - publishEvictEvent(AiSettingsCacheEvictEvent.of(tenantId, aiSettingsId)); + publishDeleteEvent(toDeleteOpt.get()); + publishEvictEvent(AiModelSettingsCacheEvictEvent.of(tenantId, settingsId)); } return deleted; } @@ -144,23 +144,23 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService deletedSettings = aiSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); - if (deletedSettings.isEmpty()) { + List toDelete = aiModelSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData(); + if (toDelete.isEmpty()) { return; } - aiSettingsDao.deleteByTenantId(tenantId); + aiModelSettingsDao.deleteByTenantId(tenantId); - Set cacheKeys = Sets.newHashSetWithExpectedSize(deletedSettings.size()); - deletedSettings.forEach(settings -> { + Set cacheKeys = Sets.newHashSetWithExpectedSize(toDelete.size()); + toDelete.forEach(settings -> { publishDeleteEvent(settings); - cacheKeys.add(AiSettingsCacheKey.of(settings.getTenantId(), settings.getId())); + cacheKeys.add(AiModelSettingsCacheKey.of(settings.getTenantId(), settings.getId())); }); - publishEvictEvent(new AiSettingsCacheEvictEvent(cacheKeys)); + publishEvictEvent(new AiModelSettingsCacheEvictEvent(cacheKeys)); } - private void publishDeleteEvent(AiSettings settings) { + private void publishDeleteEvent(AiModelSettings settings) { eventPublisher.publishEvent(DeleteEntityEvent.builder() .tenantId(settings.getTenantId()) .entityId(settings.getId()) @@ -170,7 +170,7 @@ class AiSettingsServiceImpl extends CachedVersionedEntityService skippedEntities = EnumSet.of( EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_SETTINGS + EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL_SETTINGS ); @TransactionalEventListener(fallbackExecution = true) // after transaction commit diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ecd93df6fd..87c432268a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -740,15 +740,12 @@ public class ModelConstants { public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; /** - * AI settings constants. + * AI model settings constants. */ - public static final String AI_SETTINGS_TABLE_NAME = "ai_settings"; - public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; - public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; - public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider"; - public static final String AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME = "provider_config"; - public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; - public static final String AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME = "model_config"; + public static final String AI_MODEL_SETTINGS_TABLE_NAME = "ai_model_settings"; + public static final String AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; + public static final String AI_MODEL_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; + public static final String AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java similarity index 53% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java index e2775e3cbb..06774f5a7a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java @@ -18,23 +18,20 @@ package org.thingsboard.server.dao.model.sql; import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxy; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProvider; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.model.AiModel; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -42,53 +39,40 @@ import java.util.UUID; @Setter @ToString @Entity -@Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) -public class AiSettingsEntity extends BaseVersionedEntity { +@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME) +public class AiModelSettingsEntity extends BaseVersionedEntity { - @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") + public static final Map COLUMN_MAP = Map.of( + "createdTime", "created_time" + ); + + @Column(name = ModelConstants.AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") private UUID tenantId; - @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) + @Column(name = ModelConstants.AI_MODEL_SETTINGS_NAME_COLUMN_NAME, nullable = false) private String name; - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false) - private AiProvider provider; - @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") - private AiProviderConfig providerConfig; + @Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiModel configuration; - @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) - private String model; + public AiModelSettingsEntity() {} - @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME, columnDefinition = "JSONB") - private AiModelConfig modelConfig; - - public AiSettingsEntity() {} - - public AiSettingsEntity(AiSettings aiSettings) { - super(aiSettings); - tenantId = getTenantUuid(aiSettings.getTenantId()); - name = aiSettings.getName(); - provider = aiSettings.getProvider(); - providerConfig = aiSettings.getProviderConfig(); - model = aiSettings.getModel(); - modelConfig = aiSettings.getModelConfig(); + public AiModelSettingsEntity(AiModelSettings aiModelSettings) { + super(aiModelSettings); + tenantId = getTenantUuid(aiModelSettings.getTenantId()); + name = aiModelSettings.getName(); + configuration = aiModelSettings.getConfiguration(); } @Override - public AiSettings toData() { - var settings = new AiSettings(new AiSettingsId(id)); + public AiModelSettings toData() { + var settings = new AiModelSettings(new AiModelSettingsId(id)); settings.setCreatedTime(createdTime); settings.setVersion(version); settings.setTenantId(TenantId.fromUUID(tenantId)); settings.setName(name); - settings.setProvider(provider); - settings.setProviderConfig(providerConfig); - settings.setModel(model); - settings.setModelConfig(modelConfig); + settings.setConfiguration(configuration); return settings; } @@ -99,7 +83,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - AiSettingsEntity that = (AiSettingsEntity) o; + AiModelSettingsEntity that = (AiModelSettingsEntity) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java new file mode 100644 index 0000000000..6ed84e3c3e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiModelSettingsDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +class AiModelSettingsDataValidator extends DataValidator { + + private final TenantService tenantService; + private final AiModelSettingsDao aiModelSettingsDao; + + @Override + protected void validateCreate(TenantId tenantId, AiModelSettings settings) { + validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_MODEL_SETTINGS); + } + + @Override + protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { + Optional existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); + if (existing.isEmpty()) { + throw new DataValidationException("Cannot update non-existent AI model settings!"); + } + return existing.get(); + } + + @Override + protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) { + // ID validation + if (settings.getId() != null) { + if (settings.getUuidId() == null) { + throw new DataValidationException("AI model settings UUID should be specified!"); + } + if (settings.getId().isNullUid()) { + throw new DataValidationException("AI model settings UUID must not be the reserved null value!"); + } + } + + // tenant ID validation + if (settings.getTenantId() == null || settings.getTenantId().getId() == null) { + throw new DataValidationException("AI model settings should be assigned to tenant!"); + } + if (settings.getTenantId().isSysTenantId()) { + throw new DataValidationException("AI model settings cannot be assigned to the system tenant!"); + } + if (!tenantService.tenantExists(tenantId)) { + throw new DataValidationException("AI model settings reference a non-existent tenant!"); + } + + // name validation + validateString("AI model settings name", settings.getName()); + if (settings.getName().length() > 255) { + throw new DataValidationException("AI model settings name should be between 1 and 255 symbols!"); + } + + // model config validation + if (settings.getConfiguration() == null) { + throw new DataValidationException("AI model settings configuration should be specified!"); + } + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java deleted file mode 100644 index 24d4df23ee..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiSettingsDataValidator.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.validator; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ai.AiSettingsDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TenantService; - -import java.util.Objects; -import java.util.Optional; - -@Component -@RequiredArgsConstructor -class AiSettingsDataValidator extends DataValidator { - - private final TenantService tenantService; - private final AiSettingsDao aiSettingsDao; - - @Override - protected void validateCreate(TenantId tenantId, AiSettings aiSettings) { - validateNumberOfEntitiesPerTenant(tenantId, EntityType.AI_SETTINGS); - } - - @Override - protected AiSettings validateUpdate(TenantId tenantId, AiSettings aiSettings) { - Optional old = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettings.getId()); - if (old.isEmpty()) { - throw new DataValidationException("Cannot update non-existent AI settings!"); - } - return old.get(); - } - - @Override - protected void validateDataImpl(TenantId tenantId, AiSettings aiSettings) { - // ID validation - if (aiSettings.getId() != null) { - if (aiSettings.getUuidId() == null) { - throw new DataValidationException("AI settings UUID should be specified!"); - } - if (aiSettings.getId().isNullUid()) { - throw new DataValidationException("AI settings UUID must not be the reserved null value!"); - } - } - - // tenant ID validation - if (aiSettings.getTenantId() == null || aiSettings.getTenantId().getId() == null) { - throw new DataValidationException("AI settings should be assigned to tenant!"); - } - if (aiSettings.getTenantId().isSysTenantId()) { - throw new DataValidationException("AI settings cannot be assigned to the system tenant!"); - } - if (!tenantService.tenantExists(tenantId)) { - throw new DataValidationException("AI settings reference a non-existent tenant!"); - } - - // name validation - validateString("AI settings name", aiSettings.getName()); - if (aiSettings.getName().length() > 255) { - throw new DataValidationException("AI settings name should be between 1 and 255 symbols!"); - } - - // provider validation - if (aiSettings.getProvider() == null) { - throw new DataValidationException("AI provider should be specified!"); - } - - // provider config validation - if (aiSettings.getProviderConfig() == null) { - throw new DataValidationException("AI provider config should be specified!"); - } - if (aiSettings.getProviderConfig().getProvider() != aiSettings.getProvider()) { - throw new DataValidationException("AI provider configuration should match the selected AI provider!"); - } - validateString("AI provider API key", aiSettings.getProviderConfig().getApiKey()); - - // model identifier validation - validateString("AI model identifier", aiSettings.getModel()); - if (aiSettings.getModel().length() > 255) { - throw new DataValidationException("AI model identifier should be between 1 and 255 symbols!"); - } - - // model config validation - if (aiSettings.getModelConfig() != null) { - if (!Objects.equals(aiSettings.getModelConfig().getModel(), aiSettings.getModel())) { - throw new DataValidationException("AI model configuration should match the selected AI model!"); - } - } - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java similarity index 57% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java index a73569eac2..b645db8d66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java @@ -22,30 +22,32 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import org.thingsboard.server.dao.model.sql.AiSettingsEntity; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import java.util.Optional; import java.util.Set; import java.util.UUID; -public interface AiSettingsRepository extends JpaRepository { +interface AiModelSettingsRepository extends JpaRepository { - @Query("SELECT ai " + - "FROM AiSettingsEntity ai " + - "WHERE ai.tenantId = :tenantId " + - "AND (:textSearch IS NULL " + - "OR ilike(ai.name, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(ai.provider, CONCAT('%', :textSearch, '%')) = true " + - "OR ilike(ai.model, CONCAT('%', :textSearch, '%')) = true)") - Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); + @Query(nativeQuery = true, value = """ + SELECT * + FROM ai_model_settings ai_model + WHERE ai_model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR ai_model.name ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration -> 'providerConfig' ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (ai_model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') + """) + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - Optional findByTenantIdAndId(UUID tenantId, UUID id); + Optional findByTenantIdAndId(UUID tenantId, UUID id); long countByTenantId(UUID tenantId); @Transactional @Modifying - @Query("DELETE FROM AiSettingsEntity ai WHERE ai.id IN (:ids)") + @Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.id IN (:ids)") int deleteByIdIn(@Param("ids") Set ids); @Transactional @@ -53,7 +55,7 @@ public interface AiSettingsRepository extends JpaRepository ids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java index c56c29b835..e5d7df2ee9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiSettingsDao.java @@ -20,14 +20,14 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiSettings; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.ai.AiSettingsDao; -import org.thingsboard.server.dao.model.sql.AiSettingsEntity; +import org.thingsboard.server.dao.ai.AiModelSettingsDao; +import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; @@ -38,55 +38,55 @@ import java.util.UUID; @SqlDao @Component @RequiredArgsConstructor -class JpaAiSettingsDao extends JpaAbstractDao implements AiSettingsDao { +class JpaAiSettingsDao extends JpaAbstractDao implements AiModelSettingsDao { - private final AiSettingsRepository aiSettingsRepository; + private final AiModelSettingsRepository aiModelSettingsRepository; @Override - public Optional findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData); + public Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData); } @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(aiSettingsRepository.findByTenantId( - tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink)) + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( + tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink, AiModelSettingsEntity.COLUMN_MAP)) ); } @Override public Long countByTenantId(TenantId tenantId) { - return aiSettingsRepository.countByTenantId(tenantId.getId()); + return aiModelSettingsRepository.countByTenantId(tenantId.getId()); } @Override - public boolean deleteById(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByIdIn(Set.of(aiSettingsId.getId())) > 0; + public boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.deleteByIdIn(Set.of(settingsId.getId())) > 0; } @Override public int deleteByTenantId(TenantId tenantId) { - return aiSettingsRepository.deleteByTenantId(tenantId.getId()); + return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()); } @Override - public boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) { - return aiSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(aiSettingsId.getId())) > 0; + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { + return aiModelSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(settingsId.getId())) > 0; } @Override public EntityType getEntityType() { - return EntityType.AI_SETTINGS; + return EntityType.AI_MODEL_SETTINGS; } @Override - protected Class getEntityClass() { - return AiSettingsEntity.class; + protected Class getEntityClass() { + return AiModelSettingsEntity.class; } @Override - protected JpaRepository getRepository() { - return aiSettingsRepository; + protected JpaRepository getRepository() { + return aiModelSettingsRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 505fe2a50a..ef85ba30c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService> ChatModel configureChatModel(AiChatModel chatModel); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index e5306c0193..ab0f3bd050 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -418,9 +418,9 @@ public interface TbContext { AuditLogService getAuditLogService(); - RuleEngineAiService getAiService(); + RuleEngineAiModelService getAiModelService(); - AiSettingsService getAiSettingsService(); + AiModelSettingsService getAiModelSettingsService(); AiRequestsExecutor getAiRequestsExecutor(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 4d56b43c8e..2ac3ea3e6c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -36,9 +36,11 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; -import org.thingsboard.server.common.data.ai.model.AiModelConfig; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.model.AiModelType; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; @@ -46,6 +48,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; @@ -64,7 +67,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String userPrompt; private ResponseFormat responseFormat; private int timeoutSeconds; - private AiSettingsId aiSettingsId; + private AiModelSettingsId modelSettingsId; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -86,11 +89,16 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); timeoutSeconds = config.getTimeoutSeconds(); + modelSettingsId = config.getAiModelSettingsId(); - if (!aiSettingsExist(ctx, config.getAiSettingsId())) { - throw new TbNodeException("[" + ctx.getTenantId() + "] AI settings with ID: " + config.getAiSettingsId() + " were not found", true); + Optional modelSettings = ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndId(ctx.getTenantId(), modelSettingsId); + if (modelSettings.isEmpty()) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found", true); + } + AiModelType modelType = modelSettings.get().getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType, true); } - aiSettingsId = config.getAiSettingsId(); } private static JsonSchema getJsonSchema(ResponseFormatType responseFormatType, ObjectNode jsonSchema) { @@ -100,12 +108,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return responseFormatType == ResponseFormatType.JSON && jsonSchema != null ? Langchain4jJsonSchemaAdapter.fromJsonNode(jsonSchema) : null; } - private static boolean aiSettingsExist(TbContext ctx, AiSettingsId aiSettingsId) { - return ctx.getAiSettingsService().findAiSettingsByTenantIdAndId(ctx.getTenantId(), aiSettingsId).isPresent(); - } - @Override - public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { + public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); var systemMessage = SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg)); @@ -137,19 +141,25 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { }, directExecutor()); } - private FluentFuture configureChatModelAsync(TbContext ctx) { - return ctx.getAiSettingsService().findAiSettingsByTenantIdAndIdAsync(ctx.getTenantId(), aiSettingsId).transform(aiSettingsOpt -> { - if (aiSettingsOpt.isEmpty()) { - throw new NoSuchElementException("AI settings with ID: " + aiSettingsId + " were not found"); + private > FluentFuture configureChatModelAsync(TbContext ctx) { + return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transform(settingsOpt -> { + if (settingsOpt.isEmpty()) { + throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found"); + } + AiModelSettings settings = settingsOpt.get(); + AiModelType modelType = settings.getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType); } - AiProviderConfig providerConfig = aiSettingsOpt.get().getProviderConfig(); - AiModelConfig modelConfig = aiSettingsOpt.get().getModelConfig(); + @SuppressWarnings("unchecked") + AiChatModel chatModel = (AiChatModel) settingsOpt.get().getConfiguration(); - modelConfig.setTimeoutSeconds(timeoutSeconds); - modelConfig.setMaxRetries(0); // disable retries to respect timeout set in rule node config + chatModel = chatModel.withModelConfig(chatModel.modelConfig() + .withTimeoutSeconds(timeoutSeconds) + .withMaxRetries(0)); // disable retries to respect timeout set in rule node config - return ctx.getAiService().configureChatModel(providerConfig, modelConfig); + return ctx.getAiModelService().configureChatModel(chatModel); }, ctx.getDbCallbackExecutor()); } @@ -172,7 +182,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = null; userPrompt = null; responseFormat = null; - aiSettingsId = null; + modelSettingsId = null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 96ecde330a..4c99b6f613 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -26,14 +26,14 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.validation.Length; @Data public class TbAiNodeConfiguration implements NodeConfiguration { @NotNull - private AiSettingsId aiSettingsId; + private AiModelSettingsId aiModelSettingsId; @NotBlank @Length(min = 1, max = 1000) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 62ca65db1b..0df2652ccb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AiSettingsId; +import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; @@ -176,8 +176,8 @@ public class TenantIdLoader { tenantEntity = null; } break; - case AI_SETTINGS: - tenantEntity = ctx.getAiSettingsService().findAiSettingsById(ctxTenantId, new AiSettingsId(id)).orElse(null); + case AI_MODEL_SETTINGS: + tenantEntity = ctx.getAiModelSettingsService().findAiModelSettingsById(ctxTenantId, new AiModelSettingsId(id)).orElse(null); break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index c763a1b069..873393ef2b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiSettings; +import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -69,7 +69,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiSettingsService; +import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; @@ -164,7 +164,7 @@ public class TenantIdLoaderTest { @Mock private CalculatedFieldService calculatedFieldService; @Mock - private AiSettingsService aiSettingsService; + private AiModelSettingsService aiModelSettingsService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -424,11 +424,11 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); break; - case AI_SETTINGS: - AiSettings aiSettings = new AiSettings(); - aiSettings.setTenantId(tenantId); - when(ctx.getAiSettingsService()).thenReturn(aiSettingsService); - doReturn(Optional.of(aiSettings)).when(aiSettingsService).findAiSettingsById(eq(tenantId), any()); + case AI_MODEL_SETTINGS: + AiModelSettings aiModelSettings = new AiModelSettings(); + aiModelSettings.setTenantId(tenantId); + when(ctx.getAiModelSettingsService()).thenReturn(aiModelSettingsService); + doReturn(Optional.of(aiModelSettings)).when(aiModelSettingsService).findAiModelSettingsById(eq(tenantId), any()); break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType);