Browse Source

AI rule node: rename AI model settings to AI model

pull/13371/head
Dmytro Skarzhynets 1 year ago
parent
commit
b6b30dbba9
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 8
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 122
      application/src/main/java/org/thingsboard/server/controller/AiModelController.java
  4. 143
      application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java
  5. 22
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java
  8. 9
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  9. 214
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  10. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  12. 36
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java
  13. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  15. 10
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  17. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java
  18. 44
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java
  19. 8
      application/src/main/resources/thingsboard.yml
  20. 18
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
  21. 4
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  22. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  23. 38
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  24. 4
      common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java
  25. 84
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java
  26. 60
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java
  27. 41
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java
  28. 15
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
  29. 21
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
  30. 23
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
  31. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
  32. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
  33. 27
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
  34. 27
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
  35. 16
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java
  36. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
  37. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
  38. 16
      common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
  39. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
  41. 2
      common/proto/src/main/proto/queue.proto
  42. 16
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java
  43. 18
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java
  44. 10
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java
  45. 14
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java
  46. 10
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java
  47. 149
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
  48. 149
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java
  49. 2
      dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java
  50. 10
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  51. 54
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java
  52. 36
      dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java
  53. 40
      dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java
  54. 66
      dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java
  55. 2
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  56. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  57. 6
      dao/src/main/resources/sql/schema-entities.sql
  58. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiChatModelService.java
  59. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  60. 43
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
  61. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java
  62. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java
  63. 16
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

8
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; 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.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -315,11 +315,11 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private RuleEngineAiModelService aiModelService; private RuleEngineAiChatModelService aiChatModelService;
@Autowired @Autowired
@Getter @Getter
private AiModelSettingsService aiModelSettingsService; private AiModelService aiModelService;
@Autowired @Autowired
@Getter @Getter

12
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; 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.TbMsgProcessingStackItem;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -1027,13 +1027,13 @@ public class DefaultTbContext implements TbContext {
} }
@Override @Override
public RuleEngineAiModelService getAiModelService() { public RuleEngineAiChatModelService getAiChatModelService() {
return mainCtx.getAiModelService(); return mainCtx.getAiChatModelService();
} }
@Override @Override
public AiModelSettingsService getAiModelSettingsService() { public AiModelService getAiModelService() {
return mainCtx.getAiModelSettingsService(); return mainCtx.getAiModelService();
} }
@Override @Override

122
application/src/main/java/org/thingsboard/server/controller/AiModelController.java

@ -17,31 +17,141 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequest;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.dto.TbChatRequest; import org.thingsboard.server.common.data.ai.dto.TbChatRequest;
import org.thingsboard.server.common.data.ai.dto.TbChatResponse; import org.thingsboard.server.common.data.ai.dto.TbChatResponse;
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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.service.ai.AiModelService; import org.thingsboard.server.service.ai.AiChatModelService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.time.Duration; import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@Validated
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api/ai/model") @RequestMapping("/api/ai/model")
class AiModelController extends BaseController { class AiModelController extends BaseController {
private final AiModelService aiModelService; private final AiChatModelService aiChatModelService;
@ApiOperation(
value = "Create or update AI model (saveAiModel)",
notes = "Creates or updates an AI model record.\n\n" +
"• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new record 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 model 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 AiModel saveAiModel(@RequestBody @Valid AiModel model) throws ThingsboardException {
var user = getCurrentUser();
model.setTenantId(user.getTenantId());
checkEntity(model.getId(), model, Resource.AI_MODEL);
return tbAiModelService.save(model, user);
}
@ApiOperation(
value = "Get AI model by ID (getAiModelById)",
notes = "Fetches an AI model record by its `id`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/{modelUuid}")
public AiModel getAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
return checkAiModelId(new AiModelId(modelUuid), Operation.READ);
}
@ApiOperation(
value = "Get AI models (getAiModels)",
notes = "Returns a page of AI models. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping
public PageData<AiModel> getAiModels(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = AI_MODEL_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"}))
@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_MODEL, Operation.READ);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiModelService.findAiModelsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(
value = "Delete AI model by ID (deleteAiModelById)",
notes = "Deletes the AI model 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("/{modelUuid}")
public boolean deleteAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
var user = getCurrentUser();
var modelId = new AiModelId(modelUuid);
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE);
Optional<AiModel> toDelete = aiModelService.findAiModelByTenantIdAndId(user.getTenantId(), modelId);
if (toDelete.isEmpty()) {
return false;
}
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE, modelId, toDelete.get());
return tbAiModelService.delete(toDelete.get(), user);
}
@ApiOperation( @ApiOperation(
value = "Send request to AI chat model (sendChatRequest)", value = "Send request to AI chat model (sendChatRequest)",
@ -53,13 +163,13 @@ class AiModelController extends BaseController {
@PostMapping("/chat") @PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) { public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest(); ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest();
AiChatModel<?> chatModel = tbChatRequest.chatModel(); AiChatModelConfig<?> chatModelConfig = tbChatRequest.chatModelConfig();
ListenableFuture<TbChatResponse> future = aiModelService.sendChatRequestAsync(chatModel, langChainChatRequest) ListenableFuture<TbChatResponse> future = aiChatModelService.sendChatRequestAsync(chatModelConfig, langChainChatRequest)
.transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor()) .transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor())
.catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor()); .catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor());
Integer requestTimeoutSeconds = chatModel.modelConfig().timeoutSeconds(); Integer requestTimeoutSeconds = chatModelConfig.timeoutSeconds();
return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future); return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future);
} }

143
application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java

@ -1,143 +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.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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.AiModelSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
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;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Optional;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@Validated
@RestController
@RequestMapping("/api/ai/model/settings")
class AiModelSettingsController extends BaseController {
@ApiOperation(
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 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 AiModelSettings saveAiModelSettings(@RequestBody @Valid AiModelSettings settings) throws ThingsboardException {
var user = getCurrentUser();
settings.setTenantId(user.getTenantId());
checkEntity(settings.getId(), settings, Resource.AI_MODEL_SETTINGS);
return tbAiModelSettingsService.save(settings, user);
}
@ApiOperation(
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("/{settingsUuid}")
public AiModelSettings getAiModelSettingsById(
@Parameter(
description = "ID of the AI model settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID settingsUuid
) throws ThingsboardException {
return checkAiModelSettingsId(new AiModelSettingsId(settingsUuid), Operation.READ);
}
@ApiOperation(
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<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_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"}))
@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_MODEL_SETTINGS, Operation.READ);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(
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("/{settingsUuid}")
public boolean deleteAiModelSettingsById(
@Parameter(
description = "ID of the AI model settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID settingsUuid
) throws ThingsboardException {
var user = getCurrentUser();
var settingsId = new AiModelSettingsId(settingsUuid);
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_MODEL_SETTINGS, Operation.DELETE, settingsId, toDelete.get());
return tbAiModelSettingsService.delete(toDelete.get(), user);
}
}

22
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -61,7 +61,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -76,7 +76,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; 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.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -178,7 +178,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.entitiy.ai.TbAiModelSettingsService; import org.thingsboard.server.service.entitiy.ai.TbAiModelService;
import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService;
import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -383,10 +383,10 @@ public abstract class BaseController {
protected CalculatedFieldService calculatedFieldService; protected CalculatedFieldService calculatedFieldService;
@Autowired @Autowired
protected AiModelSettingsService aiModelSettingsService; protected AiModelService aiModelService;
@Autowired @Autowired
protected TbAiModelSettingsService tbAiModelSettingsService; protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}") @Value("${server.log_controller_error_stack_trace}")
@Getter @Getter
@ -400,7 +400,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) { public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e); ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause(); e = (Exception) thingsboardException.getCause();
} else { } else {
e = thingsboardException; e = thingsboardException;
@ -448,7 +448,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
@ -644,7 +644,7 @@ public abstract class BaseController {
case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation);
case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
case AI_MODEL_SETTINGS -> checkAiModelSettingsId(new AiModelSettingsId(entityId.getId()), operation); case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation);
default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}; };
} catch (Exception e) { } catch (Exception e) {
@ -848,8 +848,8 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation); return checkEntityId(jobId, jobService::findJobById, operation);
} }
AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException { AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation);
} }
protected <I extends EntityId> I emptyId(EntityType entityType) { protected <I extends EntityId> I emptyId(EntityType entityType) {

2
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -90,7 +90,7 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model settings name, provider and model ID."; protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";

4
application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java → application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java

@ -15,6 +15,6 @@
*/ */
package org.thingsboard.server.service.ai; package org.thingsboard.server.service.ai;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
public interface AiModelService extends RuleEngineAiModelService {} public interface AiChatModelService extends RuleEngineAiChatModelService {}

9
application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java → application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

@ -21,21 +21,20 @@ import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.ChatResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
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.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
class AiModelServiceImpl implements AiModelService { class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer; private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor; private final AiRequestsExecutor aiRequestsExecutor;
@Override @Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest) { public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer); ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest); return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
} }
} }

