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.MqttClientSettings;
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.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -63,7 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.ai.AiModelSettingsService;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -315,11 +315,11 @@ public class ActorSystemContext {
@Autowired
@Getter
private RuleEngineAiModelService aiModelService;
private RuleEngineAiChatModelService aiChatModelService;
@Autowired
@Getter
private AiModelSettingsService aiModelSettingsService;
private AiModelService aiModelService;
@Autowired
@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.MqttClientSettings;
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.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -77,7 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.ai.AiModelSettingsService;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -1027,13 +1027,13 @@ public class DefaultTbContext implements TbContext {
}
@Override
public RuleEngineAiModelService getAiModelService() {
return mainCtx.getAiModelService();
public RuleEngineAiChatModelService getAiChatModelService() {
return mainCtx.getAiChatModelService();
}
@Override
public AiModelSettingsService getAiModelSettingsService() {
return mainCtx.getAiModelSettingsService();
public AiModelService getAiModelService() {
return mainCtx.getAiModelService();
}
@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 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 lombok.RequiredArgsConstructor;
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.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.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.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.util.Optional;
import java.util.UUID;
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;
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/ai/model")
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(
value = "Send request to AI chat model (sendChatRequest)",
@ -53,13 +163,13 @@ class AiModelController extends BaseController {
@PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
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())
.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);
}

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.TenantProfile;
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.AlarmComment;
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.ThingsboardErrorCode;
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.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
@ -131,7 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelSettingsService;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService;
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.component.ComponentDiscoveryService;
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.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -383,10 +383,10 @@ public abstract class BaseController {
protected CalculatedFieldService calculatedFieldService;
@Autowired
protected AiModelSettingsService aiModelSettingsService;
protected AiModelService aiModelService;
@Autowired
protected TbAiModelSettingsService tbAiModelSettingsService;
protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}")
@Getter
@ -400,7 +400,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e);
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();
} else {
e = thingsboardException;
@ -448,7 +448,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} 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);
} else if (exception instanceof MessagingException) {
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_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(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);
};
} catch (Exception e) {
@ -848,8 +848,8 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation);
}
AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation);
AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation);
}
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 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 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 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;
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 lombok.RequiredArgsConstructor;
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.Langchain4jChatModelConfigurer;
@Service
@RequiredArgsConstructor
class AiModelServiceImpl implements AiModelService {
class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor;
@Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest) {
ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest);
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
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.PredictionServiceSettings;
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.chat.ChatModel;
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 org.springframework.stereotype.Component;
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.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.Langchain4jChatModelConfigurer;
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.model.chat.MistralAiChatModelConfig;
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.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
@ -54,61 +60,57 @@ import java.time.Duration;
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override
public ChatModel configureChatModel(OpenAiChatModel chatModel) {
OpenAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.openai.OpenAiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) {
AzureOpenAiProviderConfig providerConfig = chatModel.providerConfig();
AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder()
public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
return AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey())
.deploymentName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
.deploymentName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) {
GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.topK(modelConfig.topK())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxOutputTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
return GoogleAiGeminiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig();
GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
// construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials;
@ -131,8 +133,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.toBuilder();
// set request timeout from model config
if (modelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds()));
if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
@ -154,30 +156,30 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder();
if (modelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue());
if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
}
if (modelConfig.topP() != null) {
generationConfigBuilder.setTopP(modelConfig.topP().floatValue());
if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
}
if (modelConfig.topK() != null) {
generationConfigBuilder.setTopK(modelConfig.topK());
if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(chatModelConfig.topK());
}
if (modelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue());
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (modelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue());
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (modelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens());
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
}
var generationConfig = generationConfigBuilder.build();
// 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) {
@ -189,40 +191,37 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
}
@Override
public ChatModel configureChatModel(MistralAiChatModel chatModel) {
MistralAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.mistralai.MistralAiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) {
return MistralAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AnthropicChatModel chatModel) {
AnthropicChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.anthropic.AnthropicChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.topK(modelConfig.topK())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) {
return AnthropicChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) {
AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig();
AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig();
public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
@ -234,33 +233,32 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build();
var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.maxOutputTokens(modelConfig.maxOutputTokens())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.build();
return BedrockChatModel.builder()
.client(bedrockClient)
.modelId(modelConfig.modelId())
.modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GitHubModelsChatModel chatModel) {
GitHubModelsChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.github.GitHubModelsChatModel.builder()
.gitHubToken(chatModel.providerConfig().personalAccessToken())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
return GitHubModelsChatModel.builder()
.gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}

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

