AI rule node: refactor model config data structure; rename from AI settings to AI model settings
This commit is contained in:
		
							parent
							
								
									459cc6d27e
								
							
						
					
					
						commit
						1343c4af3b
					
				@ -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)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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<String> ALLOWED_SORT_PROPERTIES = Set.of("createdTime", "name", "provider", "model");
 | 
			
		||||
    private static final Set<String> 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<AiSettings> getAiSettings(
 | 
			
		||||
    public PageData<AiModelSettings> 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<AiSettings> aiSettingsOpt = aiSettingsService.findAiSettingsByTenantIdAndId(user.getTenantId(), aiSettingsId);
 | 
			
		||||
        if (aiSettingsOpt.isEmpty()) {
 | 
			
		||||
        var settingsId = new AiModelSettingsId(aiModelSettingsUuid);
 | 
			
		||||
        accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE);
 | 
			
		||||
        Optional<AiModelSettings> 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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 extends EntityId> I emptyId(EntityType entityType) {
 | 
			
		||||
 | 
			
		||||
@ -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 <C extends AiChatModelConfig<C>> ChatModel configureChatModel(AiChatModel<C> chatModel) {
 | 
			
		||||
        return chatModel.configure(chatModelConfigurer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettings> 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();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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(() -> {
 | 
			
		||||
 | 
			
		||||
@ -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 -> {
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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<EntityType> entityTypes;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<AiSettingsId, AiSettings> aiSettingsPermissionChecker = new PermissionChecker<>() {
 | 
			
		||||
    private static final PermissionChecker<AiModelSettingsId, AiModelSettings> 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());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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}"
 | 
			
		||||
 | 
			
		||||
@ -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<AiSettings> findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
    Optional<AiModelSettings> findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId);
 | 
			
		||||
 | 
			
		||||
    PageData<AiSettings> findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink);
 | 
			
		||||
    PageData<AiModelSettings> findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink);
 | 
			
		||||
 | 
			
		||||
    Optional<AiSettings> findAiSettingsByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
    Optional<AiModelSettings> findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId);
 | 
			
		||||
 | 
			
		||||
    FluentFuture<Optional<AiSettings>> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
    FluentFuture<Optional<AiModelSettings>> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId);
 | 
			
		||||
 | 
			
		||||
    boolean deleteByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
    boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -112,6 +112,10 @@
 | 
			
		||||
            <artifactId>leshan-core</artifactId>
 | 
			
		||||
            <scope>compile</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>dev.langchain4j</groupId>
 | 
			
		||||
            <artifactId>langchain4j</artifactId>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 | 
			
		||||
@ -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";
 | 
			
		||||
 | 
			
		||||
@ -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";
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<AiSettingsId> implements HasTenantId, HasVersion, HasName {
 | 
			
		||||
public final class AiModelSettings extends BaseData<AiModelSettingsId> implements HasTenantId, HasVersion, HasName {
 | 
			
		||||
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = 9017108678716011604L;
 | 
			
		||||
@ -44,7 +42,7 @@ public final class AiSettings extends BaseData<AiSettingsId> 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<AiSettingsId> 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<AiSettingsId> 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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<C extends AiModelConfig<C>> {
 | 
			
		||||
 | 
			
		||||
    AiProviderConfig providerConfig();
 | 
			
		||||
 | 
			
		||||
    AiModelType modelType();
 | 
			
		||||
 | 
			
		||||
    String modelId();
 | 
			
		||||
 | 
			
		||||
    C modelConfig();
 | 
			
		||||
 | 
			
		||||
    AiModel<C> withModelConfig(C config);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<C extends AiModelConfig<C>> {}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<C extends AiChatModelConfig<C>> extends AiModel<C>
 | 
			
		||||
        permits OpenAiChatModel, GoogleAiGeminiChatModel, MistralAiChatModel {
 | 
			
		||||
 | 
			
		||||
    ChatModel configure(Langchain4jChatModelConfigurer configurer);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    default AiModelType modelType() {
 | 
			
		||||
        return AiModelType.CHAT;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    C modelConfig();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AiChatModel<C> withModelConfig(C config);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<C extends AiChatModelConfig<C>> extends AiModelConfig<C>
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<GoogleAiGeminiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
    public record Config(
 | 
			
		||||
            Double temperature,
 | 
			
		||||
            Integer timeoutSeconds,
 | 
			
		||||
            Integer maxRetries
 | 
			
		||||
    ) implements AiChatModelConfig<GoogleAiGeminiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
        @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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<MistralAiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
    public record Config(
 | 
			
		||||
            Double temperature,
 | 
			
		||||
            Integer timeoutSeconds,
 | 
			
		||||
            Integer maxRetries
 | 
			
		||||
    ) implements AiChatModelConfig<MistralAiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
        @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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<OpenAiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
    public record Config(
 | 
			
		||||
            Double temperature,
 | 
			
		||||
            Integer timeoutSeconds,
 | 
			
		||||
            Integer maxRetries
 | 
			
		||||
    ) implements AiChatModelConfig<OpenAiChatModel.Config> {
 | 
			
		||||
 | 
			
		||||
        @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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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!");
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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<AiSettingsCacheKey> keys) {
 | 
			
		||||
record AiModelSettingsCacheEvictEvent(Set<AiModelSettingsCacheKey> 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)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettingsCacheKey, AiSettings> {
 | 
			
		||||
class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache<AiModelSettingsCacheKey, AiModelSettings> {
 | 
			
		||||
 | 
			
		||||
    AiSettingsCaffeineCache(CacheManager cacheManager) {
 | 
			
		||||
        super(cacheManager, CacheConstants.AI_SETTINGS_CACHE);
 | 
			
		||||
    AiModelSettingsCaffeineCache(CacheManager cacheManager) {
 | 
			
		||||
        super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettings>, TenantEntityDao<AiSettings> {
 | 
			
		||||
public interface AiModelSettingsDao extends Dao<AiModelSettings>, TenantEntityDao<AiModelSettings> {
 | 
			
		||||
 | 
			
		||||
    Optional<AiSettings> findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
    Optional<AiModelSettings> 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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettingsCacheKey, AiSettings> {
 | 
			
		||||
class AiModelSettingsRedisCache extends VersionedRedisTbCache<AiModelSettingsCacheKey, AiModelSettings> {
 | 
			
		||||
 | 
			
		||||
    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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettingsCacheKey, AiSettings, AiSettingsCacheEvictEvent> implements AiSettingsService {
 | 
			
		||||
class AiModelSettingsServiceImpl extends CachedVersionedEntityService<AiModelSettingsCacheKey, AiModelSettings, AiModelSettingsCacheEvictEvent> implements AiModelSettingsService {
 | 
			
		||||
 | 
			
		||||
    private final DataValidator<AiSettings> aiSettingsValidator;
 | 
			
		||||
    private final DataValidator<AiModelSettings> 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<AiSettingsCache
 | 
			
		||||
                .broadcastEvent(true)
 | 
			
		||||
                .build());
 | 
			
		||||
 | 
			
		||||
        publishEvictEvent(AiSettingsCacheEvictEvent.of(savedSettings.getTenantId(), savedSettings.getId()));
 | 
			
		||||
        publishEvictEvent(AiModelSettingsCacheEvictEvent.of(savedSettings.getTenantId(), savedSettings.getId()));
 | 
			
		||||
 | 
			
		||||
        return savedSettings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<AiSettings> findAiSettingsById(TenantId tenantId, AiSettingsId aiSettingsId) {
 | 
			
		||||
        return Optional.ofNullable(aiSettingsDao.findById(tenantId, aiSettingsId.getId()));
 | 
			
		||||
    public Optional<AiModelSettings> findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) {
 | 
			
		||||
        return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PageData<AiSettings> findAiSettingsByTenantId(TenantId tenantId, PageLink pageLink) {
 | 
			
		||||
    public PageData<AiModelSettings> findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) {
 | 
			
		||||
        validatePageLink(pageLink);
 | 
			
		||||
        return aiSettingsDao.findAllByTenantId(tenantId, pageLink);
 | 
			
		||||
        return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<AiSettings> 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<AiModelSettings> 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<Optional<AiSettings>> findAiSettingsByTenantIdAndIdAsync(TenantId tenantId, AiSettingsId aiSettingsId) {
 | 
			
		||||
        return FluentFuture.from(jpaExecutor.submit(() -> findAiSettingsByTenantIdAndId(tenantId, aiSettingsId)));
 | 
			
		||||
    public FluentFuture<Optional<AiModelSettings>> 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<HasId<?>> 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<AiSettings> aiSettingsOpt = aiSettingsDao.findByTenantIdAndId(tenantId, aiSettingsId);
 | 
			
		||||
        if (aiSettingsOpt.isEmpty()) {
 | 
			
		||||
    private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelSettingsId settingsId) {
 | 
			
		||||
        Optional<AiModelSettings> 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<AiSettingsCache
 | 
			
		||||
    @Override
 | 
			
		||||
    @Transactional
 | 
			
		||||
    public void deleteByTenantId(TenantId tenantId) {
 | 
			
		||||
        List<AiSettings> deletedSettings = aiSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData();
 | 
			
		||||
        if (deletedSettings.isEmpty()) {
 | 
			
		||||
        List<AiModelSettings> toDelete = aiModelSettingsDao.findAllByTenantId(tenantId, new PageLink(Integer.MAX_VALUE)).getData();
 | 
			
		||||
        if (toDelete.isEmpty()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        aiSettingsDao.deleteByTenantId(tenantId);
 | 
			
		||||
        aiModelSettingsDao.deleteByTenantId(tenantId);
 | 
			
		||||
 | 
			
		||||
        Set<AiSettingsCacheKey> cacheKeys = Sets.newHashSetWithExpectedSize(deletedSettings.size());
 | 
			
		||||
        deletedSettings.forEach(settings -> {
 | 
			
		||||
        Set<AiModelSettingsCacheKey> 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<AiSettingsCache
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public EntityType getEntityType() {
 | 
			
		||||
        return EntityType.AI_SETTINGS;
 | 
			
		||||
        return EntityType.AI_MODEL_SETTINGS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -46,7 +46,7 @@ public class CleanUpService {
 | 
			
		||||
    private final Set<EntityType> 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
 | 
			
		||||
 | 
			
		||||
@ -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};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<AiSettings> {
 | 
			
		||||
@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME)
 | 
			
		||||
public class AiModelSettingsEntity extends BaseVersionedEntity<AiModelSettings> {
 | 
			
		||||
 | 
			
		||||
    @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID")
 | 
			
		||||
    public static final Map<String, String> 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<AiSettings> {
 | 
			
		||||
        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());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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<AiModelSettings> {
 | 
			
		||||
 | 
			
		||||
    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<AiModelSettings> 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!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettings> {
 | 
			
		||||
 | 
			
		||||
    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<AiSettings> 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!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettingsEntity, UUID> {
 | 
			
		||||
interface AiModelSettingsRepository extends JpaRepository<AiModelSettingsEntity, UUID> {
 | 
			
		||||
 | 
			
		||||
    @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<AiSettingsEntity> 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<AiModelSettingsEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable);
 | 
			
		||||
 | 
			
		||||
    Optional<AiSettingsEntity> findByTenantIdAndId(UUID tenantId, UUID id);
 | 
			
		||||
    Optional<AiModelSettingsEntity> 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<UUID> ids);
 | 
			
		||||
 | 
			
		||||
    @Transactional
 | 
			
		||||
@ -53,7 +55,7 @@ public interface AiSettingsRepository extends JpaRepository<AiSettingsEntity, UU
 | 
			
		||||
 | 
			
		||||
    @Transactional
 | 
			
		||||
    @Modifying
 | 
			
		||||
    @Query("DELETE FROM AiSettingsEntity ai WHERE ai.tenantId = :tenantId AND ai.id IN (:ids)")
 | 
			
		||||
    @Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId AND ai_model.id IN (:ids)")
 | 
			
		||||
    int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set<UUID> ids);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<AiSettingsEntity, AiSettings> implements AiSettingsDao {
 | 
			
		||||
class JpaAiSettingsDao extends JpaAbstractDao<AiModelSettingsEntity, AiModelSettings> implements AiModelSettingsDao {
 | 
			
		||||
 | 
			
		||||
    private final AiSettingsRepository aiSettingsRepository;
 | 
			
		||||
    private final AiModelSettingsRepository aiModelSettingsRepository;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<AiSettings> findByTenantIdAndId(TenantId tenantId, AiSettingsId aiSettingsId) {
 | 
			
		||||
        return aiSettingsRepository.findByTenantIdAndId(tenantId.getId(), aiSettingsId.getId()).map(DaoUtil::getData);
 | 
			
		||||
    public Optional<AiModelSettings> findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) {
 | 
			
		||||
        return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public PageData<AiSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
 | 
			
		||||
        return DaoUtil.toPageData(aiSettingsRepository.findByTenantId(
 | 
			
		||||
                tenantId.getId(), StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), DaoUtil.toPageable(pageLink))
 | 
			
		||||
    public PageData<AiModelSettings> 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<AiSettingsEntity> getEntityClass() {
 | 
			
		||||
        return AiSettingsEntity.class;
 | 
			
		||||
    protected Class<AiModelSettingsEntity> getEntityClass() {
 | 
			
		||||
        return AiModelSettingsEntity.class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected JpaRepository<AiSettingsEntity, UUID> getRepository() {
 | 
			
		||||
        return aiSettingsRepository;
 | 
			
		||||
    protected JpaRepository<AiModelSettingsEntity, UUID> getRepository() {
 | 
			
		||||
        return aiModelSettingsRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
 | 
			
		||||
                EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE,
 | 
			
		||||
                EntityType.NOTIFICATION_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER,
 | 
			
		||||
                EntityType.DOMAIN, EntityType.MOBILE_APP_BUNDLE, EntityType.MOBILE_APP, EntityType.OAUTH2_CLIENT,
 | 
			
		||||
                EntityType.AI_SETTINGS
 | 
			
		||||
                EntityType.AI_MODEL_SETTINGS
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -949,15 +949,12 @@ CREATE TABLE IF NOT EXISTS cf_debug_event (
 | 
			
		||||
    e_error varchar
 | 
			
		||||
) PARTITION BY RANGE (ts);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS ai_settings (
 | 
			
		||||
CREATE TABLE IF NOT EXISTS 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)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -16,15 +16,11 @@
 | 
			
		||||
package org.thingsboard.rule.engine.api;
 | 
			
		||||
 | 
			
		||||
import dev.langchain4j.model.chat.ChatModel;
 | 
			
		||||
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.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel;
 | 
			
		||||
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
 | 
			
		||||
 | 
			
		||||
public interface RuleEngineAiService {
 | 
			
		||||
public interface RuleEngineAiModelService {
 | 
			
		||||
 | 
			
		||||
    ChatModel configureChatModel(TenantId tenantId, AiSettingsId aiSettingsId);
 | 
			
		||||
 | 
			
		||||
    ChatModel configureChatModel(AiProviderConfig providerConfig, AiModelConfig modelConfig);
 | 
			
		||||
    <C extends AiChatModelConfig<C>> ChatModel configureChatModel(AiChatModel<C> chatModel);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<AiModelSettings> 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<ChatModel> 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 <C extends AiChatModelConfig<C>> FluentFuture<ChatModel> 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<C> chatModel = (AiChatModel<C>) 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<TbAiNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    private AiSettingsId aiSettingsId;
 | 
			
		||||
    private AiModelSettingsId aiModelSettingsId;
 | 
			
		||||
 | 
			
		||||
    @NotBlank
 | 
			
		||||
    @Length(min = 1, max = 1000)
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user