214
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -24,20 +24,26 @@ import com.google.cloud.vertexai.api.GenerationConfig;
import com.google.cloud.vertexai.api.PredictionServiceClient; import com.google.cloud.vertexai.api.PredictionServiceClient;
import com.google.cloud.vertexai.api.PredictionServiceSettings; import com.google.cloud.vertexai.api.PredictionServiceSettings;
import com.google.cloud.vertexai.generativeai.GenerativeModel; import com.google.cloud.vertexai.generativeai.GenerativeModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
import dev.langchain4j.model.bedrock.BedrockChatModel; import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel; import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; 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.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
@ -54,61 +60,57 @@ import java.time.Duration;
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override @Override
public ChatModel configureChatModel(OpenAiChatModel chatModel) { public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return OpenAiChatModel.builder()
return dev.langchain4j.model.openai.OpenAiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) { public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModel.providerConfig(); AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return AzureOpenAiChatModel.builder()
return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint()) .endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion()) .serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey()) .apiKey(providerConfig.apiKey())
.deploymentName(modelConfig.modelId()) .deploymentName(chatModelConfig.modelId())
.temperature(modelConfig.temperature()) .temperature(chatModelConfig.temperature())
.topP(modelConfig.topP()) .topP(chatModelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens()) .maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds())) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries()) .maxRetries(chatModelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); return GoogleAiGeminiChatModel.builder()
return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .topK(chatModelConfig.topK())
.topK(modelConfig.topK()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxOutputTokens(chatModelConfig.maxOutputTokens())
.maxOutputTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) { public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig(); GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
// construct service account credentials using service account key JSON // construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials; ServiceAccountCredentials serviceAccountCredentials;
@ -131,8 +133,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.toBuilder(); .toBuilder();
// set request timeout from model config // set request timeout from model config
if (modelConfig.timeoutSeconds() != null) { if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds())); retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
} }
// set updated retry settings // set updated retry settings
@ -154,30 +156,30 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// map model config to generation config // map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder(); var generationConfigBuilder = GenerationConfig.newBuilder();
if (modelConfig.temperature() != null) { if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
} }
if (modelConfig.topP() != null) { if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(modelConfig.topP().floatValue()); generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
} }
if (modelConfig.topK() != null) { if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(modelConfig.topK()); generationConfigBuilder.setTopK(chatModelConfig.topK());
} }
if (modelConfig.frequencyPenalty() != null) { if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue()); generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
} }
if (modelConfig.frequencyPenalty() != null) { if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue()); generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
} }
if (modelConfig.maxOutputTokens() != null) { if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens()); generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
} }
var generationConfig = generationConfigBuilder.build(); var generationConfig = generationConfigBuilder.build();
// construct generative model instance // construct generative model instance
var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig);
return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries());
} }
private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) { private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) {
@ -189,40 +191,37 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
} }
@Override @Override
public ChatModel configureChatModel(MistralAiChatModel chatModel) { public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) {
MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); return MistralAiChatModel.builder()
return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AnthropicChatModel chatModel) { public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) {
AnthropicChatModel.Config modelConfig = chatModel.modelConfig(); return AnthropicChatModel.builder()
return dev.langchain4j.model.anthropic.AnthropicChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .topK(chatModelConfig.topK())
.topK(modelConfig.topK()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) { public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig(); AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig();
var credentialsProvider = StaticCredentialsProvider.create( var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey()) AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
@ -234,33 +233,32 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build(); .build();
var defaultChatRequestParams = ChatRequestParameters.builder() var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(modelConfig.temperature()) .temperature(chatModelConfig.temperature())
.topP(modelConfig.topP()) .topP(chatModelConfig.topP())
.maxOutputTokens(modelConfig.maxOutputTokens()) .maxOutputTokens(chatModelConfig.maxOutputTokens())
.build(); .build();
return BedrockChatModel.builder() return BedrockChatModel.builder()
.client(bedrockClient) .client(bedrockClient)
.modelId(modelConfig.modelId()) .modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams) .defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(modelConfig.timeoutSeconds())) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries()) .maxRetries(chatModelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GitHubModelsChatModel chatModel) { public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
GitHubModelsChatModel.Config modelConfig = chatModel.modelConfig(); return GitHubModelsChatModel.builder()
return dev.langchain4j.model.github.GitHubModelsChatModel.builder() .gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
.gitHubToken(chatModel.providerConfig().personalAccessToken()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -112,7 +112,7 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { try {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) { if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
return; return;
} }
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -226,7 +226,7 @@ public class EdgeEventSourcingListener {
break; break;
case TENANT: case TENANT:
return !event.getCreated(); return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS: case API_USAGE_STATE, EDGE, AI_MODEL:
return false; return false;
case DOMAIN: case DOMAIN:
if (entity instanceof Domain domain) { if (entity instanceof Domain domain) {

2
application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java

@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener( @TransactionalEventListener(
fallbackExecution = true, fallbackExecution = true,
condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL"
) )
public void handleEvent(DeleteEntityEvent<?> event) { public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {

36
application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java → application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java

@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -30,48 +30,48 @@ import static java.util.Objects.requireNonNullElseGet;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService { class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelSettingsService aiModelSettingsService; private final AiModelService aiModelService;
@Override @Override
public AiModelSettings save(AiModelSettings settings, User user) { public AiModel save(AiModel model, User user) {
var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId(); var tenantId = user.getTenantId();
settings.setTenantId(tenantId); model.setTenantId(tenantId);
AiModelSettings savedSettings; AiModel savedModel;
try { try {
savedSettings = aiModelSettingsService.save(settings); savedModel = aiModelService.save(model);
autoCommit(user, savedSettings.getId()); autoCommit(user, savedModel.getId());
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e);
throw e; throw e;
} }
logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user); logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedSettings; return savedModel;
} }
@Override @Override
public boolean delete(AiModelSettings settings, User user) { public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED; var actionType = ActionType.DELETED;
var tenantId = user.getTenantId(); var tenantId = user.getTenantId();
var settingsId = settings.getId(); var modelId = model.getId();
boolean deleted; boolean deleted;
try { try {
deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId); deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, e, settingsId.toString()); logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString());
throw e; throw e;
} }
if (deleted) { if (deleted) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString()); logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
} }
return deleted; return deleted;

8
application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java → application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java

@ -16,12 +16,12 @@
package org.thingsboard.server.service.entitiy.ai; package org.thingsboard.server.service.entitiy.ai;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
public interface TbAiModelSettingsService { public interface TbAiModelService {
AiModelSettings save(AiModelSettings settings, User user); AiModel save(AiModel model, User user);
boolean delete(AiModelSettings settings, User user); boolean delete(AiModel model, User user);
} }

2
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -53,7 +53,7 @@ public enum Resource {
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS, MOBILE_APP_SETTINGS,
JOB(EntityType.JOB), JOB(EntityType.JOB),
AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS); AI_MODEL(EntityType.AI_MODEL);
private final Set<EntityType> entityTypes; private final Set<EntityType> entityTypes;

10
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; 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, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker);
put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker); put(Resource.AI_MODEL, aiModelPermissionChecker);
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@ -149,7 +149,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
}; };
private static final PermissionChecker<AiModelSettingsId, AiModelSettings> aiModelSettingsPermissionChecker = new PermissionChecker<>() { private static final PermissionChecker<AiModelId, AiModel> aiModelPermissionChecker = new PermissionChecker<>() {
@Override @Override
public boolean hasPermission(SecurityUser user, Operation operation) { public boolean hasPermission(SecurityUser user, Operation operation) {
@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
} }
@Override @Override
public boolean hasPermission(SecurityUser user, Operation operation, AiModelSettingsId entityId, AiModelSettings entity) { public boolean hasPermission(SecurityUser user, Operation operation, AiModelId entityId, AiModel entity) {
return user.getTenantId().equals(entity.getTenantId()); return user.getTenantId().equals(entity.getTenantId());
} }

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -70,7 +70,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.AI_MODEL_SETTINGS EntityType.AI_MODEL
); );
@Override @Override