@ -112,7 +112,7 @@ public class EdgeEventSourcingListener {
return;
}
try {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
return;
}
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -226,7 +226,7 @@ public class EdgeEventSourcingListener {
break;
case TENANT:
return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS:
case API_USAGE_STATE, EDGE, AI_MODEL:
return false;
case 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(
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) {
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.thingsboard.server.common.data.EntityType;
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.dao.ai.AiModelSettingsService;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -30,48 +30,48 @@ import static java.util.Objects.requireNonNullElseGet;
@Service
@TbCoreComponent
@RequiredArgsConstructor
class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService {
class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelSettingsService aiModelSettingsService;
private final AiModelService aiModelService;
@Override
public AiModelSettings save(AiModelSettings settings, User user) {
var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
public AiModel save(AiModel model, User user) {
var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId();
settings.setTenantId(tenantId);
model.setTenantId(tenantId);
AiModelSettings savedSettings;
AiModel savedModel;
try {
savedSettings = aiModelSettingsService.save(settings);
autoCommit(user, savedSettings.getId());
savedModel = aiModelService.save(model);
autoCommit(user, savedModel.getId());
} 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;
}
logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user);
logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedSettings;
return savedModel;
}
@Override
public boolean delete(AiModelSettings settings, User user) {
public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED;
var tenantId = user.getTenantId();
var settingsId = settings.getId();
var modelId = model.getId();
boolean deleted;
try {
deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId);
deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} 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;
}
if (deleted) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString());
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
}
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;
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),
MOBILE_APP_SETTINGS,
JOB(EntityType.JOB),
AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS);
AI_MODEL(EntityType.AI_MODEL);
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.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
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.UserId;
import org.thingsboard.server.common.data.security.Authority;
@ -58,7 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker);
put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker);
put(Resource.AI_MODEL, aiModelPermissionChecker);
}
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
public boolean hasPermission(SecurityUser user, Operation operation) {
@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
}
@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());
}

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.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.AI_MODEL_SETTINGS
EntityType.AI_MODEL
);
@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.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.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -26,11 +26,11 @@ import java.util.Set;
@Service
@TbCoreComponent
class AiModelSettingsExportService extends BaseEntityExportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> {
class AiModelExportService extends BaseEntityExportService<AiModelId, AiModel, EntityExportData<AiModel>> {
@Override
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 org.springframework.stereotype.Service;
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.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
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.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@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
protected void setOwner(
TenantId tenantId,
AiModelSettings settings,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider
AiModel model,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
settings.setTenantId(tenantId);
model.setTenantId(tenantId);
}
@Override
protected AiModelSettings prepare(
protected AiModel prepare(
EntitiesImportCtx ctx,
AiModelSettings settings,
AiModelSettings oldEntity,
EntityExportData<AiModelSettings> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider
AiModel model,
AiModel oldModel,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
return settings;
return model;
}
@Override
protected AiModelSettings deepCopy(AiModelSettings settings) {
return new AiModelSettings(settings);
protected AiModel deepCopy(AiModel model) {
return new AiModel(model);
}
@Override
protected AiModelSettings saveOrUpdate(
protected AiModel saveOrUpdate(
EntitiesImportCtx ctx,
AiModelSettings settings,
EntityExportData<AiModelSettings> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider,
AiModel model,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider,
CompareResult compareResult
) {
return aiModelSettingsService.save(settings);
return aiModelService.save(model);
}
@Override
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:
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
aiModelSettings:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
aiModel:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:
@ -874,7 +874,7 @@ audit-log:
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels.
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels.
"calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels.
"ai_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:
# Type of external sink. possible options: none, elasticsearch
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;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Optional;
public interface 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 {
private CacheConstants() {}
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations";
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 SENT_NOTIFICATIONS_CACHE = "sentNotifications";
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 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_LINK(40),
JOB(41),
AI_MODEL_SETTINGS(42, "ai_model_settings") {
AI_MODEL(42, "ai_model") {
@Override
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.HasTenantId;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ai.model.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar;
@ -39,7 +39,7 @@ import java.io.Serial;
@Builder
@AllArgsConstructor
@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
private static final long serialVersionUID = 9017108678716011604L;
@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
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"
)
private TenantId tenantId;
@ -55,7 +55,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
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",
defaultValue = "1"
)
@ -67,8 +67,8 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant",
example = "Rule node assistant"
description = "Display name for this AI model configuration; not the technical model identifier",
example = "Fast and cost-efficient model"
)
private String name;
@ -79,24 +79,24 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
accessMode = Schema.AccessMode.READ_WRITE,
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);
}
public AiModelSettings(AiModelSettings settings) {
super(settings.getId());
createdTime = settings.getCreatedTime();
tenantId = settings.getTenantId();
version = settings.getVersion();
name = settings.getName();
configuration = settings.getConfiguration();
externalId = settings.getExternalId() == null ? null : new AiModelSettingsId(settings.getExternalId().getId());
public AiModel(AiModel model) {
super(model.getId());
createdTime = model.getCreatedTime();
tenantId = model.getTenantId();
version = model.getVersion();
name = model.getName();
configuration = model.getConfiguration();
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 jakarta.validation.Valid;
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.List;
@ -51,7 +51,7 @@ public record TbChatRequest(
description = "Configuration of the AI chat model that should execute the request"
)
@NotNull @Valid
AiChatModel<?> chatModel
AiChatModelConfig<?> chatModelConfig
) {
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;
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 {
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;
import dev.langchain4j.model.chat.ChatModel;
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
permits
OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config,
GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config,
AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config {
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
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.AmazonBedrockProviderConfig;
public record AmazonBedrockChatModel(
public record AmazonBedrockChatModelConfig(
AiModelType modelType,
@NotNull @Valid AmazonBedrockProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AmazonBedrockChatModel.Config> {
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {
@Override
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.AnthropicProviderConfig;
public record AnthropicChatModel(
public record AnthropicChatModelConfig(
AiModelType modelType,
@NotNull @Valid AnthropicProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AnthropicChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.AzureOpenAiProviderConfig;
public record AzureOpenAiChatModel(
public record AzureOpenAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid AzureOpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AzureOpenAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GitHubModelsProviderConfig;
public record GitHubModelsChatModel(
public record GitHubModelsChatModelConfig(
AiModelType modelType,
@NotNull @Valid GitHubModelsProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GitHubModelsChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GoogleAiGeminiProviderConfig;
public record GoogleAiGeminiChatModel(
public record GoogleAiGeminiChatModelConfig(
AiModelType modelType,
@NotNull @Valid GoogleAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GoogleAiGeminiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GoogleVertexAiGeminiProviderConfig;
public record GoogleVertexAiGeminiChatModel(
public record GoogleVertexAiGeminiChatModelConfig(
AiModelType modelType,
@NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GoogleVertexAiGeminiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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 {
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.MistralAiProviderConfig;
public record MistralAiChatModel(
public record MistralAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid MistralAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<MistralAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.OpenAiProviderConfig;
public record OpenAiChatModel(
public record OpenAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid OpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<OpenAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.util.UUID;
public final class AiModelSettingsId extends UUIDBased implements EntityId {
public final class AiModelId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 3021036138554389754L;
@JsonCreator
public AiModelSettingsId(@JsonProperty("id") UUID id) {
public AiModelId(@JsonProperty("id") UUID id) {
super(id);
}
@Override
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
description = "Entity type of the AI model settings",
example = "AI_MODEL_SETTINGS",
allowableValues = "AI_MODEL_SETTINGS"
description = "Entity type of the AI model",
example = "AI_MODEL",
allowableValues = "AI_MODEL"
)
public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS;
return EntityType.AI_MODEL;
}
public static AiModelSettingsId fromString(String uuid) {
return new AiModelSettingsId(UUID.fromString(uuid));
public static AiModelId fromString(String 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_LINK -> new CalculatedFieldLinkId(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!");
};
}

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.EntityView;
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.AssetProfile;
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_RULE", value = NotificationRule.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)
public @interface JsonTbEntity {}

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

@ -64,7 +64,7 @@ enum EntityTypeProto {
CALCULATED_FIELD = 39;
CALCULATED_FIELD_LINK = 40;
JOB = 41;
AI_MODEL_SETTINGS = 42;
AI_MODEL = 42;
}
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;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.ai.AiModel;
import static java.util.Objects.requireNonNull;
import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Deleted;
import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Saved;
import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Deleted;
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 {
requireNonNull(cacheKey);
requireNonNull(savedSettings);
requireNonNull(savedModel);
}
}
record Deleted(AiModelSettingsCacheKey cacheKey) implements AiModelSettingsCacheEvictEvent {
record Deleted(AiModelCacheKey cacheKey) implements AiModelCacheEvictEvent {
public Deleted {
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.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.TenantId;
@ -25,22 +25,22 @@ import java.util.UUID;
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(settingsId);
requireNonNull(modelId);
if (TenantId.SYS_TENANT_ID.getId().equals(tenantId)) {
throw new IllegalArgumentException("Tenant ID must not be the system tenant ID");
}
if (EntityId.NULL_UUID.equals(settingsId)) {
throw new IllegalArgumentException("Settings ID must not be reserved null UUID");
if (EntityId.NULL_UUID.equals(modelId)) {
throw new IllegalArgumentException("Model ID must not be reserved null UUID");
}
}
static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) {
return new AiModelSettingsCacheKey(tenantId.getId(), settingsId.getId());
static AiModelCacheKey of(TenantId tenantId, AiModelId modelId) {
return new AiModelCacheKey(tenantId.getId(), modelId.getId());
}
@Override
@ -51,7 +51,7 @@ record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements Versio
@NonNull
@Override
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.thingsboard.server.cache.VersionedCaffeineTbCache;
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)
class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache<AiModelSettingsCacheKey, AiModelSettings> {
class AiModelCaffeineCache extends VersionedCaffeineTbCache<AiModelCacheKey, AiModel> {
AiModelSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE);
AiModelCaffeineCache(CacheManager cacheManager) {
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;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityDao;
@ -24,14 +24,14 @@ import org.thingsboard.server.dao.TenantEntityDao;
import java.util.Optional;
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.VersionedRedisTbCache;
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")
class AiModelSettingsRedisCache extends VersionedRedisTbCache<AiModelSettingsCacheKey, AiModelSettings> {
class AiModelRedisCache extends VersionedRedisTbCache<AiModelCacheKey, AiModel> {
AiModelSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.AI_MODEL_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModelSettings.class));
AiModelRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
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(
EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE,
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

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";
/**
* 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_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN;
public static final String AI_MODEL_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY;
public static final String AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration";
public static final String AI_MODEL_TABLE_NAME = "ai_model";
public static final String AI_MODEL_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN;
public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY;
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};

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 org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.ai.model.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseVersionedEntity;
import org.thingsboard.server.dao.model.ModelConstants;
@ -43,52 +43,52 @@ import java.util.UUID;
@Setter
@ToString
@Entity
@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME)
public class AiModelSettingsEntity extends BaseVersionedEntity<AiModelSettings> {
@Table(name = ModelConstants.AI_MODEL_TABLE_NAME)
public class AiModelEntity extends BaseVersionedEntity<AiModel> {
public static final Map<String, String> COLUMN_MAP = Map.of(
"createdTime", "created_time",
"provider", "(configuration ->> 'provider')",
"modelId", "(configuration -> 'modelConfig' ->> 'modelId')"
"modelId", "(configuration ->> 'modelId')"
);
public static final Set<String> ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet(
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;
@Column(name = ModelConstants.AI_MODEL_SETTINGS_NAME_COLUMN_NAME, nullable = false)
@Column(name = ModelConstants.AI_MODEL_NAME_COLUMN_NAME, nullable = false)
private String name;
@Type(JsonBinaryType.class)
@Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiModel<?> configuration;
@Column(name = ModelConstants.AI_MODEL_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiModelConfig configuration;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID")
private UUID externalId;
public AiModelSettingsEntity() {}
public AiModelEntity() {}
public AiModelSettingsEntity(AiModelSettings aiModelSettings) {
super(aiModelSettings);
tenantId = getTenantUuid(aiModelSettings.getTenantId());
name = aiModelSettings.getName();
configuration = aiModelSettings.getConfiguration();
externalId = getUuid(aiModelSettings.getExternalId());
public AiModelEntity(AiModel aiModel) {
super(aiModel);
tenantId = getTenantUuid(aiModel.getTenantId());
name = aiModel.getName();
configuration = aiModel.getConfiguration();
externalId = getUuid(aiModel.getExternalId());
}
@Override
public AiModelSettings toData() {
var settings = new AiModelSettings(new AiModelSettingsId(id));
settings.setCreatedTime(createdTime);
settings.setVersion(version);
settings.setTenantId(TenantId.fromUUID(tenantId));
settings.setName(name);
settings.setConfiguration(configuration);
settings.setExternalId(getEntityId(externalId, AiModelSettingsId::new));
return settings;
public AiModel toData() {
var model = new AiModel(new AiModelId(id));
model.setCreatedTime(createdTime);
model.setVersion(version);
model.setTenantId(TenantId.fromUUID(tenantId));
model.setName(name);
model.setConfiguration(configuration);
model.setExternalId(getEntityId(externalId, AiModelId::new));
return model;
}
@Override
@ -98,7 +98,7 @@ public class AiModelSettingsEntity extends BaseVersionedEntity<AiModelSettings>
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
AiModelSettingsEntity that = (AiModelSettingsEntity) o;
AiModelEntity that = (AiModelEntity) o;
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 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.dao.ai.AiModelSettingsDao;
import org.thingsboard.server.dao.ai.AiModelDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
@ -28,41 +28,41 @@ import java.util.Optional;
@Component
@RequiredArgsConstructor
class AiModelSettingsDataValidator extends DataValidator<AiModelSettings> {
class AiModelDataValidator extends DataValidator<AiModel> {
private final TenantService tenantService;
private final AiModelSettingsDao aiModelSettingsDao;
private final AiModelDao aiModelDao;
@Override
protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) {
Optional<AiModelSettings> existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId());
protected AiModel validateUpdate(TenantId tenantId, AiModel model) {
Optional<AiModel> existing = aiModelDao.findByTenantIdAndId(tenantId, model.getId());
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();
}
@Override
protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) {
protected void validateDataImpl(TenantId tenantId, AiModel model) {
// ID validation
if (settings.getId() != null) {
if (settings.getUuidId() == null) {
throw new DataValidationException("AI model settings UUID should be specified!");
if (model.getId() != null) {
if (model.getUuidId() == null) {
throw new DataValidationException("AI model UUID should be specified!");
}
if (settings.getId().isNullUid()) {
throw new DataValidationException("AI model settings UUID must not be the reserved null value!");
if (model.getId().isNullUid()) {
throw new DataValidationException("AI model UUID must not be the reserved null value!");
}
}
// tenant ID validation
if (settings.getTenantId() == null || settings.getTenantId().getId() == null) {
throw new DataValidationException("AI model settings should be assigned to tenant!");
if (model.getTenantId() == null || model.getTenantId().getId() == null) {
throw new DataValidationException("AI model should be assigned to tenant!");
}
if (settings.getTenantId().isSysTenantId()) {
throw new DataValidationException("AI model settings cannot be assigned to the system tenant!");
if (model.getTenantId().isSysTenantId()) {
throw new DataValidationException("AI model cannot be assigned to the system tenant!");
}
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.transaction.annotation.Transactional;
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.Set;
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(
value = """
SELECT *
FROM ai_model_settings ai_model
WHERE ai_model.tenant_id = :tenantId
FROM ai_model model
WHERE model.tenant_id = :tenantId
AND (:textSearch IS NULL
OR ai_model.name ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%')
OR model.name ILIKE '%' || :textSearch || '%'
OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%')
""",
countQuery = """
SELECT COUNT(*)
FROM ai_model_settings ai_model
WHERE ai_model.tenant_id = :tenantId
FROM ai_model model
WHERE model.tenant_id = :tenantId
AND (:textSearch IS NULL
OR ai_model.name ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%')
OR model.name ILIKE '%' || :textSearch || '%'
OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%'
OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%')
""",
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);
@Query("SELECT externalId FROM AiModelSettingsEntity WHERE id = :id")
@Query("SELECT externalId FROM AiModelEntity WHERE id = :id")
Optional<UUID> getExternalIdById(@Param("id") UUID id);
long countByTenantId(UUID tenantId);
@Transactional
@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);
@Transactional
@Modifying
@Query(value = """
DELETE FROM ai_model_settings
DELETE FROM ai_model
WHERE tenant_id = :tenantId
RETURNING id
""", nativeQuery = true
@ -83,7 +83,7 @@ interface AiModelSettingsRepository extends JpaRepository<AiModelSettingsEntity,
@Transactional
@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);
}

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.stereotype.Component;
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.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
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.common.data.page.SortOrder;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.ai.AiModelSettingsDao;
import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity;
import org.thingsboard.server.dao.ai.AiModelDao;
import org.thingsboard.server.dao.model.sql.AiModelEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
@ -44,40 +44,40 @@ import static java.util.stream.Collectors.toSet;
@SqlDao
@Component
@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
public Optional<AiModelSettings> findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) {
return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData);
public Optional<AiModel> findByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return aiModelRepository.findByTenantIdAndId(tenantId.getId(), modelId.getId()).map(DaoUtil::getData);
}
@Override
public AiModelSettings findByTenantIdAndName(UUID tenantId, String name) {
return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndName(tenantId, name));
public AiModel findByTenantIdAndName(UUID tenantId, String name) {
return DaoUtil.getData(aiModelRepository.findByTenantIdAndName(tenantId, name));
}
@Override
public AiModelSettings findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndExternalId(tenantId, externalId));
public AiModel findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(aiModelRepository.findByTenantIdAndExternalId(tenantId, externalId));
}
@Override
public PageData<AiModelSettings> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<AiModel> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return findByTenantId(tenantId.getId(), pageLink);
}
@Override
public PageData<AiModelSettings> findByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId(
public PageData<AiModel> findByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(aiModelRepository.findByTenantId(
tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink))
);
}
@Override
public PageData<AiModelSettingsId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new));
public PageData<AiModelId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(aiModelRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelId::new));
}
private static PageRequest toPageRequest(PageLink pageLink) {
@ -88,52 +88,52 @@ class JpaAiModelSettingsDao extends JpaAbstractDao<AiModelSettingsEntity, AiMode
} else {
sort = JpaSort.unsafe(
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);
}
@Override
public AiModelSettingsId getExternalIdByInternal(AiModelSettingsId internalId) {
return aiModelSettingsRepository.getExternalIdById(internalId.getId()).map(AiModelSettingsId::new).orElse(null);
public AiModelId getExternalIdByInternal(AiModelId internalId) {
return aiModelRepository.getExternalIdById(internalId.getId()).map(AiModelId::new).orElse(null);
}
@Override
public Long countByTenantId(TenantId tenantId) {
return aiModelSettingsRepository.countByTenantId(tenantId.getId());
return aiModelRepository.countByTenantId(tenantId.getId());
}
@Override
public boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId) {
return aiModelSettingsRepository.deleteByIdIn(Set.of(settingsId.getId())) > 0;
public boolean deleteById(TenantId tenantId, AiModelId modelId) {
return aiModelRepository.deleteByIdIn(Set.of(modelId.getId())) > 0;
}
@Override
public Set<AiModelSettingsId> deleteByTenantId(TenantId tenantId) {
return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()).stream()
.map(AiModelSettingsId::new)
public Set<AiModelId> deleteByTenantId(TenantId tenantId) {
return aiModelRepository.deleteByTenantId(tenantId.getId()).stream()
.map(AiModelId::new)
.collect(toSet());
}
@Override
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) {
return aiModelSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(settingsId.getId())) > 0;
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return aiModelRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(modelId.getId())) > 0;
}
@Override
public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS;
return EntityType.AI_MODEL;
}
@Override
protected Class<AiModelSettingsEntity> getEntityClass() {
return AiModelSettingsEntity.class;
protected Class<AiModelEntity> getEntityClass() {
return AiModelEntity.class;
}
@Override
protected JpaRepository<AiModelSettingsEntity, UUID> getRepository() {
return aiModelSettingsRepository;
protected JpaRepository<AiModelEntity, UUID> getRepository() {
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_TARGET, EntityType.QUEUE_STATS, EntityType.CUSTOMER,
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_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
);
CREATE TABLE IF NOT EXISTS ai_model_settings (
CREATE TABLE IF NOT EXISTS ai_model (
id UUID NOT NULL PRIMARY KEY,
external_id UUID,
created_time BIGINT NOT NULL,
@ -970,6 +970,6 @@ CREATE TABLE IF NOT EXISTS ai_model_settings (
version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL,
CONSTRAINT ai_model_settings_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT ai_model_settings_external_id_unq_key UNIQUE (tenant_id, external_id)
CONSTRAINT ai_model_name_unq_key UNIQUE (tenant_id, name),
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 dev.langchain4j.model.chat.request.ChatRequest;
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;
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.msg.TbMsg;
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.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -423,9 +423,9 @@ public interface TbContext {
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

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.util.TbNodeUtils;
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.chat.AiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbMsg;
@ -65,7 +64,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
private String userPrompt;
private ResponseFormat responseFormat;
private int timeoutSeconds;
private AiModelSettingsId modelSettingsId;
private AiModelId modelId;
@Override
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);
}
// 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) {
responseFormat = config.getResponseFormat().toLangChainResponseFormat();
}
@ -87,15 +86,15 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
systemPrompt = config.getSystemPrompt();
userPrompt = config.getUserPrompt();
timeoutSeconds = config.getTimeoutSeconds();
modelSettingsId = config.getAiModelSettingsId();
modelId = config.getAiModelId();
Optional<AiModelSettings> modelSettings = ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndId(ctx.getTenantId(), modelSettingsId);
if (modelSettings.isEmpty()) {
throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found", true);
Optional<AiModel> model = ctx.getAiModelService().findAiModelByTenantIdAndId(ctx.getTenantId(), modelId);
if (model.isEmpty()) {
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) {
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) {
return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transformAsync(settingsOpt -> {
if (settingsOpt.isEmpty()) {
throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found");
return ctx.getAiModelService().findAiModelByTenantIdAndIdAsync(ctx.getTenantId(), modelId).transformAsync(modelOpt -> {
if (modelOpt.isEmpty()) {
throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found");
}
AiModelSettings settings = settingsOpt.get();
AiModelType modelType = settings.getConfiguration().modelType();
AiModel model = modelOpt.get();
AiModelType modelType = model.getConfiguration().modelType();
if (modelType != AiModelType.CHAT) {
throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType);
throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType);
}
@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)
.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());
}
@ -174,7 +173,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode {
systemPrompt = null;
userPrompt = 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 lombok.Data;
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 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> {
@NotNull
private AiModelSettingsId aiModelSettingsId;
private AiModelId aiModelId;
@Pattern(regexp = ".*\\S.*", message = "must not be blank")
@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.HasTenantId;
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.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
@ -180,8 +180,8 @@ public class TenantIdLoader {
case JOB:
tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id));
break;
case AI_MODEL_SETTINGS:
tenantEntity = ctx.getAiModelSettingsService().findAiModelSettingsById(ctxTenantId, new AiModelSettingsId(id)).orElse(null);
case AI_MODEL:
tenantEntity = ctx.getAiModelService().findAiModelById(ctxTenantId, new AiModelId(id)).orElse(null);
break;
default:
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.TenantProfile;
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.asset.Asset;
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.widget.WidgetType;
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.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
@ -168,7 +168,7 @@ public class TenantIdLoaderTest {
@Mock
private JobService jobService;
@Mock
private AiModelSettingsService aiModelSettingsService;
private AiModelService aiModelService;
private TenantId tenantId;
private TenantProfileId tenantProfileId;
@ -434,11 +434,11 @@ public class TenantIdLoaderTest {
when(ctx.getJobService()).thenReturn(jobService);
doReturn(job).when(jobService).findJobById(eq(tenantId), any());
break;
case AI_MODEL_SETTINGS:
AiModelSettings aiModelSettings = new AiModelSettings();
aiModelSettings.setTenantId(tenantId);
when(ctx.getAiModelSettingsService()).thenReturn(aiModelSettingsService);
doReturn(Optional.of(aiModelSettings)).when(aiModelSettingsService).findAiModelSettingsById(eq(tenantId), any());
case AI_MODEL:
AiModel aiModel = new AiModel();
aiModel.setTenantId(tenantId);
when(ctx.getAiModelService()).thenReturn(aiModelService);
doReturn(Optional.of(aiModel)).when(aiModelService).findAiModelById(eq(tenantId), any());
break;
default:
throw new RuntimeException("Unexpected originator EntityType " + entityType);

Loading…
Cancel
Save