8
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java → application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java

@ -17,8 +17,8 @@ package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -26,11 +26,11 @@ import java.util.Set;
@Service @Service
@TbCoreComponent @TbCoreComponent
class AiModelSettingsExportService extends BaseEntityExportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> { class AiModelExportService extends BaseEntityExportService<AiModelId, AiModel, EntityExportData<AiModel>> {
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.AI_MODEL_SETTINGS); return Set.of(EntityType.AI_MODEL);
} }
} }

44
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java → application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java

@ -18,60 +18,60 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
class AiModelSettingsImportService extends BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> { class AiModelImportService extends BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>> {
private final AiModelSettingsService aiModelSettingsService; private final AiModelService aiModelService;
@Override @Override
protected void setOwner( protected void setOwner(
TenantId tenantId, TenantId tenantId,
AiModelSettings settings, AiModel model,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) { ) {
settings.setTenantId(tenantId); model.setTenantId(tenantId);
} }
@Override @Override
protected AiModelSettings prepare( protected AiModel prepare(
EntitiesImportCtx ctx, EntitiesImportCtx ctx,
AiModelSettings settings, AiModel model,
AiModelSettings oldEntity, AiModel oldModel,
EntityExportData<AiModelSettings> exportData, EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) { ) {
return settings; return model;
} }
@Override @Override
protected AiModelSettings deepCopy(AiModelSettings settings) { protected AiModel deepCopy(AiModel model) {
return new AiModelSettings(settings); return new AiModel(model);
} }
@Override @Override
protected AiModelSettings saveOrUpdate( protected AiModel saveOrUpdate(
EntitiesImportCtx ctx, EntitiesImportCtx ctx,
AiModelSettings settings, AiModel model,
EntityExportData<AiModelSettings> exportData, EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider, BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider,
CompareResult compareResult CompareResult compareResult
) { ) {
return aiModelSettingsService.save(settings); return aiModelService.save(model);
} }
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS; return EntityType.AI_MODEL;
} }
} }

8
application/src/main/resources/thingsboard.yml

@ -656,9 +656,9 @@ cache:
trendzSettings: trendzSettings:
timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL 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 maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
aiModelSettings: aiModel:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above # Deliberately placed outside the 'specs' group above
notificationRules: notificationRules:
@ -874,7 +874,7 @@ audit-log:
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels.
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package 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. "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels.
"ai_model_settings": "${AUDIT_LOG_MASK_AI_MODEL_SETTINGS:W}" # AI model settings logging levels. "ai_model": "${AUDIT_LOG_MASK_AI_MODEL:W}" # AI model logging levels.
sink: sink:
# Type of external sink. possible options: none, elasticsearch # Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}" type: "${AUDIT_LOG_SINK_TYPE:none}"

18
common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java → common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java

@ -16,8 +16,8 @@
package org.thingsboard.server.dao.ai; package org.thingsboard.server.dao.ai;
import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Optional; import java.util.Optional;
public interface AiModelSettingsService extends EntityDaoService { public interface AiModelService extends EntityDaoService {
AiModelSettings save(AiModelSettings settings); AiModel save(AiModel model);
Optional<AiModelSettings> findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
PageData<AiModelSettings> findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
Optional<AiModelSettings> findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); Optional<AiModel> findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId);
FluentFuture<Optional<AiModelSettings>> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data;
public final class CacheConstants { public final class CacheConstants {
private CacheConstants() {}
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations"; public static final String RELATIONS_CACHE = "relations";
public static final String DEVICE_CACHE = "devices"; public static final String DEVICE_CACHE = "devices";
@ -37,7 +39,7 @@ public final class CacheConstants {
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings";
public static final String AI_MODEL_SETTINGS_CACHE = "aiModelSettings"; public static final String AI_MODEL_CACHE = "aiModel";
public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes"; public static final String ATTRIBUTES_CACHE = "attributes";

4
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -66,10 +66,10 @@ public enum EntityType {
CALCULATED_FIELD(39), CALCULATED_FIELD(39),
CALCULATED_FIELD_LINK(40), CALCULATED_FIELD_LINK(40),
JOB(41), JOB(41),
AI_MODEL_SETTINGS(42, "ai_model_settings") { AI_MODEL(42, "ai_model") {
@Override @Override
public String getNormalName() { public String getNormalName() {
return "AI model settings"; return "AI model";
} }
}; };

38
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java

@ -27,8 +27,8 @@ import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoNullChar;
@ -39,7 +39,7 @@ import java.io.Serial;
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class AiModelSettings extends BaseData<AiModelSettingsId> implements HasTenantId, HasVersion, ExportableEntity<AiModelSettingsId> { public final class AiModel extends BaseData<AiModelId> implements HasTenantId, HasVersion, ExportableEntity<AiModelId> {
@Serial @Serial
private static final long serialVersionUID = 9017108678716011604L; private static final long serialVersionUID = 9017108678716011604L;
@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY, accessMode = Schema.AccessMode.READ_ONLY,
description = "JSON object representing the ID of the tenant associated with these AI model settings", description = "JSON object representing the ID of the tenant associated with this AI model",
example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d"
) )
private TenantId tenantId; private TenantId tenantId;
@ -55,7 +55,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY, accessMode = Schema.AccessMode.READ_ONLY,
description = "Version of the AI model settings; increments automatically whenever the settings are changed", description = "Version of the AI model record; increments automatically whenever the record is changed",
example = "7", example = "7",
defaultValue = "1" defaultValue = "1"
) )
@ -67,8 +67,8 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,
description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant", description = "Display name for this AI model configuration; not the technical model identifier",
example = "Rule node assistant" example = "Fast and cost-efficient model"
) )
private String name; private String name;
@ -79,24 +79,24 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,
description = "Configuration of the AI model" description = "Configuration of the AI model"
) )
private AiModel<?> configuration; private AiModelConfig configuration;
private AiModelSettingsId externalId; private AiModelId externalId;
public AiModelSettings() {} public AiModel() {}
public AiModelSettings(AiModelSettingsId id) { public AiModel(AiModelId id) {
super(id); super(id);
} }
public AiModelSettings(AiModelSettings settings) { public AiModel(AiModel model) {
super(settings.getId()); super(model.getId());
createdTime = settings.getCreatedTime(); createdTime = model.getCreatedTime();
tenantId = settings.getTenantId(); tenantId = model.getTenantId();
version = settings.getVersion(); version = model.getVersion();
name = settings.getName(); name = model.getName();
configuration = settings.getConfiguration(); configuration = model.getConfiguration();
externalId = settings.getExternalId() == null ? null : new AiModelSettingsId(settings.getExternalId().getId()); externalId = model.getExternalId() == null ? null : new AiModelId(model.getExternalId().getId());
} }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java

@ -23,7 +23,7 @@ import dev.langchain4j.model.chat.request.ChatRequest;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -51,7 +51,7 @@ public record TbChatRequest(
description = "Configuration of the AI chat model that should execute the request" description = "Configuration of the AI chat model that should execute the request"
) )
@NotNull @Valid @NotNull @Valid
AiChatModel<?> chatModel AiChatModelConfig<?> chatModelConfig
) { ) {
public ChatRequest toLangChainChatRequest() { public ChatRequest toLangChainChatRequest() {

84
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java

@ -1,84 +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 com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel;
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.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "provider",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiChatModel.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiChatModel.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModel.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiChatModel.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModel.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModel.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModel.class, name = "GITHUB_MODELS")
})
public interface AiModel<C extends AiModelConfig> {
AiProvider provider();
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "provider"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS")
})
AiProviderConfig providerConfig();
@JsonProperty("modelType")
AiModelType modelType();
C modelConfig();
AiModel<C> withModelConfig(C config);
}

60
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java

@ -15,8 +15,66 @@
*/ */
package org.thingsboard.server.common.data.ai.model; package org.thingsboard.server.common.data.ai.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "provider",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiChatModelConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModelConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS")
})
public interface AiModelConfig { public interface AiModelConfig {
String modelId(); AiProvider provider();
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "provider"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS")
})
AiProviderConfig providerConfig();
@JsonProperty("modelType")
AiModelType modelType();
} }

41
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java

@ -1,41 +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.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, AzureOpenAiChatModel, GoogleAiGeminiChatModel,
GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel,
AmazonBedrockChatModel, GitHubModelsChatModel {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
@Override
C modelConfig();
@Override
AiChatModel<C> withModelConfig(C config);
}

15
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java

@ -15,13 +15,22 @@
*/ */
package org.thingsboard.server.common.data.ai.model.chat; package org.thingsboard.server.common.data.ai.model.chat;
import dev.langchain4j.model.chat.ChatModel;
import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.ai.model.AiModelType;
public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extends AiModelConfig public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extends AiModelConfig
permits permits
OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config { AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
Integer timeoutSeconds(); Integer timeoutSeconds();

21
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

@ -27,27 +27,22 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
public record AmazonBedrockChatModel( public record AmazonBedrockChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AmazonBedrockProviderConfig providerConfig, @NotNull @Valid AmazonBedrockProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AmazonBedrockChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.AMAZON_BEDROCK; return AiProvider.AMAZON_BEDROCK;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

23
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java

@ -27,28 +27,23 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
public record AnthropicChatModel( public record AnthropicChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AnthropicProviderConfig providerConfig, @NotNull @Valid AnthropicProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AnthropicChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.ANTHROPIC; return AiProvider.ANTHROPIC;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
public record AzureOpenAiChatModel( public record AzureOpenAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AzureOpenAiProviderConfig providerConfig, @NotNull @Valid AzureOpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AzureOpenAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.AZURE_OPENAI; return AiProvider.AZURE_OPENAI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig;
public record GitHubModelsChatModel( public record GitHubModelsChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GitHubModelsProviderConfig providerConfig, @NotNull @Valid GitHubModelsProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GitHubModelsChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GITHUB_MODELS; return AiProvider.GITHUB_MODELS;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

27
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java

@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
public record GoogleAiGeminiChatModel( public record GoogleAiGeminiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GoogleAiGeminiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GOOGLE_AI_GEMINI; return AiProvider.GOOGLE_AI_GEMINI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

27
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java

@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
public record GoogleVertexAiGeminiChatModel( public record GoogleVertexAiGeminiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GoogleVertexAiGeminiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GOOGLE_VERTEX_AI_GEMINI; return AiProvider.GOOGLE_VERTEX_AI_GEMINI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

16
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java

@ -19,20 +19,20 @@ import dev.langchain4j.model.chat.ChatModel;
public interface Langchain4jChatModelConfigurer { public interface Langchain4jChatModelConfigurer {
ChatModel configureChatModel(OpenAiChatModel chatModel); ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(AzureOpenAiChatModel chatModel); ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig);
ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel); ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig);
ChatModel configureChatModel(MistralAiChatModel chatModel); ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(AnthropicChatModel chatModel); ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig);
ChatModel configureChatModel(AmazonBedrockChatModel chatModel); ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig);
ChatModel configureChatModel(GitHubModelsChatModel chatModel); ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig);
} }

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
public record MistralAiChatModel( public record MistralAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid MistralAiProviderConfig providerConfig, @NotNull @Valid MistralAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<MistralAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.MISTRAL_AI; return AiProvider.MISTRAL_AI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
public record OpenAiChatModel( public record OpenAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid OpenAiProviderConfig providerConfig, @NotNull @Valid OpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<OpenAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.OPENAI; return AiProvider.OPENAI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

16
common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java → common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java

@ -23,29 +23,29 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
public final class AiModelSettingsId extends UUIDBased implements EntityId { public final class AiModelId extends UUIDBased implements EntityId {
@Serial @Serial
private static final long serialVersionUID = 3021036138554389754L; private static final long serialVersionUID = 3021036138554389754L;
@JsonCreator @JsonCreator
public AiModelSettingsId(@JsonProperty("id") UUID id) { public AiModelId(@JsonProperty("id") UUID id) {
super(id); super(id);
} }
@Override @Override
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
description = "Entity type of the AI model settings", description = "Entity type of the AI model",
example = "AI_MODEL_SETTINGS", example = "AI_MODEL",
allowableValues = "AI_MODEL_SETTINGS" allowableValues = "AI_MODEL"
) )
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS; return EntityType.AI_MODEL;
} }
public static AiModelSettingsId fromString(String uuid) { public static AiModelId fromString(String uuid) {
return new AiModelSettingsId(UUID.fromString(uuid)); return new AiModelId(UUID.fromString(uuid));
} }
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -85,7 +85,7 @@ public class EntityIdFactory {
case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid);
case JOB -> new JobId(uuid); case JOB -> new JobId(uuid);
case AI_MODEL_SETTINGS -> new AiModelSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid);
default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}; };
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
@ -60,7 +60,7 @@ import java.lang.annotation.Target;
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class),
@Type(name = "TB_RESOURCE", value = TbResource.class), @Type(name = "TB_RESOURCE", value = TbResource.class),
@Type(name = "AI_MODEL_SETTINGS", value = AiModelSettings.class) @Type(name = "AI_MODEL", value = AiModel.class)
}) })
@JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true)
public @interface JsonTbEntity {} public @interface JsonTbEntity {}

2
common/proto/src/main/proto/queue.proto

@ -64,7 +64,7 @@ enum EntityTypeProto {
CALCULATED_FIELD = 39; CALCULATED_FIELD = 39;
CALCULATED_FIELD_LINK = 40; CALCULATED_FIELD_LINK = 40;
JOB = 41; JOB = 41;
AI_MODEL_SETTINGS = 42; AI_MODEL = 42;
} }
enum ApiUsageRecordKeyProto { enum ApiUsageRecordKeyProto {

16
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java → dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java

@ -15,26 +15,26 @@
*/ */
package org.thingsboard.server.dao.ai; package org.thingsboard.server.dao.ai;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Deleted; import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Deleted;
import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Saved; import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Saved;
sealed interface AiModelSettingsCacheEvictEvent permits Saved, Deleted { sealed interface AiModelCacheEvictEvent permits Saved, Deleted {
AiModelSettingsCacheKey cacheKey(); AiModelCacheKey cacheKey();
record Saved(AiModelSettingsCacheKey cacheKey, AiModelSettings savedSettings) implements AiModelSettingsCacheEvictEvent { record Saved(AiModelCacheKey cacheKey, AiModel savedModel) implements AiModelCacheEvictEvent {
public Saved { public Saved {
requireNonNull(cacheKey); requireNonNull(cacheKey);
requireNonNull(savedSettings); requireNonNull(savedModel);
} }
} }
record Deleted(AiModelSettingsCacheKey cacheKey) implements AiModelSettingsCacheEvictEvent { record Deleted(AiModelCacheKey cacheKey) implements AiModelCacheEvictEvent {
public Deleted { public Deleted {
requireNonNull(cacheKey); requireNonNull(cacheKey);

18
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java → dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java

@ -17,7 +17,7 @@ package org.thingsboard.server.dao.ai;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.thingsboard.server.cache.VersionedCacheKey; import org.thingsboard.server.cache.VersionedCacheKey;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -25,22 +25,22 @@ import java.util.UUID;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements VersionedCacheKey { record AiModelCacheKey(UUID tenantId, UUID modelId) implements VersionedCacheKey {
AiModelSettingsCacheKey { AiModelCacheKey {
requireNonNull(tenantId); requireNonNull(tenantId);
requireNonNull(settingsId); requireNonNull(modelId);
if (TenantId.SYS_TENANT_ID.getId().equals(tenantId)) { if (TenantId.SYS_TENANT_ID.getId().equals(tenantId)) {
throw new IllegalArgumentException("Tenant ID must not be the system tenant ID"); throw new IllegalArgumentException("Tenant ID must not be the system tenant ID");
} }
if (EntityId.NULL_UUID.equals(settingsId)) { if (EntityId.NULL_UUID.equals(modelId)) {
throw new IllegalArgumentException("Settings ID must not be reserved null UUID"); throw new IllegalArgumentException("Model ID must not be reserved null UUID");
} }
} }
static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) { static AiModelCacheKey of(TenantId tenantId, AiModelId modelId) {
return new AiModelSettingsCacheKey(tenantId.getId(), settingsId.getId()); return new AiModelCacheKey(tenantId.getId(), modelId.getId());
} }
@Override @Override
@ -51,7 +51,7 @@ record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements Versio
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return /* cache name */ "_" + tenantId + "_" + settingsId; return /* cache name */ "_" + tenantId + "_" + modelId;
} }
} }

10
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java → dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java

@ -20,14 +20,14 @@ import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.VersionedCaffeineTbCache; import org.thingsboard.server.cache.VersionedCaffeineTbCache;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
@Component("AiModelSettingsCache") @Component("AiModelCache")
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache<AiModelSettingsCacheKey, AiModelSettings> { class AiModelCaffeineCache extends VersionedCaffeineTbCache<AiModelCacheKey, AiModel> {
AiModelSettingsCaffeineCache(CacheManager cacheManager) { AiModelCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE); super(cacheManager, CacheConstants.AI_MODEL_CACHE);
} }
} }

14
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java → dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java

@ -15,8 +15,8 @@
*/ */
package org.thingsboard.server.dao.ai; package org.thingsboard.server.dao.ai;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.TenantEntityDao;
@ -24,14 +24,14 @@ import org.thingsboard.server.dao.TenantEntityDao;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
public interface AiModelSettingsDao extends TenantEntityDao<AiModelSettings>, ExportableEntityDao<AiModelSettingsId, AiModelSettings> { public interface AiModelDao extends TenantEntityDao<AiModel>, ExportableEntityDao<AiModelId, AiModel> {
Optional<AiModelSettings> findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); Optional<AiModel> findByTenantIdAndId(TenantId tenantId, AiModelId modelId);
boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); boolean deleteById(TenantId tenantId, AiModelId modelId);
Set<AiModelSettingsId> deleteByTenantId(TenantId tenantId); Set<AiModelId> deleteByTenantId(TenantId tenantId);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
} }

10
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java → dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java

@ -23,14 +23,14 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer; import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.cache.VersionedRedisTbCache;
import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
@Component("AiSettingsCache") @Component("AiModelCache")
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
class AiModelSettingsRedisCache extends VersionedRedisTbCache<AiModelSettingsCacheKey, AiModelSettings> { class AiModelRedisCache extends VersionedRedisTbCache<AiModelCacheKey, AiModel> {
AiModelSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { AiModelRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.AI_MODEL_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModelSettings.class)); super(CacheConstants.AI_MODEL_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModel.class));
} }
} }

149
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java

@ -0,0 +1,149 @@
/**
* 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
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.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
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.entity.CachedVersionedEntityService;
import org.thingsboard.server.dao.model.sql.AiModelEntity;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.Optional;
import java.util.Set;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@RequiredArgsConstructor
class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, AiModel, AiModelCacheEvictEvent> implements AiModelService {
private final DataValidator<AiModel> aiModelValidator;
private final JpaExecutorService jpaExecutor;
private final AiModelDao aiModelDao;
@Override
@TransactionalEventListener
public void handleEvictEvent(AiModelCacheEvictEvent event) {
var cacheKey = event.cacheKey();
if (event instanceof AiModelCacheEvictEvent.Saved savedEvent) {
cache.put(cacheKey, savedEvent.savedModel());
} else if (event instanceof AiModelCacheEvictEvent.Deleted) {
cache.evict(cacheKey);
} else {
throw new UnsupportedOperationException("Unsupported event type: " + event.getClass().getSimpleName());
}
}
@Override
@Transactional
public AiModel save(AiModel model) {
aiModelValidator.validate(model, AiModel::getTenantId);
AiModel savedModel;
try {
savedModel = aiModelDao.saveAndFlush(model.getTenantId(), model);
} catch (Exception e) {
checkConstraintViolation(e,
"ai_model_name_unq_key", "AI model with such name already exist!",
"ai_model_external_id_unq_key", "AI model with such external ID already exists!");
throw e;
}
var cacheKey = AiModelCacheKey.of(savedModel.getTenantId(), savedModel.getId());
publishEvictEvent(new AiModelCacheEvictEvent.Saved(cacheKey, savedModel));
return savedModel;
}
@Override
public Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId) {
return Optional.ofNullable(aiModelDao.findById(tenantId, modelId.getId()));
}
@Override
public PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink) {
validatePageLink(pageLink, AiModelEntity.ALLOWED_SORT_PROPERTIES);
return aiModelDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public Optional<AiModel> findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
var cacheKey = AiModelCacheKey.of(tenantId, modelId);
return Optional.ofNullable(cache.get(cacheKey, () -> aiModelDao.findByTenantIdAndId(tenantId, modelId).orElse(null)));
}
@Override
public FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId) {
return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId)));
}
@Override
@Transactional
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return deleteByTenantIdAndIdInternal(tenantId, modelId);
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return findAiModelByTenantIdAndId(tenantId, (AiModelId) entityId)
.map(model -> model); // necessary to cast to HasId<?>
}
@Override
public long countByTenantId(TenantId tenantId) {
return aiModelDao.countByTenantId(tenantId);
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteByTenantIdAndIdInternal(tenantId, new AiModelId(id.getId()));
}
private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelId modelId) {
boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, modelId);
if (deleted) {
publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, modelId)));
}
return deleted;
}
@Override
@Transactional
public void deleteByTenantId(TenantId tenantId) {
Set<AiModelId> deleted = aiModelDao.deleteByTenantId(tenantId);
deleted.forEach(id -> publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, id))));
}
@Override
public EntityType getEntityType() {
return EntityType.AI_MODEL;
}
}

149
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java

@ -1,149 +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.ai;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
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.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;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.CachedVersionedEntityService;
import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.Optional;
import java.util.Set;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@RequiredArgsConstructor
class AiModelSettingsServiceImpl extends CachedVersionedEntityService<AiModelSettingsCacheKey, AiModelSettings, AiModelSettingsCacheEvictEvent> implements AiModelSettingsService {
private final DataValidator<AiModelSettings> aiModelSettingsValidator;
private final JpaExecutorService jpaExecutor;
private final AiModelSettingsDao aiModelSettingsDao;
@Override
@TransactionalEventListener
public void handleEvictEvent(AiModelSettingsCacheEvictEvent event) {
var cacheKey = event.cacheKey();
if (event instanceof AiModelSettingsCacheEvictEvent.Saved savedEvent) {
cache.put(cacheKey, savedEvent.savedSettings());
} else if (event instanceof AiModelSettingsCacheEvictEvent.Deleted) {
cache.evict(cacheKey);
} else {
throw new UnsupportedOperationException("Unsupported event type: " + event.getClass().getSimpleName());
}
}
@Override
@Transactional
public AiModelSettings save(AiModelSettings settings) {
aiModelSettingsValidator.validate(settings, AiModelSettings::getTenantId);
AiModelSettings savedSettings;
try {
savedSettings = aiModelSettingsDao.saveAndFlush(settings.getTenantId(), settings);
} catch (Exception e) {
checkConstraintViolation(e,
"ai_model_settings_name_unq_key", "AI model settings with such name already exist!",
"ai_model_settings_external_id_unq_key", "AI model settings with such external ID already exist!");
throw e;
}
var cacheKey = AiModelSettingsCacheKey.of(savedSettings.getTenantId(), savedSettings.getId());
publishEvictEvent(new AiModelSettingsCacheEvictEvent.Saved(cacheKey, savedSettings));
return savedSettings;
}
@Override
public Optional<AiModelSettings> findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) {
return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId()));
}
@Override
public PageData<AiModelSettings> findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) {
validatePageLink(pageLink, AiModelSettingsEntity.ALLOWED_SORT_PROPERTIES);
return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink);
}
@Override
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<AiModelSettings>> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId) {
return FluentFuture.from(jpaExecutor.submit(() -> findAiModelSettingsByTenantIdAndId(tenantId, settingsId)));
}
@Override
@Transactional
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) {
return deleteByTenantIdAndIdInternal(tenantId, settingsId);
}
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return findAiModelSettingsByTenantIdAndId(tenantId, (AiModelSettingsId) entityId)
.map(settings -> settings); // necessary to cast to HasId<?>
}
@Override
public long countByTenantId(TenantId tenantId) {
return aiModelSettingsDao.countByTenantId(tenantId);
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteByTenantIdAndIdInternal(tenantId, new AiModelSettingsId(id.getId()));
}
private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelSettingsId settingsId) {
boolean deleted = aiModelSettingsDao.deleteByTenantIdAndId(tenantId, settingsId);
if (deleted) {
publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, settingsId)));
}
return deleted;
}
@Override
@Transactional
public void deleteByTenantId(TenantId tenantId) {
Set<AiModelSettingsId> deleted = aiModelSettingsDao.deleteByTenantId(tenantId);
deleted.forEach(id -> publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, id))));
}
@Override
public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS;
}
}

2
dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java

@ -47,7 +47,7 @@ public class CleanUpService {
private final Set<EntityType> skippedEntities = EnumSet.of( private final Set<EntityType> skippedEntities = EnumSet.of(
EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL_SETTINGS EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL
); );
@TransactionalEventListener(fallbackExecution = true) // after transaction commit @TransactionalEventListener(fallbackExecution = true) // after transaction commit

10
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -752,12 +752,12 @@ public class ModelConstants {
public static final String JOB_RESULT_PROPERTY = "result"; public static final String JOB_RESULT_PROPERTY = "result";
/** /**
* AI model settings constants. * AI model constants.
*/ */
public static final String AI_MODEL_SETTINGS_TABLE_NAME = "ai_model_settings"; public static final String AI_MODEL_TABLE_NAME = "ai_model";
public static final String AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; public static final String AI_MODEL_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_NAME_COLUMN_NAME = NAME_PROPERTY;
public static final String AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration"; public static final String AI_MODEL_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}; 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};

54
dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java → dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java

@ -24,9 +24,9 @@ import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.BaseVersionedEntity;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
@ -43,52 +43,52 @@ import java.util.UUID;
@Setter @Setter
@ToString @ToString
@Entity @Entity
@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME) @Table(name = ModelConstants.AI_MODEL_TABLE_NAME)
public class AiModelSettingsEntity extends BaseVersionedEntity<AiModelSettings> { public class AiModelEntity extends BaseVersionedEntity<AiModel> {
public static final Map<String, String> COLUMN_MAP = Map.of( public static final Map<String, String> COLUMN_MAP = Map.of(
"createdTime", "created_time", "createdTime", "created_time",
"provider", "(configuration ->> 'provider')", "provider", "(configuration ->> 'provider')",
"modelId", "(configuration -> 'modelConfig' ->> 'modelId')" "modelId", "(configuration ->> 'modelId')"
); );
public static final Set<String> ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( public static final Set<String> ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet(
new LinkedHashSet<>(List.of("createdTime", "name", "provider", "modelId")) new LinkedHashSet<>(List.of("createdTime", "name", "provider", "modelId"))
); );
@Column(name = ModelConstants.AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") @Column(name = ModelConstants.AI_MODEL_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID")
private UUID tenantId; private UUID tenantId;
@Column(name = ModelConstants.AI_MODEL_SETTINGS_NAME_COLUMN_NAME, nullable = false) @Column(name = ModelConstants.AI_MODEL_NAME_COLUMN_NAME, nullable = false)
private String name; private String name;
@Type(JsonBinaryType.class) @Type(JsonBinaryType.class)
@Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") @Column(name = ModelConstants.AI_MODEL_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiModel<?> configuration; private AiModelConfig configuration;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID") @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID")
private UUID externalId; private UUID externalId;
public AiModelSettingsEntity() {} public AiModelEntity() {}
public AiModelSettingsEntity(AiModelSettings aiModelSettings) { public AiModelEntity(AiModel aiModel) {
super(aiModelSettings); super(aiModel);
tenantId = getTenantUuid(aiModelSettings.getTenantId()); tenantId = getTenantUuid(aiModel.getTenantId());
name = aiModelSettings.getName(); name = aiModel.getName();
configuration = aiModelSettings.getConfiguration(); configuration = aiModel.getConfiguration();
externalId = getUuid(aiModelSettings.getExternalId()); externalId = getUuid(aiModel.getExternalId());
} }
@Override @Override
public AiModelSettings toData() { public AiModel toData() {
var settings = new AiModelSettings(new AiModelSettingsId(id)); var model = new AiModel(new AiModelId(id));
settings.setCreatedTime(createdTime); model.setCreatedTime(createdTime);
settings.setVersion(version); model.setVersion(version);
settings.setTenantId(TenantId.fromUUID(tenantId)); model.setTenantId(TenantId.fromUUID(tenantId));
settings.setName(name); model.setName(name);
settings.setConfiguration(configuration); model.setConfiguration(configuration);
settings.setExternalId(getEntityId(externalId, AiModelSettingsId::new)); model.setExternalId(getEntityId(externalId, AiModelId::new));
return settings; return model;
} }
@Override @Override
@ -98,7 +98,7 @@ public class AiModelSettingsEntity extends BaseVersionedEntity<AiModelSettings>
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false; if (thisEffectiveClass != oEffectiveClass) return false;
AiModelSettingsEntity that = (AiModelSettingsEntity) o; AiModelEntity that = (AiModelEntity) o;
return getId() != null && Objects.equals(getId(), that.getId()); return getId() != null && Objects.equals(getId(), that.getId());
} }

36
dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java → dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java

@ -17,9 +17,9 @@ package org.thingsboard.server.dao.service.validator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.ai.AiModelSettingsDao; import org.thingsboard.server.dao.ai.AiModelDao;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -28,41 +28,41 @@ import java.util.Optional;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
class AiModelSettingsDataValidator extends DataValidator<AiModelSettings> { class AiModelDataValidator extends DataValidator<AiModel> {
private final TenantService tenantService; private final TenantService tenantService;
private final AiModelSettingsDao aiModelSettingsDao; private final AiModelDao aiModelDao;
@Override @Override
protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { protected AiModel validateUpdate(TenantId tenantId, AiModel model) {
Optional<AiModelSettings> existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); Optional<AiModel> existing = aiModelDao.findByTenantIdAndId(tenantId, model.getId());
if (existing.isEmpty()) { if (existing.isEmpty()) {
throw new DataValidationException("Cannot update non-existent AI model settings!"); throw new DataValidationException("Cannot update non-existent AI model!");
} }
return existing.get(); return existing.get();
} }
@Override @Override
protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) { protected void validateDataImpl(TenantId tenantId, AiModel model) {
// ID validation // ID validation
if (settings.getId() != null) { if (model.getId() != null) {
if (settings.getUuidId() == null) { if (model.getUuidId() == null) {
throw new DataValidationException("AI model settings UUID should be specified!"); throw new DataValidationException("AI model UUID should be specified!");
} }
if (settings.getId().isNullUid()) { if (model.getId().isNullUid()) {
throw new DataValidationException("AI model settings UUID must not be the reserved null value!"); throw new DataValidationException("AI model UUID must not be the reserved null value!");
} }
} }
// tenant ID validation // tenant ID validation
if (settings.getTenantId() == null || settings.getTenantId().getId() == null) { if (model.getTenantId() == null || model.getTenantId().getId() == null) {
throw new DataValidationException("AI model settings should be assigned to tenant!"); throw new DataValidationException("AI model should be assigned to tenant!");
} }
if (settings.getTenantId().isSysTenantId()) { if (model.getTenantId().isSysTenantId()) {
throw new DataValidationException("AI model settings cannot be assigned to the system tenant!"); throw new DataValidationException("AI model cannot be assigned to the system tenant!");
} }
if (!tenantService.tenantExists(tenantId)) { if (!tenantService.tenantExists(tenantId)) {
throw new DataValidationException("AI model settings reference a non-existent tenant!"); throw new DataValidationException("AI model reference a non-existent tenant!");
} }
} }

40
dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java → dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java

@ -23,58 +23,58 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.model.sql.AiModelEntity;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
interface AiModelSettingsRepository extends JpaRepository<AiModelSettingsEntity, UUID>, ExportableEntityRepository<AiModelSettingsEntity> { interface AiModelRepository extends JpaRepository<AiModelEntity, UUID>, ExportableEntityRepository<AiModelEntity> {
Optional<AiModelSettingsEntity> findByTenantIdAndId(UUID tenantId, UUID id); Optional<AiModelEntity> findByTenantIdAndId(UUID tenantId, UUID id);
Optional<AiModelSettingsEntity> findByTenantIdAndName(UUID tenantId, String name); Optional<AiModelEntity> findByTenantIdAndName(UUID tenantId, String name);
@Query( @Query(
value = """ value = """
SELECT * SELECT *
FROM ai_model_settings ai_model FROM ai_model model
WHERE ai_model.tenant_id = :tenantId WHERE model.tenant_id = :tenantId
AND (:textSearch IS NULL AND (:textSearch IS NULL
OR ai_model.name ILIKE '%' || :textSearch || '%' OR model.name ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%')
""", """,
countQuery = """ countQuery = """
SELECT COUNT(*) SELECT COUNT(*)
FROM ai_model_settings ai_model FROM ai_model model
WHERE ai_model.tenant_id = :tenantId WHERE model.tenant_id = :tenantId
AND (:textSearch IS NULL AND (:textSearch IS NULL
OR ai_model.name ILIKE '%' || :textSearch || '%' OR model.name ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%')
""", """,
nativeQuery = true nativeQuery = true
) )
Page<AiModelSettingsEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); Page<AiModelEntity> findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable);
@Query("SELECT ai_model.id FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId") @Query("SELECT ai_model.id FROM AiModelEntity ai_model WHERE ai_model.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
@Query("SELECT externalId FROM AiModelSettingsEntity WHERE id = :id") @Query("SELECT externalId FROM AiModelEntity WHERE id = :id")
Optional<UUID> getExternalIdById(@Param("id") UUID id); Optional<UUID> getExternalIdById(@Param("id") UUID id);
long countByTenantId(UUID tenantId); long countByTenantId(UUID tenantId);
@Transactional @Transactional
@Modifying @Modifying
@Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.id IN (:ids)") @Query("DELETE FROM AiModelEntity ai_model WHERE ai_model.id IN (:ids)")
int deleteByIdIn(@Param("ids") Set<UUID> ids); int deleteByIdIn(@Param("ids") Set<UUID> ids);
@Transactional @Transactional
@Modifying @Modifying
@Query(value = """ @Query(value = """
DELETE FROM ai_model_settings DELETE FROM ai_model
WHERE tenant_id = :tenantId WHERE tenant_id = :tenantId
RETURNING id RETURNING id
""", nativeQuery = true """, nativeQuery = true
@ -83,7 +83,7 @@ interface AiModelSettingsRepository extends JpaRepository<AiModelSettingsEntity,
@Transactional @Transactional
@Modifying @Modifying
@Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId AND ai_model.id IN (:ids)") @Query("DELETE FROM AiModelEntity ai_model WHERE ai_model.tenantId = :tenantId AND ai_model.id IN (:ids)")
int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set<UUID> ids); int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set<UUID> ids);
} }

66
dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java → dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java

@ -23,15 +23,15 @@ import org.springframework.data.jpa.domain.JpaSort;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.ai.AiModelSettingsDao; import org.thingsboard.server.dao.ai.AiModelDao;
import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; import org.thingsboard.server.dao.model.sql.AiModelEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
@ -44,40 +44,40 @@ import static java.util.stream.Collectors.toSet;
@SqlDao @SqlDao
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
class JpaAiModelSettingsDao extends JpaAbstractDao<AiModelSettingsEntity, AiModelSettings> implements AiModelSettingsDao { class JpaAiModelDao extends JpaAbstractDao<AiModelEntity, AiModel> implements AiModelDao {
private final AiModelSettingsRepository aiModelSettingsRepository; private final AiModelRepository aiModelRepository;
@Override @Override
public Optional<AiModelSettings> findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { public Optional<AiModel> findByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData); return aiModelRepository.findByTenantIdAndId(tenantId.getId(), modelId.getId()).map(DaoUtil::getData);
} }
@Override @Override
public AiModelSettings findByTenantIdAndName(UUID tenantId, String name) { public AiModel findByTenantIdAndName(UUID tenantId, String name) {
return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndName(tenantId, name)); return DaoUtil.getData(aiModelRepository.findByTenantIdAndName(tenantId, name));
} }
@Override @Override
public AiModelSettings findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { public AiModel findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndExternalId(tenantId, externalId)); return DaoUtil.getData(aiModelRepository.findByTenantIdAndExternalId(tenantId, externalId));
} }
@Override @Override
public PageData<AiModelSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink) { public PageData<AiModel> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return findByTenantId(tenantId.getId(), pageLink); return findByTenantId(tenantId.getId(), pageLink);
} }
@Override @Override
public PageData<AiModelSettings> findByTenantId(UUID tenantId, PageLink pageLink) { public PageData<AiModel> findByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( return DaoUtil.toPageData(aiModelRepository.findByTenantId(
tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink))
); );
} }
@Override @Override
public PageData<AiModelSettingsId> findIdsByTenantId(UUID tenantId, PageLink pageLink) { public PageData<AiModelId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new)); return DaoUtil.pageToPageData(aiModelRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelId::new));
} }
private static PageRequest toPageRequest(PageLink pageLink) { private static PageRequest toPageRequest(PageLink pageLink) {
@ -88,52 +88,52 @@ class JpaAiModelSettingsDao extends JpaAbstractDao<AiModelSettingsEntity, AiMode
} else { } else {
sort = JpaSort.unsafe( sort = JpaSort.unsafe(
Sort.Direction.fromString(sortOrder.getDirection().name()), Sort.Direction.fromString(sortOrder.getDirection().name()),
AiModelSettingsEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty()) AiModelEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty())
); );
} }
return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort);
} }
@Override @Override
public AiModelSettingsId getExternalIdByInternal(AiModelSettingsId internalId) { public AiModelId getExternalIdByInternal(AiModelId internalId) {
return aiModelSettingsRepository.getExternalIdById(internalId.getId()).map(AiModelSettingsId::new).orElse(null); return aiModelRepository.getExternalIdById(internalId.getId()).map(AiModelId::new).orElse(null);
} }
@Override @Override
public Long countByTenantId(TenantId tenantId) { public Long countByTenantId(TenantId tenantId) {
return aiModelSettingsRepository.countByTenantId(tenantId.getId()); return aiModelRepository.countByTenantId(tenantId.getId());
} }
@Override @Override
public boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId) { public boolean deleteById(TenantId tenantId, AiModelId modelId) {
return aiModelSettingsRepository.deleteByIdIn(Set.of(settingsId.getId())) > 0; return aiModelRepository.deleteByIdIn(Set.of(modelId.getId())) > 0;
} }
@Override @Override
public Set<AiModelSettingsId> deleteByTenantId(TenantId tenantId) { public Set<AiModelId> deleteByTenantId(TenantId tenantId) {
return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()).stream() return aiModelRepository.deleteByTenantId(tenantId.getId()).stream()
.map(AiModelSettingsId::new) .map(AiModelId::new)
.collect(toSet()); .collect(toSet());
} }
@Override @Override
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return aiModelSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(settingsId.getId())) > 0; return aiModelRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(modelId.getId())) > 0;
} }
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS; return EntityType.AI_MODEL;
} }
@Override @Override
protected Class<AiModelSettingsEntity> getEntityClass() { protected Class<AiModelEntity> getEntityClass() {
return AiModelSettingsEntity.class; return AiModelEntity.class;
} }
@Override @Override
protected JpaRepository<AiModelSettingsEntity, UUID> getRepository() { protected JpaRepository<AiModelEntity, UUID> getRepository() {
return aiModelSettingsRepository; return aiModelRepository;
} }
} }

2
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER, EntityType.NOTIFICATION_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER,
EntityType.DOMAIN, EntityType.MOBILE_APP_BUNDLE, EntityType.MOBILE_APP, EntityType.OAUTH2_CLIENT, EntityType.DOMAIN, EntityType.MOBILE_APP_BUNDLE, EntityType.MOBILE_APP, EntityType.OAUTH2_CLIENT,
EntityType.AI_MODEL_SETTINGS EntityType.AI_MODEL
); );
} }

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -132,4 +132,4 @@ CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tena
CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id); CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id);
CREATE INDEX IF NOT EXISTS idx_ai_model_settings_tenant_id ON ai_model_settings(tenant_id); CREATE INDEX IF NOT EXISTS idx_ai_model_tenant_id ON ai_model(tenant_id);

6
dao/src/main/resources/sql/schema-entities.sql

@ -962,7 +962,7 @@ CREATE TABLE IF NOT EXISTS job (
result varchar result varchar
); );
CREATE TABLE IF NOT EXISTS ai_model_settings ( CREATE TABLE IF NOT EXISTS ai_model (
id UUID NOT NULL PRIMARY KEY, id UUID NOT NULL PRIMARY KEY,
external_id UUID, external_id UUID,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
@ -970,6 +970,6 @@ CREATE TABLE IF NOT EXISTS ai_model_settings (
version BIGINT NOT NULL DEFAULT 1, version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL, configuration JSONB NOT NULL,
CONSTRAINT ai_model_settings_name_unq_key UNIQUE (tenant_id, name), CONSTRAINT ai_model_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT ai_model_settings_external_id_unq_key UNIQUE (tenant_id, external_id) CONSTRAINT ai_model_external_id_unq_key UNIQUE (tenant_id, external_id)
); );

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiModelService.java → rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiChatModelService.java

@ -18,11 +18,10 @@ package org.thingsboard.rule.engine.api;
import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.ChatResponse;
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.AiChatModelConfig;
public interface RuleEngineAiModelService { public interface RuleEngineAiChatModelService {
<C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest); <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest);
} }

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -423,9 +423,9 @@ public interface TbContext {
AuditLogService getAuditLogService(); AuditLogService getAuditLogService();
RuleEngineAiModelService getAiModelService(); RuleEngineAiChatModelService getAiChatModelService();
AiModelSettingsService getAiModelSettingsService(); AiModelService getAiModelService();
// Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node

43
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java

@ -33,11 +33,10 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModelType; 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.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -65,7 +64,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
private String userPrompt; private String userPrompt;
private ResponseFormat responseFormat; private ResponseFormat responseFormat;
private int timeoutSeconds; private int timeoutSeconds;
private AiModelSettingsId modelSettingsId; private AiModelId modelId;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
@ -79,7 +78,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
throw new TbNodeException(e, true); throw new TbNodeException(e, true);
} }
// LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT
if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) { if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) {
responseFormat = config.getResponseFormat().toLangChainResponseFormat(); responseFormat = config.getResponseFormat().toLangChainResponseFormat();
} }
@ -87,15 +86,15 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
systemPrompt = config.getSystemPrompt(); systemPrompt = config.getSystemPrompt();
userPrompt = config.getUserPrompt(); userPrompt = config.getUserPrompt();
timeoutSeconds = config.getTimeoutSeconds(); timeoutSeconds = config.getTimeoutSeconds();
modelSettingsId = config.getAiModelSettingsId(); modelId = config.getAiModelId();
Optional<AiModelSettings> modelSettings = ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndId(ctx.getTenantId(), modelSettingsId); Optional<AiModel> model = ctx.getAiModelService().findAiModelByTenantIdAndId(ctx.getTenantId(), modelId);
if (modelSettings.isEmpty()) { if (model.isEmpty()) {
throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found", true); throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found", true);
} }
AiModelType modelType = modelSettings.get().getConfiguration().modelType(); AiModelType modelType = model.get().getConfiguration().modelType();
if (modelType != AiModelType.CHAT) { if (modelType != AiModelType.CHAT) {
throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType, true); throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType, true);
} }
} }
@ -134,24 +133,24 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
} }
private <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(TbContext ctx, ChatRequest chatRequest) { private <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(TbContext ctx, ChatRequest chatRequest) {
return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transformAsync(settingsOpt -> { return ctx.getAiModelService().findAiModelByTenantIdAndIdAsync(ctx.getTenantId(), modelId).transformAsync(modelOpt -> {
if (settingsOpt.isEmpty()) { if (modelOpt.isEmpty()) {
throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found"); throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found");
} }
AiModelSettings settings = settingsOpt.get(); AiModel model = modelOpt.get();
AiModelType modelType = settings.getConfiguration().modelType(); AiModelType modelType = model.getConfiguration().modelType();
if (modelType != AiModelType.CHAT) { if (modelType != AiModelType.CHAT) {
throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType); throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
AiChatModel<C> chatModel = (AiChatModel<C>) settingsOpt.get().getConfiguration(); AiChatModelConfig<C> chatModelConfig = (AiChatModelConfig<C>) model.getConfiguration();
chatModel = chatModel.withModelConfig(chatModel.modelConfig() chatModelConfig = chatModelConfig
.withTimeoutSeconds(timeoutSeconds) .withTimeoutSeconds(timeoutSeconds)
.withMaxRetries(0)); // disable retries to respect timeout set in rule node config .withMaxRetries(0); // disable retries to respect timeout set in rule node config
return ctx.getAiModelService().sendChatRequestAsync(chatModel, chatRequest); return ctx.getAiChatModelService().sendChatRequestAsync(chatModelConfig, chatRequest);
}, ctx.getDbCallbackExecutor()); }, ctx.getDbCallbackExecutor());
} }
@ -174,7 +173,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
systemPrompt = null; systemPrompt = null;
userPrompt = null; userPrompt = null;
responseFormat = null; responseFormat = null;
modelSettingsId = null; modelId = null;
} }
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java

@ -23,7 +23,7 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
@ -32,7 +32,7 @@ import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseForm
public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfiguration> { public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfiguration> {
@NotNull @NotNull
private AiModelSettingsId aiModelSettingsId; private AiModelId aiModelId;
@Pattern(regexp = ".*\\S.*", message = "must not be blank") @Pattern(regexp = ".*\\S.*", message = "must not be blank")
@Length(min = 1, max = 10000) @Length(min = 1, max = 10000)

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java

@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -180,8 +180,8 @@ public class TenantIdLoader {
case JOB: case JOB:
tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id));
break; break;
case AI_MODEL_SETTINGS: case AI_MODEL:
tenantEntity = ctx.getAiModelSettingsService().findAiModelSettingsById(ctxTenantId, new AiModelSettingsId(id)).orElse(null); tenantEntity = ctx.getAiModelService().findAiModelById(ctxTenantId, new AiModelId(id)).orElse(null);
break; break;
default: default:
throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType());

16
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
@ -70,7 +70,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
@ -168,7 +168,7 @@ public class TenantIdLoaderTest {
@Mock @Mock
private JobService jobService; private JobService jobService;
@Mock @Mock
private AiModelSettingsService aiModelSettingsService; private AiModelService aiModelService;
private TenantId tenantId; private TenantId tenantId;
private TenantProfileId tenantProfileId; private TenantProfileId tenantProfileId;
@ -434,11 +434,11 @@ public class TenantIdLoaderTest {
when(ctx.getJobService()).thenReturn(jobService); when(ctx.getJobService()).thenReturn(jobService);
doReturn(job).when(jobService).findJobById(eq(tenantId), any()); doReturn(job).when(jobService).findJobById(eq(tenantId), any());
break; break;
case AI_MODEL_SETTINGS: case AI_MODEL:
AiModelSettings aiModelSettings = new AiModelSettings(); AiModel aiModel = new AiModel();
aiModelSettings.setTenantId(tenantId); aiModel.setTenantId(tenantId);
when(ctx.getAiModelSettingsService()).thenReturn(aiModelSettingsService); when(ctx.getAiModelService()).thenReturn(aiModelService);
doReturn(Optional.of(aiModelSettings)).when(aiModelSettingsService).findAiModelSettingsById(eq(tenantId), any()); doReturn(Optional.of(aiModel)).when(aiModelService).findAiModelById(eq(tenantId), any());
break; break;
default: default:
throw new RuntimeException("Unexpected originator EntityType " + entityType); throw new RuntimeException("Unexpected originator EntityType " + entityType);

Loading…
Cancel
Save